平滑的颜色过渡算法

17

我正在寻找一种通用算法,能够使两种颜色之间实现平滑过渡。

例如,这张图片是从维基百科中获取的,显示了从橙色到蓝色的过渡。

输入图像描述

当我尝试使用我的代码(C++)进行相同的操作时,首先想到的是使用HSV颜色空间,但是烦人的中间颜色会出现。

输入图像描述

如何才能达到好的效果?似乎与对比度的减小或使用不同的颜色空间有关?


3
请注意,从橙色到蓝色的特定变化是一个非常具体的例子,而不是一般算法的应用。特别地,这是普朗克辐射定律曲线 - 黑体发射器在从暗淡红色发光到极热状态时的可见颜色。 - MSalters
你尝试过在YUV色彩空间中使用线性插值,而不是HSV吗? - Eli Algranti
5个回答

20

过去我已经做了很多这样的工作。平滑处理可以用许多不同的方法进行,但他们可能在这里采用的是一种简单的线性方法。换句话说,对于每个R、G和B分量,他们只需找出连接两个点的“y=m*x+b”方程,并使用它来确定之间的分量。

m[RED]   = (ColorRight[RED]   - ColorLeft[RED])   / PixelsWidthAttemptingToFillIn
m[GREEN] = (ColorRight[GREEN] - ColorLeft[GREEN]) / PixelsWidthAttemptingToFillIn
m[BLUE]  = (ColorRight[BLUE]  - ColorLeft[BLUE])  / PixelsWidthAttemptingToFillIn

b[RED]   = ColorLeft[RED]
b[GREEN] = ColorLeft[GREEN]
b[BLUE]  = ColorLeft[BLUE]

现在,介于任何两种颜色之间的新颜色为:

NewCol[pixelXFromLeft][RED]   = m[RED]   * pixelXFromLeft + ColorLeft[RED]    
NewCol[pixelXFromLeft][GREEN] = m[GREEN] * pixelXFromLeft + ColorLeft[GREEN]
NewCol[pixelXFromLeft][BLUE]  = m[BLUE]  * pixelXFromLeft + ColorLeft[BLUE]

有许多数学方法可以创建转换效果,我们真正想要做的是了解您想要看到的转换。如果您想要看到上面图像的确切转换,则值得查看该图像的颜色值。我曾经编写过一个程序来查看这样的图像,并以图形方式输出其值。这是我针对上述伪彩色比例尺的程序输出。

enter image description here

根据观察图表,它比我上面所述的线性更为复杂。蓝色部分看起来大多是线性的,红色部分可以模拟成线性的,而绿色部分则看起来具有更加圆润的形状。我们可以对绿色进行数学分析,以更好地了解其数学函数,并使用该函数代替。您可能会发现,在0和约70个像素之间具有增加斜率的线性插值,在70像素后具有线性减少斜率是足够好的。

如果您查看屏幕底部,则此程序提供每个颜色分量的某些统计量(例如最小值、最大值和平均值)以及读取图像的像素宽度。


谢谢,看起来我把事情复杂化了,而其实只需要简单明了的方法就可以了! - Smash
2
@trumpetlicks:只是一个非常简短的评论。有可能上面的图像实际上是采用了线性方法,但是使用的是CMYK空间而不是RGB空间吗? - philkark
@phil13131 - 很有趣你问到这个问题,因为我正在制作这个分析程序的一个版本,可以在Mac上运行,并且能够在绘图之前将其预转换为任何颜色空间,以便可以在任何颜色空间中进行分析。目标是能够在图表上设置近似函数,并生成许多不同语言的颜色比例尺代码。如果你有Mac,我很乐意分享 :-) - trumpetlicks
1
我怀疑线性插值和图表中显示的曲线之间的差异是由于伽马校正。如果您的坐标空间是线性的,则线性插值是有意义的。这里实际的算法可能是将您的sRGB(或伽马校正的RGB)转换为线性RGB,使用它们计算线性插值,并将插值后的值转换回非线性显示色彩空间。 - Adrian McCarthy

10

简单的线性插值R、G、B值就可以实现。

trumpetlicks表明,您使用的图像不是纯线性插值。但我认为,插值可以给您所需的效果。下面我展示了一个带有线性插值的图像和您原始图像在底部的图像。

顶部是线性插值,底部是原始图像

以下是生成此图的(Python)代码:

for y in range(height/2):
    for x in range(width):
        p = x / float(width - 1)
        r = int((1.0-p) * r1 + p * r2 + 0.5)
        g = int((1.0-p) * g1 + p * g2 + 0.5)
        b = int((1.0-p) * b1 + p * b2 + 0.5)
        pix[x,y] = (r,g,b)

有趣的 - 取决于原帖作者真正想要什么。原始比例(图像)似乎提供了更高的对比度。 - trumpetlicks
不是很相关,但是在计算给定x的颜色时进行y循环会更有意义,循环次数相同但需要计算的内容更少(我知道,这只是针对小图像的微小优化)。 - engineercoding
@engineercoding 对于像这样的小例子,实际上并不重要。即使在重要的情况下,这也不是一个简单的决定——图像通常按照x在低位顺序排列,因此使用我的示例代码所显示的方式访问更加高效。现代处理器可以在缓存未命中的时间内完成大量计算。 - Mark Ransom
你能解释一下这段代码吗?r1r2等是什么意思? - NullByte08
@NullByte08 r1、g1、b1 是左侧的 RGB 值,r2、g2、b2 是右侧的 RGB 值。如果您查找“线性插值”的含义,其余部分应该就会明白了。 - Mark Ransom

6
HSV颜色空间不是一个非常适合平滑过渡的颜色空间。这是因为"h"值,也就是色调,仅用于任意定义“色轮”周围的不同颜色。这意味着如果你在轮子上跨越两种远离的颜色,你将不得不穿过一堆其他颜色。根本不平滑。
使用RGB(或CMYK)会更有意义。这些“组件”颜色空间更好地定义了平滑过渡,因为它们表示每种颜色需要多少“组件”。
每个组件值R、G和B的线性过渡(参见@trumpetlicks答案)应该看起来“相当不错”。任何超过“相当不错”的东西都需要实际的人来调整值,因为我们的眼睛对不同颜色组中颜色值的感知存在差异和不对称,这些差异和不对称在RBG或CMYK(或任何标准)中都没有体现。
维基百科图片使用的是Photoshop使用的算法。不幸的是,该算法不公开。

1
色调绝非随意。互补色的色调相差180度。问题在于橙色到蓝色的过渡是在互补色之间,相差180度,而保持SV恒定(仅变化H)的路径会产生半个彩虹。 - MSalters
@Msalters 我明白,但是虽然“互补”颜色有明确定义,但它们在色轮周围的顺序确实是任意的。有许多不同的方式可以定义这个轮子。 - Damien Black
1
那也不是任意的。从RGB空间到HSV空间的转换是连续的;相邻的颜色仍然保持相邻。如果色调是任意排序的,那就不是这种情况了。 - MSalters
@MSalters RGB颜色空间是三维的,这意味着每种颜色有6个“相邻”的颜色。颜色轮只选择其中两种相邻的颜色。有许多不同的方式可以设置颜色轮,以保持RGB到HSV的连续性约束。“任意”可能是一个过于强烈的词,但并不只有一种颜色排序是有意义的。有许多排序方式都可以工作。传统的颜色轮只是其中之一。 - Damien Black
@trumpetlicks:这在数学上是相当明显的。所有3D颜色空间都试图描述可见颜色的整个色域,并在所有颜色属性上保持连续性。这意味着连续路径在变换下仍然保持连续。由于所有颜色空间都试图公平地划分不同颜色的空间,因此这些路径不能被大幅压缩或拉伸。(否则,你会发现在两种类似的蓝色之间的路径长度可能会因颜色空间而异)。 - MSalters
显示剩余2条评论

6

我一直在研究如何构建一个算法,它以灰度图像为输入,并根据调色板进行人工着色

■■■■ 灰度输入 ■■■■ 输出 ■■■■■■■■■■■■■■■

enter image description here enter image description here

和其他许多解决方案一样,该算法使用线性插值来实现颜色之间的过渡。对于您的示例,应使用以下参数调用smooth_color_transition():
QImage input("gradient.jpg");

QVector<QColor> colors;
colors.push_back(QColor(242, 177, 103)); // orange
colors.push_back(QColor(124, 162, 248)); // blue-ish

QImage output = smooth_color_transition(input, colors);    
output.save("output.jpg");

下面是算法处理前后原始图像 VS 输出图像的比较:

enter image description here (输出图像)

enter image description here (原始图像)

可以观察到输出图像中的视觉伪影已经存在于输入(灰度)中。当将输入图像调整为189x51时,输入图像就出现了这些伪影。

这里有另一个使用更复杂的色彩调色板创建的示例:

■■■■ 灰度输入 ■■■■ 输出 ■■■■■■■■■■■■■■■


3

在我看来,使用RGB值创建渐变会更容易。您应该根据渐变的宽度首先计算每个值的颜色变化。以下伪代码需要针对R、G和B值执行。

redDifference = (redValue2 - redValue1) / widthOfGradient

然后,您可以使用这些值呈现每个像素,如下所示:

for (int i = 0; i < widthOfGradient; i++) {
    int r = round(redValue1 + i * redDifference)
    // ...repeat for green and blue

    drawLine(i, r, g, b)
}

我知道你指定了使用C++,但是我创建了一个JSFiddle,以你的第一个渐变为例,展示了它的工作方式:http://jsfiddle.net/eumf7/


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接