颜色渐变算法 [英] Color gradient algorithm

查看:234
本文介绍了颜色渐变算法的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

给出两种rgb颜色和一个矩形,我可以创建一个基本的线性渐变。这篇将被提出。我认为有时候它太过分了,因为它试图适应这样一种感觉:蓝色比其他颜色(例如黄色)的强度要深。的确如此,但是我们习惯于在自然环境和渐变中看到这种效果,最终会导致过度补偿。



A










Given two rgb colors and a rectangle, I'm able to create a basic linear gradient. This blog post gives very good explanation on how to create it. But I want to add one more variable to this algorithm, angle. I want to create linear gradient where I can specified the angle of the color.

For example, I have a rectangle (400x100). From color is red (255, 0, 0) and to color is green (0, 255, 0) and angle is 0°, so I will have the following color gradient.

Given I have the same rectangle, from color and to color. But this time I change angle to 45°. So I should have the following color gradient.

解决方案

Your question actually consists of two parts:

  1. How to generate a smooth color gradient between two colors.
  2. How to render a gradient on an angle.

The intensity of the gradient must be constant in a perceptual color space or it will look unnaturally dark or light at points in the gradient. You can see this easily in a gradient based on simple interpolation of the sRGB values, particularly the red-green gradient is too dark in the middle. Using interpolation on linear values rather than gamma-corrected values makes the red-green gradient better, but at the expense of the back-white gradient. By separating the light intensities from the color you can get the best of both worlds.

Often when a perceptual color space is required, the Lab color space will be proposed. I think sometimes it goes too far, because it tries to accommodate the perception that blue is darker than an equivalent intensity of other colors such as yellow. This is true, but we are used to seeing this effect in our natural environment and in a gradient you end up with an overcompensation.

A power-law function of 0.43 was experimentally determined by researchers to be the best fit for relating gray light intensity to perceived brightness.

I have taken here the wonderful samples prepared by Ian Boyd and added my own proposed method at the end. I hope you'll agree that this new method is superior in all cases.

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

Here's the code in 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

Now for part 2 of your question. You need an equation to define the line that represents the midpoint of the gradient, and a distance from the line that corresponds to the endpoint colors of the gradient. It would be natural to put the endpoints at the farthest corners of the rectangle, but judging by your example in the question that is not what you did. I picked a distance of 71 pixels to approximate the example.

The code to generate the gradient needs to change slightly from what's shown above, to be a little more flexible. Instead of breaking the gradient into a fixed number of steps, it is calculated on a continuum based on the parameter t which ranges between 0.0 and 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)

And here's the result of the above:

这篇关于颜色渐变算法的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆