【问题标题】:Color gradient algorithm颜色渐变算法
【发布时间】:2014-05-01 16:05:27
【问题描述】:

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

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

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

【问题讨论】:

  • 看起来每一行都只是向右移动了一个像素。根据角度,必须在您的示例中为 curr_vector 的计算添加一些常量。
  • 输入参数中缺少关键部分。如果您曾经使用过 photoshop/gimp/...,您就会知道渐变在两点之间。只给出一个角度会使它模棱两可......它应该伸展多远?
  • @KarolyHorvath 在 Photoshop 中,您可以在两种以上颜色之间创建渐变,每种颜色都有自己的位置。在上面的示例中,渐变应在矩形 (400x100) 内拉伸
  • @Sophy:我说的是在画布上绘制渐变(实际使用它),而不是定义一个。
  • @KarolyHorvath 这取决于画布的大小。如果画布大小为 200x300,则渐变应绘制在 200 宽度上。

标签: algorithm colors


【解决方案1】:

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

  1. 如何在两种颜色之间生成平滑的颜色渐变。
  2. 如何在某个角度上渲染渐变。

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

通常当需要感知色彩空间时,会建议使用Lab color space。我认为有时它走得太远了,因为它试图适应蓝色比其他颜色(如黄色)的同等强度更暗的感觉。确实如此,但我们习惯于在自然环境中看到这种效果,而在渐变中,您最终会得到过度补偿。

研究人员通过实验确定 A 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

现在是您问题的第 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)

这是上面的结果:

【讨论】:

  • 我假设 sum(color1_lin) 取 R+G+B 的总和(每个都在 0..1 的范围内),并返回一个标量。我假设在混合两种颜色的最简单情况下,lerp((bright1, bright2), step, steps)bright1bright2 之间进行插值。但是zip(color1_lin, color2_lin)到底在做什么呢?什么是zip。我想比较结果,我会从c = ColorMix(c1, c2, ratio)
  • @IanBoyd 抱歉,我在该代码中使用了很多 Pythonism。是的,sum 只是给你所有渠道的总和。我的意思是包含lerp 的代码,但我很着急,抱歉 - 我稍后会添加它。这只是一个线性插值。 zip 是 Python 内置的,它可以将两个数组的组件配对,因此您得到 (R1, R2), (G1, G2), (B1, B2)。
  • 这个算法很简洁,但我认为它可以进一步改进。与其将“亮度”计算为Pow(r+g+b, gamma),不如根据每个通道的相对亮度来加权,以获得更高的感知精度。即Pow((r*0.2126+g*0.7152+b*0.0722)*3, gamma)
  • @Retr0id 我不认为感知准确性是最好的方法,否则实验室颜色会给出最好的结果。
【解决方案2】:

我想指出当人们尝试平均 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 的 gamma。为了正确地混合颜色,您必须首先撤消此伽玛调整:

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

如果不应用反伽玛,混合颜色会比预期的颜色更暗。这可以在并排颜色渐变实验中看到。

  • 顶部(错误):未考虑 sRGB 伽玛
  • 底部():考虑到 sRGB 伽马

算法

而不是天真:

//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 的 Gamma 调整不仅仅是 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):






【讨论】:

  • 这个答案太简单了。当两个端点的 R+G+B 值相同时,效果很好,如示例中所示。当端点不同时它不是很好,当一个端点是黑色而另一个是白色时它是最糟糕的 - 你需要考虑感知间隔和线性间隔之间的差异。我自己现在也在为这种区别而苦苦挣扎,还没有一个好的答案。
  • 很高兴我回来检查,我看到你改进了你的答案。实验室空间确实是一种更好的方式,但并不完美。您可能遇到了最坏的情况,即绿蓝色渐变。它包含很多红色,即使两个端点都没有红色。这就是为什么我仍在努力创造完美的渐变。哦,顺便说一句,你还没有回答实际的问题 - 即如何在一个角度上创建渐变。
  • @MarkRansom 是的,我整个周末都在研究 xyY、XYZ、加性光、光谱功率密度的兔子洞。虽然 sRGB 的反伽马 在数学上 是正确的,但它在美学上并不令人愉悦。 L*a*b* 梯度有时很讨人喜欢,但显然不讨人喜欢。它开始涉及到“混合”两种颜色意味着什么的哲学问题。我们是否添加了不同强度的光?或者我们是否将原始红光通过不同强度的绿色滤光片。你是对的,没有好的答案。
  • 如果有人感兴趣,我可以使用这段代码并创建一个类,它可以根据目标值从一种颜色混合到另一种颜色 - gist.github.com/Steztric/b6582d046dab754850417ca4ee2cba38
  • 我终于有时间发布自己的答案了,看看吧。
【解决方案3】:

这很简单。除了角度之外,您实际上还需要一个参数,即渐变应该有多紧/多宽。让我们只处理两点:

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

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

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

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


这就是理论。现在实现取决于您实际绘制像素的方式。假设什么都不做,假设您想逐个像素地决定颜色(因此您可以按任何像素顺序绘制。)

这很简单!让我们提出一个观点:

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

点 P,与 MD 定义的坐标系具有角度 A。我们知道沿着向量M'M",颜色没有变化,所以sin(A)没有任何意义。相反,cos(A) 相对显示像素颜色应该到DD' 多远。点CA 显示|PM|cos(A),表示PMD 定义的线上的映射,或者详细地说是PM 线的长度乘以cos(A)

所以算法变成如下

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

根据你的cmets,如果你想从画布大小确定宽度,你可以根据你的输入角度和画布大小轻松计算D,尽管我个人建议使用单独的参数。

【讨论】:

  • @AssadEbrahim,我不断收到that,我的回答一文不值吗? ;)
  • 怀疑,正如您从您发布的链接中看到的那样——+36 对于回复来说并不算太破旧...... :)
【解决方案4】:

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

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

【讨论】:

    猜你喜欢
    • 2010-09-24
    • 2014-08-21
    • 1970-01-01
    • 2012-01-03
    • 1970-01-01
    • 1970-01-01
    • 2011-04-26
    • 2011-03-06
    • 1970-01-01
    相关资源
    最近更新 更多