将RGB转换为HSV和HSV转换为RGB的算法(范围为0-255)

122

我正在寻找一款将RGB颜色空间转换为HSV颜色空间的工具,特别是针对0到255范围内的两种颜色空间。

14个回答

165

我已经使用这些工具很长时间了——现在不知道它们来自哪里...请注意,除了以度为单位的角度之外,输入和输出的范围都在0到1.0之间。

注意:此代码对输入没有进行真正的合理性检查。请谨慎使用!

typedef struct {
    double r;       // a fraction between 0 and 1
    double g;       // a fraction between 0 and 1
    double b;       // a fraction between 0 and 1
} rgb;

typedef struct {
    double h;       // angle in degrees
    double s;       // a fraction between 0 and 1
    double v;       // a fraction between 0 and 1
} hsv;

static hsv   rgb2hsv(rgb in);
static rgb   hsv2rgb(hsv in);

hsv rgb2hsv(rgb in)
{
    hsv         out;
    double      min, max, delta;

    min = in.r < in.g ? in.r : in.g;
    min = min  < in.b ? min  : in.b;

    max = in.r > in.g ? in.r : in.g;
    max = max  > in.b ? max  : in.b;

    out.v = max;                                // v
    delta = max - min;
    if (delta < 0.00001)
    {
        out.s = 0;
        out.h = 0; // undefined, maybe nan?
        return out;
    }
    if( max > 0.0 ) { // NOTE: if Max is == 0, this divide would cause a crash
        out.s = (delta / max);                  // s
    } else {
        // if max is 0, then r = g = b = 0              
        // s = 0, h is undefined
        out.s = 0.0;
        out.h = NAN;                            // its now undefined
        return out;
    }
    if( in.r >= max )                           // > is bogus, just keeps compilor happy
        out.h = ( in.g - in.b ) / delta;        // between yellow & magenta
    else
    if( in.g >= max )
        out.h = 2.0 + ( in.b - in.r ) / delta;  // between cyan & yellow
    else
        out.h = 4.0 + ( in.r - in.g ) / delta;  // between magenta & cyan

    out.h *= 60.0;                              // degrees

    if( out.h < 0.0 )
        out.h += 360.0;

    return out;
}


rgb hsv2rgb(hsv in)
{
    double      hh, p, q, t, ff;
    long        i;
    rgb         out;

    if(in.s <= 0.0) {       // < is bogus, just shuts up warnings
        out.r = in.v;
        out.g = in.v;
        out.b = in.v;
        return out;
    }
    hh = in.h;
    if(hh >= 360.0) hh = 0.0;
    hh /= 60.0;
    i = (long)hh;
    ff = hh - i;
    p = in.v * (1.0 - in.s);
    q = in.v * (1.0 - (in.s * ff));
    t = in.v * (1.0 - (in.s * (1.0 - ff)));

    switch(i) {
    case 0:
        out.r = in.v;
        out.g = t;
        out.b = p;
        break;
    case 1:
        out.r = q;
        out.g = in.v;
        out.b = p;
        break;
    case 2:
        out.r = p;
        out.g = in.v;
        out.b = t;
        break;

    case 3:
        out.r = p;
        out.g = q;
        out.b = in.v;
        break;
    case 4:
        out.r = t;
        out.g = p;
        out.b = in.v;
        break;
    case 5:
    default:
        out.r = in.v;
        out.g = p;
        out.b = q;
        break;
    }
    return out;     
}

13
如果你做数学计算,结果应该是相等的,但如果你使用这个结果,可能会收到一个有关比较浮点数的投诉。虽然从理论上讲它不可能大于,但使用">="而不是"=="可以消除我在Mac上使用llvm/Xcode时遇到的编译器错误。 - David H
4
@Gerard out 是以度为单位的。60 度是一个完整圆的六分之一。这不是弧度。 - David H
4
>=和编译器错误的原因是大多数编译器中double == double无效并且非法。浮点算术和浮点存储意味着两个值可以在_近似_值上相等,但存储的值可能不相等,即使它们从公式上来看是相同的。应该使用abs(double_a - double_b) <= epsilon进行比较,其中epsilon是某个值,通常为1e-4 - Brandon LeBlanc
7
“double == double在大多数编译器中是无效和非法的”并不准确。比较两个浮点数是否相等是完全明确定义合法的操作。没有主流或符合标准的编译器会阻止你这样做。问题在于你可能得不到实际想要的答案,并且可能本意是执行一个更松散的比较。 - Lightness Races in Orbit
2
实际上,有人这样说过,引用原话是“在大多数编译器中无效和非法”。而且使用<=仍然使用了==,最好只使用<。这些只是小问题,上面的代码非常有帮助。 :) - ForeverZer0
显示剩余17条评论

50
您也可以尝试不使用浮点数的代码(更快但不够准确):
typedef struct RgbColor
{
    unsigned char r;
    unsigned char g;
    unsigned char b;
} RgbColor;

typedef struct HsvColor
{
    unsigned char h;
    unsigned char s;
    unsigned char v;
} HsvColor;

RgbColor HsvToRgb(HsvColor hsv)
{
    RgbColor rgb;
    unsigned char region, remainder, p, q, t;
    
    if (hsv.s == 0)
    {
        rgb.r = hsv.v;
        rgb.g = hsv.v;
        rgb.b = hsv.v;
        return rgb;
    }
    
    region = hsv.h / 43;
    remainder = (hsv.h - (region * 43)) * 6; 
    
    p = (hsv.v * (255 - hsv.s)) >> 8;
    q = (hsv.v * (255 - ((hsv.s * remainder) >> 8))) >> 8;
    t = (hsv.v * (255 - ((hsv.s * (255 - remainder)) >> 8))) >> 8;
    
    switch (region)
    {
        case 0:
            rgb.r = hsv.v; rgb.g = t; rgb.b = p;
            break;
        case 1:
            rgb.r = q; rgb.g = hsv.v; rgb.b = p;
            break;
        case 2:
            rgb.r = p; rgb.g = hsv.v; rgb.b = t;
            break;
        case 3:
            rgb.r = p; rgb.g = q; rgb.b = hsv.v;
            break;
        case 4:
            rgb.r = t; rgb.g = p; rgb.b = hsv.v;
            break;
        default:
            rgb.r = hsv.v; rgb.g = p; rgb.b = q;
            break;
    }
    
    return rgb;
}

HsvColor RgbToHsv(RgbColor rgb)
{
    HsvColor hsv;
    unsigned char rgbMin, rgbMax;

    rgbMin = rgb.r < rgb.g ? (rgb.r < rgb.b ? rgb.r : rgb.b) : (rgb.g < rgb.b ? rgb.g : rgb.b);
    rgbMax = rgb.r > rgb.g ? (rgb.r > rgb.b ? rgb.r : rgb.b) : (rgb.g > rgb.b ? rgb.g : rgb.b);
    
    hsv.v = rgbMax;
    if (hsv.v == 0)
    {
        hsv.h = 0;
        hsv.s = 0;
        return hsv;
    }

    hsv.s = 255 * long(rgbMax - rgbMin) / hsv.v;
    if (hsv.s == 0)
    {
        hsv.h = 0;
        return hsv;
    }

    if (rgbMax == rgb.r)
        hsv.h = 0 + 43 * (rgb.g - rgb.b) / (rgbMax - rgbMin);
    else if (rgbMax == rgb.g)
        hsv.h = 85 + 43 * (rgb.b - rgb.r) / (rgbMax - rgbMin);
    else
        hsv.h = 171 + 43 * (rgb.r - rgb.g) / (rgbMax - rgbMin);

    return hsv;
}

请注意,该算法使用 0-255 作为其范围(而不是 0-360),因为这是该问题的作者所要求的。

9
您可以将所有16,777,216种可能的RGB颜色转换为HSV并再次转换为RGB。不幸的是,使用此算法,您会发现某些颜色无法很好地进行双向转换。也许它们在感知上看起来差不多,但数字上存在实质性的差异,例如(0,237,11)将会通过回路转换为(0,237,0)等。但是,如果使用基于浮点计算的David H算法,则不会出现这种情况。 - Martin Liversage
3
@rightaway717 - 这给了我全部范围,也许你正在使用0-360作为范围?这个算法(谢天谢地)使用0x00-0xFF作为它的范围。 - Anne Quinn
@AnneQuinn 没错! 我本来以为它应该是0-360的,但当我看到被接受的答案可以正常工作时,我就没有足够的热情去找出问题所在了。虽然感谢Leszek发布了这个答案,但我认为他应该在答案中提到色调范围。 - rightaway717

26

我用HLSL为我们的渲染引擎编写了这段代码,其中没有任何条件判断:

    float3  HSV2RGB( float3 _HSV )
    {
        _HSV.x = fmod( 100.0 + _HSV.x, 1.0 );                                       // Ensure [0,1[

        float   HueSlice = 6.0 * _HSV.x;                                            // In [0,6[
        float   HueSliceInteger = floor( HueSlice );
        float   HueSliceInterpolant = HueSlice - HueSliceInteger;                   // In [0,1[ for each hue slice

        float3  TempRGB = float3(   _HSV.z * (1.0 - _HSV.y),
                                    _HSV.z * (1.0 - _HSV.y * HueSliceInterpolant),
                                    _HSV.z * (1.0 - _HSV.y * (1.0 - HueSliceInterpolant)) );

        // The idea here to avoid conditions is to notice that the conversion code can be rewritten:
        //    if      ( var_i == 0 ) { R = V         ; G = TempRGB.z ; B = TempRGB.x }
        //    else if ( var_i == 2 ) { R = TempRGB.x ; G = V         ; B = TempRGB.z }
        //    else if ( var_i == 4 ) { R = TempRGB.z ; G = TempRGB.x ; B = V     }
        // 
        //    else if ( var_i == 1 ) { R = TempRGB.y ; G = V         ; B = TempRGB.x }
        //    else if ( var_i == 3 ) { R = TempRGB.x ; G = TempRGB.y ; B = V     }
        //    else if ( var_i == 5 ) { R = V         ; G = TempRGB.x ; B = TempRGB.y }
        //
        // This shows several things:
        //  . A separation between even and odd slices
        //  . If slices (0,2,4) and (1,3,5) can be rewritten as basically being slices (0,1,2) then
        //      the operation simply amounts to performing a "rotate right" on the RGB components
        //  . The base value to rotate is either (V, B, R) for even slices or (G, V, R) for odd slices
        //
        float   IsOddSlice = fmod( HueSliceInteger, 2.0 );                          // 0 if even (slices 0, 2, 4), 1 if odd (slices 1, 3, 5)
        float   ThreeSliceSelector = 0.5 * (HueSliceInteger - IsOddSlice);          // (0, 1, 2) corresponding to slices (0, 2, 4) and (1, 3, 5)

        float3  ScrollingRGBForEvenSlices = float3( _HSV.z, TempRGB.zx );           // (V, Temp Blue, Temp Red) for even slices (0, 2, 4)
        float3  ScrollingRGBForOddSlices = float3( TempRGB.y, _HSV.z, TempRGB.x );  // (Temp Green, V, Temp Red) for odd slices (1, 3, 5)
        float3  ScrollingRGB = lerp( ScrollingRGBForEvenSlices, ScrollingRGBForOddSlices, IsOddSlice );

        float   IsNotFirstSlice = saturate( ThreeSliceSelector );                   // 1 if NOT the first slice (true for slices 1 and 2)
        float   IsNotSecondSlice = saturate( ThreeSliceSelector-1.0 );              // 1 if NOT the first or second slice (true only for slice 2)

        return  lerp( ScrollingRGB.xyz, lerp( ScrollingRGB.zxy, ScrollingRGB.yzx, IsNotSecondSlice ), IsNotFirstSlice );    // Make the RGB rotate right depending on final slice index
    }

2
你是否有使用相同方法进行RGB2HSV的双向转换? - Carlos Barcellos

9

这是一个基于Agoston的计算机图形学与几何建模:实现与算法第304页的C语言实现,其中H∈[0,360],SV∈[0,1]。

#include <math.h>

typedef struct {
    double r;       // ∈ [0, 1]
    double g;       // ∈ [0, 1]
    double b;       // ∈ [0, 1]
} rgb;

typedef struct {
    double h;       // ∈ [0, 360]
    double s;       // ∈ [0, 1]
    double v;       // ∈ [0, 1]
} hsv;

rgb hsv2rgb(hsv HSV)
{
    rgb RGB;
    double H = HSV.h, S = HSV.s, V = HSV.v,
            P, Q, T,
            fract;

    (H == 360.)?(H = 0.):(H /= 60.);
    fract = H - floor(H);

    P = V*(1. - S);
    Q = V*(1. - S*fract);
    T = V*(1. - S*(1. - fract));

    if      (0. <= H && H < 1.)
        RGB = (rgb){.r = V, .g = T, .b = P};
    else if (1. <= H && H < 2.)
        RGB = (rgb){.r = Q, .g = V, .b = P};
    else if (2. <= H && H < 3.)
        RGB = (rgb){.r = P, .g = V, .b = T};
    else if (3. <= H && H < 4.)
        RGB = (rgb){.r = P, .g = Q, .b = V};
    else if (4. <= H && H < 5.)
        RGB = (rgb){.r = T, .g = P, .b = V};
    else if (5. <= H && H < 6.)
        RGB = (rgb){.r = V, .g = P, .b = Q};
    else
        RGB = (rgb){.r = 0., .g = 0., .b = 0.};

    return RGB;
}

有没有类似的C代码可以将HSV转换为RGB?谢谢! - user3236841
@user3236841,该伪代码在Agoston的《计算机图形学和几何建模:实现与算法》(https://isidore.co/calibre/browse/book/5588)的上一页(第303页)中。 - Geremia
1
请检查 - 这是HSL2RGB,而不是HSV。 - Kromster

7

@fins的答案在将饱和度降低时存在Arduino溢出问题。这里将一些值转换为int来防止此问题。

typedef struct RgbColor
{
    unsigned char r;
    unsigned char g;
    unsigned char b;
} RgbColor;

typedef struct HsvColor
{
    unsigned char h;
    unsigned char s;
    unsigned char v;
} HsvColor;

RgbColor HsvToRgb(HsvColor hsv)
{
    RgbColor rgb;
    unsigned char region, p, q, t;
    unsigned int h, s, v, remainder;

    if (hsv.s == 0)
    {
        rgb.r = hsv.v;
        rgb.g = hsv.v;
        rgb.b = hsv.v;
        return rgb;
    }

    // converting to 16 bit to prevent overflow
    h = hsv.h;
    s = hsv.s;
    v = hsv.v;

    region = h / 43;
    remainder = (h - (region * 43)) * 6; 

    p = (v * (255 - s)) >> 8;
    q = (v * (255 - ((s * remainder) >> 8))) >> 8;
    t = (v * (255 - ((s * (255 - remainder)) >> 8))) >> 8;

    switch (region)
    {
        case 0:
            rgb.r = v;
            rgb.g = t;
            rgb.b = p;
            break;
        case 1:
            rgb.r = q;
            rgb.g = v;
            rgb.b = p;
            break;
        case 2:
            rgb.r = p;
            rgb.g = v;
            rgb.b = t;
            break;
        case 3:
            rgb.r = p;
            rgb.g = q;
            rgb.b = v;
            break;
        case 4:
            rgb.r = t;
            rgb.g = p;
            rgb.b = v;
            break;
        default:
            rgb.r = v;
            rgb.g = p;
            rgb.b = q;
            break;
    }

    return rgb;
}

HsvColor RgbToHsv(RgbColor rgb)
{
    HsvColor hsv;
    unsigned char rgbMin, rgbMax;

    rgbMin = rgb.r < rgb.g ? (rgb.r < rgb.b ? rgb.r : rgb.b) : (rgb.g < rgb.b ? rgb.g : rgb.b);
    rgbMax = rgb.r > rgb.g ? (rgb.r > rgb.b ? rgb.r : rgb.b) : (rgb.g > rgb.b ? rgb.g : rgb.b);

    hsv.v = rgbMax;
    if (hsv.v == 0)
    {
        hsv.h = 0;
        hsv.s = 0;
        return hsv;
    }

    hsv.s = 255 * ((long)(rgbMax - rgbMin)) / hsv.v;
    if (hsv.s == 0)
    {
        hsv.h = 0;
        return hsv;
    }

    if (rgbMax == rgb.r)
        hsv.h = 0 + 43 * (rgb.g - rgb.b) / (rgbMax - rgbMin);
    else if (rgbMax == rgb.g)
        hsv.h = 85 + 43 * (rgb.b - rgb.r) / (rgbMax - rgbMin);
    else
        hsv.h = 171 + 43 * (rgb.r - rgb.g) / (rgbMax - rgbMin);

    return hsv;
}

7

这应该放在这里:

它仍然有效。与上面的比较,看起来很不错。

hlsl代码

        float3 Hue(float H)
        {
            half R = abs(H * 6 - 3) - 1;
            half G = 2 - abs(H * 6 - 2);
            half B = 2 - abs(H * 6 - 4);
            return saturate(half3(R,G,B));
        }

        half4 HSVtoRGB(in half3 HSV)
        {
            return half4(((Hue(HSV.x) - 1) * HSV.y + 1) * HSV.z,1);
        }

float3是一个16位精度的向量3数据类型,即float3 hue()返回一个数据类型(x,y,z),例如(r,g,b),half与半精度相同,为8位,而float4则是4个值(r,g,b,a)。


3
需要为 halfhalf4half3float3 等类型定义一些类型。 - Quuxplusone
1
half4是颜色(r,g,b,a)或任何4个半精度浮点数,也可以是全精度的,它只是一个vector4。 - bandybabboon
1
什么是saturate()? - TatiOverflow
saturate() 在 HLSL 代码参考中可用:saturate(x) 将 x 限制/夹紧在 0 和 1 之间。https://learn.microsoft.com/en-us/windows/desktop/direct3dhlsl/dx-graphics-hlsl-saturate - bandybabboon
你能解释一下HSVtoRGB中的return语句吗?它似乎是由色调乘以一个标量得到的三元素RGB向量 - 结果类似于[kr,kg,k*b,1]。 - pathfinder
抱歉,我没有设计算法,所以我没有经过数学计算,我只是将它复制到一些工作中,然后在这里发布。 - bandybabboon

5
这不是C语言,但它确实有效。我在这里看到的所有其他方法都是将一切转化为六边形的一部分,并从中近似“角度”。通过改用使用余弦的不同方程,并解决h s和v的值,您可以得到更好的hsv和rgb之间的关系,并使过渡更加平滑(代价是速度变慢)。
假设一切都是浮点数。如果r g和b从0到1,h从0到2pi,v从0到4/3,s从0到2/3。
以下代码用Lua编写。它很容易转换为任何其他语言。
local hsv do
    hsv         ={}
    local atan2 =math.atan2
    local cos   =math.cos
    local sin   =math.sin

    function hsv.fromrgb(r,b,g)
        local c=r+g+b
        if c<1e-4 then
            return 0,2/3,0
        else
            local p=2*(b*b+g*g+r*r-g*r-b*g-b*r)^0.5
            local h=atan2(b-g,(2*r-b-g)/3^0.5)
            local s=p/(c+p)
            local v=(c+p)/3
            return h,s,v
        end
    end

    function hsv.torgb(h,s,v)
        local r=v*(1+s*(cos(h)-1))
        local g=v*(1+s*(cos(h-2.09439)-1))
        local b=v*(1+s*(cos(h+2.09439)-1))
        return r,g,b
    end

    function hsv.tween(h0,s0,v0,h1,s1,v1,t)
        local dh=(h1-h0+3.14159)%6.28318-3.14159
        local h=h0+t*dh
        local s=s0+t*(s1-s0)
        local v=v0+t*(v1-v0)
        return h,s,v
    end
end

你能解释一下这个算法的推导过程,或者至少指出其中的基本关系吗?我希望找到一些仅包含单个RGB分量的特定色调 - 但是hsv.torgb函数表明在这个算法中这是不可能的。维基百科展示了HSV和RGB之间预期的关系图像 - oclyke

2

基于Patapom的答案,GLSL着色器版本:

vec3 HSV2RGB( vec3 hsv )
{
    hsv.x = mod( 100.0 + hsv.x, 1.0 ); // Ensure [0,1[
    float   HueSlice = 6.0 * hsv.x; // In [0,6[
    float   HueSliceInteger = floor( HueSlice );
    float   HueSliceInterpolant = HueSlice - HueSliceInteger; // In [0,1[ for each hue slice
    vec3  TempRGB = vec3(   hsv.z * (1.0 - hsv.y), hsv.z * (1.0 - hsv.y * HueSliceInterpolant), hsv.z * (1.0 - hsv.y * (1.0 - HueSliceInterpolant)) );
    float   IsOddSlice = mod( HueSliceInteger, 2.0 ); // 0 if even (slices 0, 2, 4), 1 if odd (slices 1, 3, 5)
    float   ThreeSliceSelector = 0.5 * (HueSliceInteger - IsOddSlice); // (0, 1, 2) corresponding to slices (0, 2, 4) and (1, 3, 5)
    vec3  ScrollingRGBForEvenSlices = vec3( hsv.z, TempRGB.zx );           // (V, Temp Blue, Temp Red) for even slices (0, 2, 4)
    vec3  ScrollingRGBForOddSlices = vec3( TempRGB.y, hsv.z, TempRGB.x );  // (Temp Green, V, Temp Red) for odd slices (1, 3, 5)
    vec3  ScrollingRGB = mix( ScrollingRGBForEvenSlices, ScrollingRGBForOddSlices, IsOddSlice );
    float   IsNotFirstSlice = clamp( ThreeSliceSelector, 0.0,1.0 );                   // 1 if NOT the first slice (true for slices 1 and 2)
    float   IsNotSecondSlice = clamp( ThreeSliceSelector-1.0, 0.0,1. );              // 1 if NOT the first or second slice (true only for slice 2)
    return  mix( ScrollingRGB.xyz, mix( ScrollingRGB.zxy, ScrollingRGB.yzx, IsNotSecondSlice ), IsNotFirstSlice );    // Make the RGB rotate right depending on final slice index
}

2
这里有一个在线转换器,其中还有一篇文章介绍了所有颜色转换算法。
你可能更喜欢现成的C版本,但应用起来不会太麻烦,而且它可以帮助其他想要使用其他语言或颜色空间进行相同操作的人。

2

我不是C++开发人员,所以我不会提供代码。但是我可以提供一个简单的hsv2rgb算法(rgb2hsv 在这里),这是我最近发现的 - 我更新了维基百科并附上了描述:HSVHLS。主要改进是我仔细观察r、g、b作为色相函数,并引入更简单的形状函数来描述它们(不失准确性)。该算法 - 输入参数为:h(0-255)、s(0-255)、v(0-255)

r = 255*f(5),   g = 255*f(3),   b = 255*f(1)

我们使用如下描述的函数f:
f(n) = v/255 - (v/255)*(s/255)*max(min(k,4-k,1),0)

其中 (mod 可以返回小数部分; k 是浮点数)

k = (n+h*360/(255*60)) mod 6;

这里有一些关于JavaScript中的SO中片段/观点: HSVHSL。请注意,保留了HTML标记。

你好 Kamil!我正在尝试使用你的算法,有一个关于 min(k,4-k,1) 部分的问题。为什么有三个值,这里到底发生了什么?提前感谢你的回答! - Eugene Alexeev
@EugeneAlexeev 我修复了维基百科上的文章(有人破坏了它),并在这里更新了链接,所以如果想更深入地了解,请阅读此文 - Kamil Kiełczewski

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接