这很简单.除了角度,你实际上还需要一个参数,即梯度应该是多么紧/宽.让我们改为只使用两点:
__D
__--
__--
__--
__--
M
Run Code Online (Sandbox Code Playgroud)
其中M是梯度的中点(红色和绿色之间),D表示方向和距离.因此,渐变变为:
M'
| __D
| __--
| __--
| __--
| __--
M
__-- |
__-- |
__-- |
__-- |
D'-- |
M"
Run Code Online (Sandbox Code Playgroud)
这意味着,沿着矢量D'D,您将从红色变为绿色,线性地如您所知.沿着矢量M'M",您可以保持颜色不变.
这就是理论.现在实现取决于您实际绘制像素的方式.我们假设你没有想要逐个像素地决定颜色(所以你可以按任何像素顺序绘制.)
那很简单!我们来说明一点:
M'
| SA __D
__--| __--
P-- |__ A __--
| -- /| \ __--
| -- | |_--
| --M
|__-- |
__--CA |
__-- |
__-- |
D'-- |
M"
Run Code Online (Sandbox Code Playgroud)
点P,A与由M和定义的坐标系具有角度D.我们知道沿着矢量M'M",颜色不会改变,所以sin(A)没有任何意义.相反,cos(A)显示相对多远向着D或D'像素的颜色应该去.该点CA表示|PM|cos(A)这意味着P通过M和定义的线的映射D,或者详细说明线的长度PM乘以cos(A).
所以算法如下
CAD,绝对是绿色.如果之前D',肯定是红色的.|D'CA|/|D'D|根据您的评论,如果您想确定画布大小的宽度,您可以D根据输入角度和画布大小轻松计算,但我个人建议使用单独的参数.
我想指出的是,发生在混色当人们试图平均常识性错误r,g和b组件:
R = (R1 + R2) / 2;
G = (G1 + G2) / 2;
B = (B1 + B2) / 2;
Run Code Online (Sandbox Code Playgroud)
您可以观看有关该主题的优秀4分钟物理视频:
简短的版本是试图通过平均组件非常混合两种颜色是错误的:
R = R1*(1-mix) + R2*mix;
G = G1*(1-mix) + G2*mix;
B = B1*(1-mix) + B2*mix;
Run Code Online (Sandbox Code Playgroud)
问题是计算机上的RGB颜色是在sRGB颜色空间中.并且这些数值具有大约 2.4 的伽马值.为了正确混合颜色,您必须先撤消此伽玛调整:
在不应用反伽马的情况下,混合颜色比它们应该更暗.这可以在并排的颜色梯度实验中看出.
而不是天真:
//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;
}
Run Code Online (Sandbox Code Playgroud)
正确的形式是:
//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;
}
Run Code Online (Sandbox Code Playgroud)
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;
}
Run Code Online (Sandbox Code Playgroud)
然后你重新应用压缩扩展:
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;
}
Run Code Online (Sandbox Code Playgroud)
我测试了@MarkRansom注释,当颜色等于RGB总值时,线性RGB空间中的颜色混合是好的; 但线性混合比例似乎不是线性的 - 特别是对于黑白情况.
所以我尝试在Lab颜色空间中混合,正如我的直觉所建议的那样(以及这个摄影堆栈交换答案):
您的问题实际上包括两部分:
在可感知的颜色空间中,渐变的强度必须恒定,否则在渐变中的点处将看起来不自然地变暗或变亮。您可以在基于sRGB值的简单插值的渐变中轻松看到这一点,尤其是中间的红绿色渐变太暗。在线性值而不是经过伽玛校正的值上使用插值可以使红绿色渐变更好,但会以背白渐变为代价。通过将光强度与颜色分开,您可以同时兼顾两个方面。
通常,当需要感知色空间时,会提出Lab色空间。我认为有时候它太过分了,因为它试图适应这样一种感觉,即蓝色比其他强度的颜色(例如黄色)要深。的确如此,但是我们习惯于在自然环境中看到这种效果,并且在渐变中您最终会得到过度补偿。
阿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
Run Code Online (Sandbox Code Playgroud)
这是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
Run Code Online (Sandbox Code Playgroud)
现在是您问题的第二部分。您需要一个方程式来定义代表渐变中点的线,以及与该线的距离(对应于渐变的端点颜色)。将端点放置在矩形的最远端很自然,但是根据您的示例判断,这不是您所做的事情。我选择了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)
Run Code Online (Sandbox Code Playgroud)
这是上面的结果:
| 归档时间: |
|
| 查看次数: |
7800 次 |
| 最近记录: |