转换RGB颜色的色调

时间:2011-12-14 16:21:23

标签: objective-c c colors rgb

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

下图显示了R,G和B值相对于色调的变化情况。

Graph of RGB values across hues

看起来编写一个函数来转换色调而不做任何讨厌的转换到不同的颜色格式似乎应该是一个相对简单的,这会引入更多的错误(如果继续应用小的转换,这可能是一个问题一种颜色),我怀疑它的计算成本会更高。

到目前为止,这是我的工作。如果你从纯黄色或青色或洋红色转移,它会很有效,但在某些地方会变得有点眩晕。

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);
}

我知道你可以用UIColors做到这一点并用这样的东西改变色调:

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然后再回来之间似乎相当过分。

我可能最终会使用查找表或预先计算我的应用程序中的颜色,但我真的很好奇是否有办法使我的代码工作。谢谢!

15 个答案:

答案 0 :(得分:38)

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)

以上是上述结果:

Hue rotation example

您可以在http://www.graficaobscura.com/matrix/index.html

找到相同构思的不同实现

答案 1 :(得分:15)

编辑,每条评论更改为“全部”为“可以线性近似”。
编辑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;
}

答案 2 :(得分:6)

我对我在这里找到的大多数答案感到失望,有些是有缺陷的,而且基本上是错误的。我最终花了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

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

答案 3 :(得分:3)

基本上有两种选择:

  1. 转换RGB - &gt; HSV,改变色调,转换HSV - &gt; RGB
  2. 使用线性变换直接更改色调
  3. 我不确定如何实现2,但基本上你必须创建一个变换矩阵并通过这个矩阵过滤图像。但是,这将重新着色图像而不是仅改变色调。如果这对您没问题,那么这可能是一个选项,但如果不是,则无法避免转换。

    修改

    一些小研究显示this,这证实了我的想法。总结:如果需要精确的结果,则应首选从RGB到HSV的转换。通过线性变换修改原始RGB图像也会产生结果,但这会使图像着色。差异解释如下:从RGB到HSV的转换是非线性的,而转换是线性的。

答案 4 :(得分:2)

帖子很旧,原来的海报正在寻找ios代码 - 但是,我是通过搜索视觉基本代码发送到这里的,所以对于像我这样的所有人,我将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为他出色的代码留下一个赞成票,但我自己没有足够的票来让它可见(Hint,Hint)。

答案 5 :(得分:2)

JavaScript实现(基于上述弗拉基米尔的PHP)

public class MyConsumer : IConsumer<Batch<MyMessage>>
{
    public async Task Consume(ConsumeContext<Batch<MyMessage>> context)
    {
        ...
    }
}

答案 6 :(得分:1)

似乎转换为HSV最有意义。 Sass提供了一些令人惊叹的颜色助手。它是红宝石,但它可能提供有用的。

http://sass-lang.com/docs/yardoc/Sass/Script/Functions.html

答案 7 :(得分:1)

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;
}

答案 8 :(得分:0)

优秀的代码,但是,我想知道如果你不使用self.matrix [2] [0],self.matrix [2] [1],self.matrix [2] [1]它会更快]

因此,set_hue_rotation可以简单地写成:

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] = 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]

答案 9 :(得分:0)

斯科特......不完全是。算法似乎与HSL / HSV相同,但速度更快。 此外,如果您只是将数组的前3个元素与灰色系数相乘,则可以添加/减少亮度。

示例... Rec709中的灰度值具有这些值 [GrayRedFactor_Rec709:R $ 0.212671  GrayGreenFactor_Rec709:R $ 0.715160  GrayBlueFactor_Rec709:R $ 0.072169]

当你多了 self.matrix [x] [x]与GreyFactor通讯员你减少亮度而不接触饱和度 例如:

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度(只是为了强制算法正常工作,同时保持图像的相同感知灵敏度),并乘以这些因素。

答案 10 :(得分:0)

此外,马克的算法产生更准确的结果。

例如,如果使用HSV色彩空间将色调旋转到180º,图像可能会产生偏红的色调。

但是在Mark的算法中,图像正确旋转。例如,皮肤色调(Pue中的Hue = 17,Sat = 170,L = 160)正确地变为蓝色,在PSP中具有144左右的Hue,并且图像的所有其他颜色都正确旋转。

算法是有道理的,因为Hue只不过是这个公式所定义的红色,绿色,蓝色的arctan的对数函数:

Hue = arctan((logR-logG)/(logR-logG+2*LogB))

答案 11 :(得分:0)

PHP实施:

class Hue
{
    public function convert(int $r, int $g, int $b, int $hue)
    {
        $cosA = cos($hue * pi() / 180);
        $sinA = sin($hue * pi() / 180);

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

        $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 array_map([$this, 'crop'], $result);
    }

    private function crop(float $value)
    {
        return 0 > $value ? 0 : (255 < $value ? 255 : (int)round($value));
    }
}

答案 12 :(得分:0)

对于需要上述(伽玛校正后的)色相偏移作为参数化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;
    }

答案 13 :(得分:0)

在glsl上最紧凑的版本,我能够做到:

vec3 hs(vec3 c, float s){
    vec3 m=vec3(cos(s),s=sin(s)*.5774,-s);
    return c*mat3(m+=(1.-m.x)/3.,m.zxy,m.yzx);
}

答案 14 :(得分:0)

稍微改变 MasterHD 的 answer 以再次添加值和饱和度,我们最终得到以下 C/C++ 代码:

#include <math.h>
typedef unsigned char uint8_t; //if no posix defs, remove if not needed

//if you use C not C++ this needs to be typedef ..
struct Color{
    uint8_t r;
    uint8_t g;
    uint8_t b;
};


uint8_t 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 (uint8_t)v;
}

//compare http://beesbuzz.biz/code/16-hsv-color-transforms
Color change_hsv_c(
    const Color &in, 
    const float fHue,
    const float fSat,
    const float fVal
)
{
    Color out;
    const float cosA = fSat*cos(fHue*3.14159265f/180); //convert degrees to radians
    const float sinA = fSat*sin(fHue*3.14159265f/180); //convert degrees to radians

    //helpers for faster calc //first 2 could actually be precomputed
    const float aThird = 1.0f/3.0f;
    const float rootThird = sqrtf(aThird);
    const float oneMinusCosA = (1.0f - cosA);
    const float aThirdOfOneMinusCosA = aThird * oneMinusCosA;
    const float rootThirdTimesSinA =  rootThird * sinA;
    const float plus = aThirdOfOneMinusCosA +rootThirdTimesSinA;
    const float minus = aThirdOfOneMinusCosA -rootThirdTimesSinA;

    //calculate the rotation matrix
    float matrix[3][3] = {
        {   cosA + oneMinusCosA / 3.0f  , minus                         , plus                          },
        {   plus                        , cosA + aThirdOfOneMinusCosA   , minus                         },
        {   minus                       , plus                          , cosA + aThirdOfOneMinusCosA   }
    };
    //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])*fVal);
    out.g = clamp((in.r*matrix[1][0] + in.g*matrix[1][1] + in.b*matrix[1][2])*fVal);
    out.b = clamp((in.r*matrix[2][0] + in.g*matrix[2][1] + in.b*matrix[2][2])*fVal);
    return out;
}