颜色渐变算法

时间:2014-03-24 10:42:09

标签: algorithm

给定两个rgb颜色和一个矩形,我能够创建一个基本的线性渐变。这个blog post给出了如何创建它的非常好的解释。但是我想在这个算法中添加一个变量angle。我想创建线性渐变,我可以指定颜色的角度。

例如,我有一个矩形(400x100)。从颜色为红色(255,0,0),颜色为绿色(0,255,0),角度为0°,因此我将具有以下颜色渐变。

enter image description here

鉴于我有相同的矩形,从颜色到颜色。但这次我将角度改为45°。所以我应该有以下颜色渐变。

enter image description here

4 个答案:

答案 0 :(得分:8)

这非常简单。除了角度,你实际上还需要一个参数,即梯度应该是多么紧/宽。让我们改为只使用两点:

                                         __D
                                     __--
                                 __--
                             __--
                         __--
                        M

其中M是渐变的中点(红色和绿色之间),D表示方向和距离。因此,渐变变为:

                  M'
                   |                     __D
                    |                __--
                     |           __--
                      |      __--
                       | __--
                        M
                   __--  |
               __--       |
           __--            |
       __--                 |
   D'--                      |
                             M"

这意味着,沿着向量D'D,您可以从红色变为绿色,如您所知,线性变化。沿着矢量M'M",您可以保持颜色不变。


这就是理论。现在实现取决于您实际绘制像素的方式。我们不假设你说你想逐个像素地决定颜色(所以你可以按任何像素顺序绘制。)

那很简单!让我们来说明一点:

                  M'
                   | SA                  __D
                __--|                __--
               P--   |__ A       __--
               |  -- /| \    __--
                |   -- | |_--
                 |    --M
                  |__--  |
               __--CA     |
           __--            |
       __--                 |
   D'--                      |
                             M"

点P,角度为A,坐标系由MD定义。我们知道,在向量M'M"中,颜色没有变化,因此sin(A)没有任何意义。相反,cos(A)相对于像素颜色应该向DD'显示相对多远。点CA显示|PM|cos(A),表示PMD定义的行上的映射,或详细说明行PM的长度1}}乘以cos(A)

所以算法如下

  • 对于每个像素
    • 计算CA
    • 如果比D更远,那肯定是绿色的。如果在D'之前,肯定是红色。
    • 否则根据|D'CA|/|D'D|
    • 的比例找到从红色到绿色的颜色

根据您的评论,如果您想要确定画布大小的宽度,您可以根据输入角度和画布大小轻松计算D,但我个人建议使用单独的参数。

答案 1 :(得分:6)

我想指出当人们尝试平均rgb分量时,颜色混合中常见的错误:

R = (R1 + R2) / 2;
G = (G1 + G2) / 2;
B = (B1 + B2) / 2;

您可以观看有关该主题的优秀4分钟物理视频:

  

Computer Color is Broken

简短版本是试图通过平均组件来混合两种颜色是错误的:

R = R1*(1-mix) + R2*mix;
G = G1*(1-mix) + G2*mix;
B = B1*(1-mix) + B2*mix;

问题是计算机上的RGB颜色位于 sRGB 颜色空间中。并且这些数值具有 2.4的伽玛。为了正确混合颜色,您必须先撤消此伽玛调整:

  • 撤消伽玛调整
  • 应用上面的r,g,b混合算法
  • 重新应用gamma

不应用反伽马,混合颜色比它们应该更暗。这可以在并排的颜色梯度实验中看出。

  • 顶部(错误):不考虑sRGB gamma
  • 底部(正确):记录sRGB gamma

enter image description here

算法

而不是天真:

//This is the wrong algorithm. Don't do this
Color ColorMixWrong(Color c1, Color c2, Single mix)
{
   //Mix [0..1]
   //  0   --> all c1
   //  0.5 --> equal mix of c1 and c2
   //  1   --> all c2
   Color result;

   result.r = c1.r*(1-mix) + c2.r*(mix);
   result.g = c1.g*(1-mix) + c2.g*(mix);
   result.b = c1.b*(1-mix) + c2.b*(mix);

   return result;
}

正确的形式是:

//This is the wrong algorithm. Don't do this
Color ColorMix(Color c1, Color c2, Single mix)
{
   //Mix [0..1]
   //  0   --> all c1
   //  0.5 --> equal mix of c1 and c2
   //  1   --> all c2

   //Invert sRGB gamma compression
   c1 = InverseSrgbCompanding(c1);
   c2 = InverseSrgbCompanding(c2);

   result.r = c1.r*(1-mix) + c2.r*(mix);
   result.g = c1.g*(1-mix) + c2.g*(mix);
   result.b = c1.b*(1-mix) + c2.b*(mix);

   //Reapply sRGB gamma compression
   result = SrgbCompanding(result);

   return result;
}

sRGB的伽玛调整不仅仅是2.4。它们实际上有一个接近黑色的线性部分 - 所以它是一个分段函数。

Color InverseSrgbCompanding(Color c)
{
    //Convert color from 0..255 to 0..1
    Single r = c.r / 255;
    Single g = c.g / 255;
    Single b = c.b / 255;

    //Inverse Red, Green, and Blue
    if (r > 0.04045) r = Power((r+0.055)/1.055, 2.4) else r = r / 12.92;
    if (g > 0.04045) g = Power((g+0.055)/1.055, 2.4) else g = g / 12.92;
    if (b > 0.04045) b = Power((b+0.055)/1.055, 2.4) else b = b / 12.92;

    //return new color. Convert 0..1 back into 0..255
    Color result;
    result.r = r*255;
    result.g = g*255;
    result.b = b*255;

    return result;
}

然后你重新应用压缩扩展:

Color SrgbCompanding(Color c)
{
    //Convert color from 0..255 to 0..1
    Single r = c.r / 255;
    Single g = c.g / 255;
    Single b = c.b / 255;

    //Apply companding to Red, Green, and Blue
    if (r > 0.0031308) r = 1.055*Power(r, 1/2.4)-0.055 else r = r * 12.92;
    if (g > 0.0031308) g = 1.055*Power(g, 1/2.4)-0.055 else g = g * 12.92;
    if (b > 0.0031308) b = 1.055*Power(b, 1/2.4)-0.055 else b = b * 12.92;

    //return new color. Convert 0..1 back into 0..255
    Color result;
    result.r = r*255;
    result.g = g*255;
    result.b = b*255;

    return result;
}

更新:马克是对的

我测试了@MarkRansom注释,当颜色等于RGB总值时,线性RGB空间中的颜色混合是好的;但线性混合比例似乎不是线性的 - 特别是对于黑白情况。

所以我尝试混合使用Lab颜色空间,正如我的直觉所建议的那样(as well as this photography stackexchange answer):

enter image description here
enter image description here
enter image description here
enter image description here
enter image description here
enter image description here

答案 2 :(得分:6)

您的问题实际上由两部分组成:

  1. 如何在两种颜色之间生成平滑的颜色渐变。
  2. 如何在角度上渲染渐变。
  3. 渐变的强度在感知颜色空间中必须是恒定的,否则在渐变中的点处看起来会显得不自然地暗或亮。基于sRGB值的简单插值,您可以在渐变中轻松地看到这一点,尤其是中间的红绿渐变太暗。在线性值而不是伽马校正值上使用插值可以使红绿渐变更好,但是以背白渐变为代价。通过将光强度与颜色分开,您可以获得两全其美的效果。

    通常在需要感知色彩空间时,会提出Lab color space。我认为有时它会走得太远,因为它试图适应蓝色比其他颜色(如黄色)的等效强度更暗的感觉。这是事实,但是我们习惯于在自然环境中看到这种效果,并且在渐变中你会得到过度补偿。

    研究人员通过实验确定power-law function of 0.43最适合将灰度光强度与感知亮度联系起来。

    我已经把Ian Boyd准备的精彩样本带到这里,并在最后添加了我自己提出的方法。我希望你会同意这种新方法在所有情况下都是优越的。

    Algorithm MarkMix
       Input:
          color1: Color, (rgb)   The first color to mix
          color2: Color, (rgb)   The second color to mix
          mix:    Number, (0..1) The mix ratio. 0 ==> pure Color1, 1 ==> pure Color2
       Output:
          color:  Color, (rgb)   The mixed color
    
       //Convert each color component from 0..255 to 0..1
       r1, g1, b1 ← Normalize(color1)
       r2, g2, b2 ← Normalize(color1)
    
       //Apply inverse sRGB companding to convert each channel into linear light
       r1, g1, b1 ← sRGBInverseCompanding(r1, g1, b1)       
       r2, g2, b2 ← sRGBInverseCompanding(r2, g2, b2)
    
       //Linearly interpolate r, g, b values using mix (0..1)
       r ← LinearInterpolation(r1, r2, mix)
       g ← LinearInterpolation(g1, g2, mix)
       b ← LinearInterpolation(b1, b2, mix)
    
       //Compute a measure of brightness of the two colors using empirically determined gamma
       gamma ← 0.43
       brightness1 ← Pow(r1+g1+b1, gamma)
       brightness2 ← Pow(r2+g2+b2, gamma)
    
       //Interpolate a new brightness value, and convert back to linear light
       brightness ← LinearInterpolation(brightness1, brightness2, mix)
       intensity ← Pow(brightness, 1/gamma)
    
       //Apply adjustment factor to each rgb value based
       if ((r+g+b) != 0) then
          factor ← (intensity / (r+g+b))
          r ← r * factor
          g ← g * factor
          b ← b * factor
       end if
    
       //Apply sRGB companding to convert from linear to perceptual light
       r, g, b ← sRGBCompanding(r, g, b)
    
       //Convert color components from 0..1 to 0..255
       Result ← MakeColor(r, g, b)
    End Algorithm MarkMix
    

    这是Python中的代码:

    def all_channels(func):
        def wrapper(channel, *args, **kwargs):
            try:
                return func(channel, *args, **kwargs)
            except TypeError:
                return tuple(func(c, *args, **kwargs) for c in channel)
        return wrapper
    
    @all_channels
    def to_sRGB_f(x):
        ''' Returns a sRGB value in the range [0,1]
            for linear input in [0,1].
        '''
        return 12.92*x if x <= 0.0031308 else (1.055 * (x ** (1/2.4))) - 0.055
    
    @all_channels
    def to_sRGB(x):
        ''' Returns a sRGB value in the range [0,255]
            for linear input in [0,1]
        '''
        return int(255.9999 * to_sRGB_f(x))
    
    @all_channels
    def from_sRGB(x):
        ''' Returns a linear value in the range [0,1]
            for sRGB input in [0,255].
        '''
        x /= 255.0
        if x <= 0.04045:
            y = x / 12.92
        else:
            y = ((x + 0.055) / 1.055) ** 2.4
        return y
    
    def all_channels2(func):
        def wrapper(channel1, channel2, *args, **kwargs):
            try:
                return func(channel1, channel2, *args, **kwargs)
            except TypeError:
                return tuple(func(c1, c2, *args, **kwargs) for c1,c2 in zip(channel1, channel2))
        return wrapper
    
    @all_channels2
    def lerp(color1, color2, frac):
        return color1 * (1 - frac) + color2 * frac
    
    
    
    def perceptual_steps(color1, color2, steps):
        gamma = .43
        color1_lin = from_sRGB(color1)
        bright1 = sum(color1_lin)**gamma
        color2_lin = from_sRGB(color2)
        bright2 = sum(color2_lin)**gamma
        for step in range(steps):
            intensity = lerp(bright1, bright2, step, steps) ** (1/gamma)
            color = lerp(color1_lin, color2_lin, step, steps)
            if sum(color) != 0:
                color = [c * intensity / sum(color) for c in color]
            color = to_sRGB(color)
            yield color
    

    red-green gradient

    green-blue gradient

    blue-red gradient

    black-white gradient

    red-white gradient

    red-black gradient

    现在问题的第2部分。您需要一个等式来定义表示渐变中点的线,以及与线对应于渐变的端点颜色的线的距离。将端点放在矩形的最远角是很自然的,但是在问题的例子中判断不是你做的。我选择了71像素的距离来近似这个例子。

    生成渐变的代码需要稍微改变上面显示的内容,以便更灵活一些。不是将梯度分成固定数量的步骤,而是根据参数t在连续统计上计算,范围介于0.0和1.0之间。

    class Line:
        ''' Defines a line of the form ax + by + c = 0 '''
        def __init__(self, a, b, c=None):
            if c is None:
                x1,y1 = a
                x2,y2 = b
                a = y2 - y1
                b = x1 - x2
                c = x2*y1 - y2*x1
            self.a = a
            self.b = b
            self.c = c
            self.distance_multiplier = 1.0 / sqrt(a*a + b*b)
    
        def distance(self, x, y):
            ''' Using the equation from
                https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line#Line_defined_by_an_equation
                modified so that the distance can be positive or negative depending
                on which side of the line it's on.
            '''
            return (self.a * x + self.b * y + self.c) * self.distance_multiplier
    
    class PerceptualGradient:
        GAMMA = .43
        def __init__(self, color1, color2):
            self.color1_lin = from_sRGB(color1)
            self.bright1 = sum(self.color1_lin)**self.GAMMA
            self.color2_lin = from_sRGB(color2)
            self.bright2 = sum(self.color2_lin)**self.GAMMA
    
        def color(self, t):
            ''' Return the gradient color for a parameter in the range [0.0, 1.0].
            '''
            intensity = lerp(self.bright1, self.bright2, t) ** (1/self.GAMMA)
            col = lerp(self.color1_lin, self.color2_lin, t)
            total = sum(col)
            if total != 0:
                col = [c * intensity / total for c in col]
            col = to_sRGB(col)
            return col
    
    def fill_gradient(im, gradient_color, line_distance=None, max_distance=None):
        w, h = im.size
        if line_distance is None:
            def line_distance(x, y):
                return x - ((w-1) / 2.0) # vertical line through the middle
        ul = line_distance(0, 0)
        ur = line_distance(w-1, 0)
        ll = line_distance(0, h-1)
        lr = line_distance(w-1, h-1)
        if max_distance is None:
            low = min([ul, ur, ll, lr])
            high = max([ul, ur, ll, lr])
            max_distance = min(abs(low), abs(high))
        pix = im.load()
        for y in range(h):
            for x in range(w):
                dist = line_distance(x, y)
                ratio = 0.5 + 0.5 * dist / max_distance
                ratio = max(0.0, min(1.0, ratio))
                if ul > ur: ratio = 1.0 - ratio
                pix[x, y] = gradient_color(ratio)
    
    >>> w, h = 406, 101
    >>> im = Image.new('RGB', [w, h])
    >>> line = Line([w/2 - h/2, 0], [w/2 + h/2, h-1])
    >>> grad = PerceptualGradient([252, 13, 27], [41, 253, 46])
    >>> fill_gradient(im, grad.color, line.distance, 71)
    

    以上是上述结果:

    Red-Green gradient on a 45 degree angle

答案 3 :(得分:0)

@ user2799037的评论完全正确: 与前一行相比,每一行都向右移动了一些像素。

实际常数可以计算为您指定的角度的正切值。