RGB颜色的色相偏移

43

我正在尝试编写一个函数来改变RGB颜色的色调。具体来说,我将在iOS应用程序中使用它,但数学原理是通用的。

下面的图显示了R、G和B值相对于色调的变化。

RGB值在色调上的变化图

从这个图可以看出,编写一个函数来改变色调似乎应该是相对简单的,而不必进行任何可能会引入更多误差的其他颜色格式的转换(如果继续对颜色应用小的变化,则可能会成为问题),我认为这种方法也更加计算密集。

这是我到目前为止的代码,它有点有效但并不完美。如果你需要改变纯黄色、青色或洋红色的颜色,那么它是完美的,但在其他一些地方会有一些小偏差。

Color4f ShiftHue(Color4f c, float d) {
    if (d==0) {
        return c;
    }
    while (d<0) {
        d+=1;
    }

    d *= 3;

    float original[] = {c.red, c.green, c.blue};
    float returned[] = {c.red, c.green, c.blue};

    // big shifts
    for (int i=0; i<3; i++) {
        returned[i] = original[(i+((int) d))%3];
    }
    d -= (float) ((int) d);
    original[0] = returned[0];
    original[1] = returned[1];
    original[2] = returned[2];

    float lower = MIN(MIN(c.red, c.green), c.blue);
    float upper = MAX(MAX(c.red, c.green), c.blue);

    float spread = upper - lower;
    float shift  = spread * d * 2;

    // little shift
    for (int i = 0; i < 3; ++i) {
        // if middle value
        if (original[(i+2)%3]==upper && original[(i+1)%3]==lower) {
            returned[i] -= shift;
            if (returned[i]<lower) {
                returned[(i+1)%3] += lower - returned[i];
                returned[i]=lower;
            } else
                if (returned[i]>upper) {
                    returned[(i+2)%3] -= returned[i] - upper;
                    returned[i]=upper;
                }
            break;
        }
    }

    return Color4fMake(returned[0], returned[1], returned[2], c.alpha);
}

我知道你可以使用UIColor,通过类似以下的方式改变色调:

CGFloat hue;
CGFloat sat;
CGFloat bri;
[[UIColor colorWithRed:parent.color.red green:parent.color.green blue:parent.color.blue alpha:1] getHue:&hue saturation:&sat brightness:&bri alpha:nil];
hue -= .03;
if (hue<0) {
    hue+=1;
}
UIColor *tempColor = [UIColor colorWithHue:hue saturation:sat brightness:bri alpha:1];
const float* components= CGColorGetComponents(tempColor.CGColor);
color = Color4fMake(components[0], components[1], components[2], 1);

但我不太喜欢这种方法,因为它只适用于iOS 5,并且需要分配多个颜色对象、从RGB转换为HSB再转回来,似乎有些过度了。

也许我最终会使用查找表或在我的应用程序中预先计算颜色,但我真的很想知道是否有办法使我的代码工作。谢谢!


1
我还没有阅读你的代码,但根据那张图,你不需要将RGB颜色转换为HSV以便确定你在图表上的位置,以便你可以计算出如何移动吗? - Oliver Charlesworth
15个回答

58

RGB颜色空间描述了一个立方体。可以将此立方体沿对角线轴从(0,0,0)旋转到(255,255,255)以改变色调。注意,某些结果将落在0到255范围之外,需要进行剪裁。

我终于有机会编写这个算法了。它是用Python编写的,但是很容易翻译成您选择的语言。3D旋转的公式来自http://en.wikipedia.org/wiki/Rotation_matrix#Rotation_matrix_from_axis_and_angle

编辑:如果您看到我之前发布的代码,请忽略它。我非常着急地想找到旋转的公式,所以将基于矩阵的解决方案转换为公式,没有意识到矩阵一直是最佳形式。我仍然使用常量sqrt(1/3)简化了矩阵的计算,但这更接近参考文献,并且在逐像素计算的apply中更简单。

from math import sqrt,cos,sin,radians

def clamp(v):
    if v < 0:
        return 0
    if v > 255:
        return 255
    return int(v + 0.5)

class RGBRotate(object):
    def __init__(self):
        self.matrix = [[1,0,0],[0,1,0],[0,0,1]]

    def set_hue_rotation(self, degrees):
        cosA = cos(radians(degrees))
        sinA = sin(radians(degrees))
        self.matrix[0][0] = cosA + (1.0 - cosA) / 3.0
        self.matrix[0][1] = 1./3. * (1.0 - cosA) - sqrt(1./3.) * sinA
        self.matrix[0][2] = 1./3. * (1.0 - cosA) + sqrt(1./3.) * sinA
        self.matrix[1][0] = 1./3. * (1.0 - cosA) + sqrt(1./3.) * sinA
        self.matrix[1][1] = cosA + 1./3.*(1.0 - cosA)
        self.matrix[1][2] = 1./3. * (1.0 - cosA) - sqrt(1./3.) * sinA
        self.matrix[2][0] = 1./3. * (1.0 - cosA) - sqrt(1./3.) * sinA
        self.matrix[2][1] = 1./3. * (1.0 - cosA) + sqrt(1./3.) * sinA
        self.matrix[2][2] = cosA + 1./3. * (1.0 - cosA)

    def apply(self, r, g, b):
        rx = r * self.matrix[0][0] + g * self.matrix[0][1] + b * self.matrix[0][2]
        gx = r * self.matrix[1][0] + g * self.matrix[1][1] + b * self.matrix[1][2]
        bx = r * self.matrix[2][0] + g * self.matrix[2][1] + b * self.matrix[2][2]
        return clamp(rx), clamp(gx), clamp(bx)

这里是上述内容的一些结果:

色相旋转示例

你可以在http://www.graficaobscura.com/matrix/index.html找到同一想法的不同实现。


3
刚刚节省了我好几个小时。这个为什么没有更多的点赞?! - Escher
@Escher 有很多原因 - 1. 我花了好几天的时间来完全阐述答案。2. 这不是很多人需要做的事情。3. 将其转换为带有色调组件的颜色空间的明显解决方案简单且对许多人来说已经足够好用。 - Mark Ransom
1
@AlicanC 当我编写代码时,我认为很明显需要针对每个像素调用apply,而set_hue_rotation仅用于设置。我想我错了。 - Mark Ransom
1
我发现这个解决方案是我在SO上找到的所有方案中最好的。色调旋转非常好。 - AndreaBogazzi
2
@Attila 一个256x256x256的立方体的对角线比256还要长,所以当你旋转它时,那些角落就会突出来。这是因为RGB描述的是一个立方体而不是一个球体。 - Mark Ransom
显示剩余5条评论

18

编辑:根据评论将“都是”改为“可以通过线性逼近”。
编辑2:添加偏置。


本质上,您想要的步骤是

RBG->HSV->Update hue->RGB

由于这些颜色可以通过线性矩阵变换进行近似(即它们是可结合的),因此您可以在不需要任何麻烦的转换或精度损失的情况下一次性执行它。只需将变换矩阵相乘,并使用它来转换您的颜色。

这里有一个快速的步骤,http://beesbuzz.biz/code/hsv_color_transforms.php

这是C++代码(去掉了饱和度和值的变换):

Color TransformH(
    const Color &in,  // color to transform
    float H
)
{
  float U = cos(H*M_PI/180);
  float W = sin(H*M_PI/180);

  Color ret;
  ret.r = (.299+.701*U+.168*W)*in.r
    + (.587-.587*U+.330*W)*in.g
    + (.114-.114*U-.497*W)*in.b;
  ret.g = (.299-.299*U-.328*W)*in.r
    + (.587+.413*U+.035*W)*in.g
    + (.114-.114*U+.292*W)*in.b;
  ret.b = (.299-.3*U+1.25*W)*in.r
    + (.587-.588*U-1.05*W)*in.g
    + (.114+.886*U-.203*W)*in.b;
  return ret;
}

19
作为您所链接页面的原始作者,我想指出RGB->HSV和HSV->RGB不是线性矩阵变换。那段代码实际上是将RGB转换为YIQ(它是HSV的线性等价物),并通过IQ平面进行旋转。有时它也不能产生人们期望的结果。然而,试图解释为什么HSV本质上是一个荒谬的颜色概念,这超出了这个评论框的范围 :) - fluffy
我无法使用您的方法重现正确的结果,然而Mark Ransom的答案效果很好。这是一个例子:输入([R,G,B],H) = ([86,52,30], 210),使用您的方法输出为[-31,15,2],而使用Mark的方法输出为[36,43,88]。我不认为舍入误差可以解释这么大的差异,一定有问题。 - MasterHD
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Jacob Eggers
@mcd 写道:"在最后一行计算中有一个符号错误。 + (.114-.886*U-.203*W)*in.b; 应该为 + (.114+.886*U-.203*W)*in.b;"。(楼主声望不足以发表评论。) - Nisse Engström
@ashes999,这几乎肯定不是重要数字的问题,更多的是期望错位的问题。YIQ颜色空间色域不是一个圆形,因此旋转它可能会导致点落在原始矩形之外。此外,无论点表示什么(颜色、位置等),都不应该对一个点进行多次旋转。 - fluffy
显示剩余2条评论

11

我对这里找到的大多数答案感到失望,其中一些存在缺陷,基本上是完全错误的。最终,我花了3个多小时来尝试解决这个问题。Mark Ransom的回答是正确的,但我想提供一个完整的C语言解决方案,也经过了MATLAB的验证。我已经彻底测试过这个解决方案,以下是C代码:

#include <math.h>
typedef unsigned char BYTE; //define an "integer" that only stores 0-255 value

typedef struct _CRGB //Define a struct to store the 3 color values
{
    BYTE r;
    BYTE g;
    BYTE b;
}CRGB;

BYTE clamp(float v) //define a function to bound and round the input float value to 0-255
{
    if (v < 0)
        return 0;
    if (v > 255)
        return 255;
    return (BYTE)v;
}

CRGB TransformH(const CRGB &in, const float fHue)
{
    CRGB out;
    const float cosA = cos(fHue*3.14159265f/180); //convert degrees to radians
    const float sinA = sin(fHue*3.14159265f/180); //convert degrees to radians
    //calculate the rotation matrix, only depends on Hue
    float matrix[3][3] = {{cosA + (1.0f - cosA) / 3.0f, 1.0f/3.0f * (1.0f - cosA) - sqrtf(1.0f/3.0f) * sinA, 1.0f/3.0f * (1.0f - cosA) + sqrtf(1.0f/3.0f) * sinA},
        {1.0f/3.0f * (1.0f - cosA) + sqrtf(1.0f/3.0f) * sinA, cosA + 1.0f/3.0f*(1.0f - cosA), 1.0f/3.0f * (1.0f - cosA) - sqrtf(1.0f/3.0f) * sinA},
        {1.0f/3.0f * (1.0f - cosA) - sqrtf(1.0f/3.0f) * sinA, 1.0f/3.0f * (1.0f - cosA) + sqrtf(1.0f/3.0f) * sinA, cosA + 1.0f/3.0f * (1.0f - cosA)}};
    //Use the rotation matrix to convert the RGB directly
    out.r = clamp(in.r*matrix[0][0] + in.g*matrix[0][1] + in.b*matrix[0][2]);
    out.g = clamp(in.r*matrix[1][0] + in.g*matrix[1][1] + in.b*matrix[1][2]);
    out.b = clamp(in.r*matrix[2][0] + in.g*matrix[2][1] + in.b*matrix[2][2]);
    return out;
}

注意:旋转矩阵只取决于色相fHue),所以一旦计算出matrix [3] [3],您可以将其 重复使用于正在进行相同色相转换的图像中的每个像素!这将极大地提高效率。 以下是验证结果的MATLAB代码:

function out = TransformH(r,g,b,H)
    cosA = cos(H * pi/180);
    sinA = sin(H * pi/180);

    matrix = [cosA + (1-cosA)/3, 1/3 * (1 - cosA) - sqrt(1/3) * sinA, 1/3 * (1 - cosA) + sqrt(1/3) * sinA;
          1/3 * (1 - cosA) + sqrt(1/3) * sinA, cosA + 1/3*(1 - cosA), 1/3 * (1 - cosA) - sqrt(1/3) * sinA;
          1/3 * (1 - cosA) - sqrt(1/3) * sinA, 1/3 * (1 - cosA) + sqrt(1/3) * sinA, cosA + 1/3 * (1 - cosA)];

    in = [r, g, b]';
    out = round(matrix*in);
end

以下是两个代码都能复现的样例输入/输出:

TransformH(86,52,30,210)
ans =
    36
    43
    88

使用色调值 210,将输入的 RGB 值 [86,52,30] 转换为 [36,43,88]


嘿 - 谢谢 - 我发现如果你在循环中这样做,颜色最终会变暗,所以你需要增加一点亮度。可能是一些舍入误差。例如我做了:float bright = 1.01; int r = (float)fg.r * bright; if (r > 255) { r = 255; } fg.r = r; int g = (float)fg.g * bright; if (g > 255) { g = 255; } fg.g = g; int b = (float)fg.b * bright; if (b > 255) { b = 255; } fg.b = b; - Goblinhack

5

JavaScript的实现(基于Vladimir上面的PHP)

const deg = Math.PI / 180;

function rotateRGBHue(r, g, b, hue) {
  const cosA = Math.cos(hue * deg);
  const sinA = Math.sin(hue * deg);
  const neo = [
    cosA + (1 - cosA) / 3,
    (1 - cosA) / 3 - Math.sqrt(1 / 3) * sinA,
    (1 - cosA) / 3 + Math.sqrt(1 / 3) * sinA,
  ];
  const result = [
    r * neo[0] + g * neo[1] + b * neo[2],
    r * neo[2] + g * neo[0] + b * neo[1],
    r * neo[1] + g * neo[2] + b * neo[0],
  ];
  return result.map(x => uint8(x));
}

function uint8(value) {
  return 0 > value ? 0 : (255 < value ? 255 : Math.round(value));
}

3

基本上有两个选项:

  1. 将 RGB 转换为 HSV,改变色相,再将 HSV 转换回 RGB
  2. 通过线性变换直接改变色相

我不太确定如何实现第二个选项,但基本上你需要创建一个转换矩阵,并通过该矩阵过滤图像。然而,这将重新着色图像而不仅仅是改变色相。如果这对您来说没有问题,则可以选择此选项,但如果不行,则必须进行转换。

编辑

一些研究显示这个,它证实了我的想法。总结一下:如果想要精确的结果,则应该首选从 RGB 到 HSV 的转换。通过线性变换修改原始的 RGB 图像也能得到结果,但这只是轻微地着色图像。区别在于:RGB 到 HSV 的转换是非线性的,而变换是线性的。


已经进行了编辑。下次我会直接处理,不需要提醒 ;) - Sebastian Dressler

3

这篇文章的发布时间比较早,原作者当时是在寻找iOS代码,但我通过搜索Visual Basic代码来到了这里。所以对于像我这样的读者,我将Mark的代码转换为VB .net模块:

Public Module HueAndTry    
    Public Function ClampIt(ByVal v As Double) As Integer    
        Return CInt(Math.Max(0F, Math.Min(v + 0.5, 255.0F)))    
    End Function    
    Public Function DegreesToRadians(ByVal degrees As Double) As Double    
        Return degrees * Math.PI / 180    
    End Function    
    Public Function RadiansToDegrees(ByVal radians As Double) As Double    
        Return radians * 180 / Math.PI    
    End Function    
    Public Sub HueConvert(ByRef rgb() As Integer, ByVal degrees As Double)
        Dim selfMatrix(,) As Double = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}
        Dim cosA As Double = Math.Cos(DegreesToRadians(degrees))
        Dim sinA As Double = Math.Sin(DegreesToRadians(degrees))
        Dim sqrtOneThirdTimesSin As Double = Math.Sqrt(1.0 / 3.0) * sinA
        Dim oneThirdTimesOneSubCos As Double = 1.0 / 3.0 * (1.0 - cosA)
        selfMatrix(0, 0) = cosA + (1.0 - cosA) / 3.0
        selfMatrix(0, 1) = oneThirdTimesOneSubCos - sqrtOneThirdTimesSin
        selfMatrix(0, 2) = oneThirdTimesOneSubCos + sqrtOneThirdTimesSin
        selfMatrix(1, 0) = selfMatrix(0, 2)
        selfMatrix(1, 1) = cosA + oneThirdTimesOneSubCos
        selfMatrix(1, 2) = selfMatrix(0, 1)
        selfMatrix(2, 0) = selfMatrix(0, 1)
        selfMatrix(2, 1) = selfMatrix(0, 2)
        selfMatrix(2, 2) = cosA + oneThirdTimesOneSubCos
        Dim rx As Double = rgb(0) * selfMatrix(0, 0) + rgb(1) * selfMatrix(0, 1) + rgb(2) * selfMatrix(0, 2)
        Dim gx As Double = rgb(0) * selfMatrix(1, 0) + rgb(1) * selfMatrix(1, 1) + rgb(2) * selfMatrix(1, 2)
        Dim bx As Double = rgb(0) * selfMatrix(2, 0) + rgb(1) * selfMatrix(2, 1) + rgb(2) * selfMatrix(2, 2)
        rgb(0) = ClampIt(rx)
        rgb(1) = ClampIt(gx)
        rgb(2) = ClampIt(bx)
    End Sub
End Module

我将常见术语放入(长)变量中,但除此之外,这是一个直接的转换 - 对我的需求起作用。

顺便说一下,我试图为Mark的优秀代码点赞,但我自己没有足够的票数来使它可见(提示,提示)。


如果我没记错的话,你需要25个声望点才能进行点赞(不是很多)。祝好运。 - lrnzcig

2

WebGL版本:

最初的回答
vec3 hueShift(vec3 col, float shift){
    vec3 m = vec3(cos(shift), -sin(shift) * .57735, 0);
    m = vec3(m.xy, -m.y) + (1. - m.x) * .33333;
    return mat3(m, m.zxy, m.yzx) * col;
}

1

1

如果有人需要上述所描述的(伽马校正)色调偏移作为参数化的HLSL像素着色器(我为WPF应用程序编写了它,想着我甚至可以分享它):

    sampler2D implicitInput : register(s0);
    float factor : register(c0);

    float4 main(float2 uv : TEXCOORD) : COLOR
    {
            float4 color = tex2D(implicitInput, uv);

            float h = 360 * factor;          //Hue
            float s = 1;                     //Saturation
            float v = 1;                     //Value
            float M_PI = 3.14159265359;

            float vsu = v * s*cos(h*M_PI / 180);
            float vsw = v * s*sin(h*M_PI / 180);

            float4 result;
            result.r = (.299*v + .701*vsu + .168*vsw)*color.r
                            + (.587*v - .587*vsu + .330*vsw)*color.g
                            + (.114*v - .114*vsu - .497*vsw)*color.b;
            result.g = (.299*v - .299*vsu - .328*vsw)*color.r
                            + (.587*v + .413*vsu + .035*vsw)*color.g
                            + (.114*v - .114*vsu + .292*vsw)*color.b;
            result.b = (.299*v - .300*vsu + 1.25*vsw)*color.r
                            + (.587*v - .588*vsu - 1.05*vsw)*color.g
                            + (.114*v + .886*vsu - .203*vsw)*color.b;;
            result.a = color.a;

            return result;
    }

1

Scott...不完全正确。该算法似乎与HSL/HSV中的算法相同,但更快。 此外,如果您只是将数组的前3个元素乘以灰色因子,您会增加/减少亮度。

例如...从Rec709的灰度具有以下值 [GrayRedFactor_Rec709:R$ 0.212671 GrayGreenFactor_Rec709:R$ 0.715160 GrayBlueFactor_Rec709:R$ 0.072169]

当您将self.matrix[x][x]与相应的灰度因子相乘时,您会减少亮度而不影响饱和度。 例如:

def set_hue_rotation(self, degrees):
    cosA = cos(radians(degrees))
    sinA = sin(radians(degrees))
    self.matrix[0][0] = (cosA + (1.0 - cosA) / 3.0) * 0.212671
    self.matrix[0][1] = (1./3. * (1.0 - cosA) - sqrt(1./3.) * sinA) * 0.715160
    self.matrix[0][2] = (1./3. * (1.0 - cosA) + sqrt(1./3.) * sinA) * 0.072169
    self.matrix[1][0] = self.matrix[0][2] <---Not sure, if this is the right code, but i think you got the idea
    self.matrix[1][1] = self.matrix[0][0]
    self.matrix[1][2] = self.matrix[0][1]

相反的情况也是如此。如果你除以而不是乘以,亮度会大幅增加。

从我的测试结果来看,这些算法可以很好地替代HSL,当然前提是你不需要饱和度。

试着这样做……将色相旋转1度(只是为了强制算法正常工作,同时保持图像的感知敏感性),然后乘以这些因子。


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