非线性颜色插值?

10

如果我有一条从0到1的直线,那么在这条直线上的0处是颜色A(255,0,0),在0.3处是颜色B(20,160,0),在1处是颜色C(0,0,0)。我该如何找到0.7处的颜色?

谢谢。


你的线上的颜色没有线性分布。 - Niki Yoshiuchi
我不确定它叫什么,但我想知道如何进行插值。 - jmasterx
你怎么知道这是线性的? - luke
@Niki,它们可能在另一种颜色表示(HSV、HSL)中是线性的。请看下面我的回答。 - Patrick
这不就是确定值所在的段,并对该段进行线性插值吗?由于0.7在0.3-1的段中,我会对(20,160,0)-(0,0,0)的0.3-1进行线性插值,结果为(9,69,0)。您没有指定其他非线性插值的类型,因此很难确定一个好的结果。 - Lasse V. Karlsen
显示剩余3条评论
5个回答

37
[对Patrick的评论进行详细阐述-一切都有些失控了!]

这是一个非常有趣的问题,而且之所以有趣是因为没有“正确”的答案。颜色表示和数据插值可以用许多不同的方式来完成。您需要适当地调整方法以适应您的问题域。由于我们没有得到关于该域的先验信息,因此我们只能探索一些可能性。

颜色业务使事情变得有些复杂,因此让我们暂时将其放在一边,首先考虑简单标量的插值。

插值探险

假设我们有一些数据点,如下所示:

Three points we might want to fit a function to

我们想要找到的是在除了我们已知数值的点上,这张图表上的y值是多少。也就是说,我们正在寻找一个函数y = f(x),使其穿过这些点。
显然,我们可以选择许多不同的方式来连接这些点。我们可以直接用直线段将它们连接起来,或者我们可能需要一个平滑的曲线,而这个曲线可以是简单的或任意复杂的。

Some possible interpolations

这里,我们很容易理解红线是从一个已知点到下一个点绘制的直线。绿线看起来也比较合理,尽管我们添加了一些关于曲线应如何行动的假设。另一方面,蓝线仅根据数据点很难证明其合理性,但在某些情况下,我们可能有理由使用这样的形状来建模系统 - 正如稍后将要看到的。
还要注意虚线沿着每条曲线向两侧延伸。这些是超出已知点的范围,称为外推而不是内插。这通常比内插更具争议。至少当你从一个已知点到另一个已知点时,你有一些证据表明你正在朝着正确的方向前进。然而,你离开已知点越远,就越有可能偏离路径。但是,这仍然是一种有用的技术,它可能是有意义的。
好吧,图片很漂亮,但我们如何生成这些线?

我们需要找到一个f(x),以便得到所需的y。根据情况,我们可以使用许多不同的函数,但迄今为止最常用的是使用多项式函数,即:

f(x) = a0 + a1 * x + a2 * x * x + a3 * x * x * x + ....

现在,假设有N个不同的数据点,总是可以使用N-1次多项式找到一个完美的拟合曲线 -- 两个点的情况下是一条直线,三个点的情况下是一个抛物线,四个点的情况下是一个立方体等等 -- 但是,除非数据表现得异常良好,否则随着次数的增加曲线会变得混乱。因此,除非有充分的理由相信数据的行为能够被高次多项式很好地建模,通常会采用分段插值的方法,对每对连续的点之间进行拟合不同的曲线段。
通常,每个片段被建模为一次或三次多项式。前者只是一条直线,因为它真的很容易,只需要两个数据点本身的信息。后者是一个立方样条,并且被使用是因为它是最简单的多项式,可以给您提供平滑的过渡,穿过每个点。然而,计算它稍微复杂一些,需要每个片段的两个额外信息(这些信息取决于您使用的特定样条形式)。
线性插值足以用一行代码实现。如果我们的第一个数据点是(x1, y1),第二个数据点是(x2, y2),那么在任何中间x处进行线性插值得到的y值为:
y = y1 + (x - x1) * (y2 - y1) / (x2 - x1)

(其他答案中也有类似的变化。)

立方样条曲线在此不便详细讨论,但是谷歌应该可以找到一些不错的参考资料。无论如何,在这种情况下,它们都可能过于复杂,因为您只有三个点。

回到问题

有了这些基础知识,让我们来看看所描述的问题:

A grey bar with three known colour points

我们有一条线(在这里以灰色显示),其颜色仅在三个点处已知,并且我们被要求计算其他某个点的颜色。由于我们没有任何关于颜色分布的知识,因此我们必须做出一些假设。
一个关键的假设是,沿着该线的颜色变化将是连续的,至少在某种程度上是连续的。显然,如果该线真的看起来像这样:

Random stripes

然后,所有关于插值的早期内容都将被抛弃。我们没有基础来决定任何部分应该是什么颜色,只能放弃并回家。假设这不是情况,并且我们有一些需要插值的东西。
已知的颜色以RGB格式指定。在这种表示中,每个通道都是一个独立的标量值,我们可以选择将其视为与其他通道完全独立。因此,一个非常合理的方法是对每个通道进行分段线性插值,然后重新组合结果。
这样做会得到类似于以下的结果:

Piecewise linear interpolation in RGB

这是可以接受的,但我们可能不喜欢结果的某些方面。其中一个问题是从红色到绿色的过渡经过相当浑浊的灰棕色。另一个问题是0.3处的绿峰有点尖锐。
需要注意的是,在没有更全面的规范的情况下,这些只是审美上的担忧。我们的技术完全可靠,但它并没有给出我们想要的结果。这种事情取决于我们的具体问题领域,最终都是选择的问题。
由于我们只有三个数据点 - 而且由于Hans Passant建议这样做 - 或许我们可以尝试逐渐模拟每个通道上的整个曲线来拟合一个二次曲线?虽然我们没有理由认为这是一个好模型,但试一试也无妨:

Quadratic interpolation in RGB

这个渐变与上一个的区别很明显。二次函数已经将事物平滑了,但也出现了严重的超调。请记住,绿色通道的起始值和结束值都为0。抛物线是对称的,因此它的最大值必须在中间。它适应绿色上升到0.3的唯一方法是继续上升到0.5。(红色通道也有类似的效果,但不那么明显,因为在这种情况下是欠拟合的,并且该值被夹在0处。)
我们有证据表明我们的颜色线真的具有这种形状吗?没有:我们通过选择模型明确引入了它。但这并不否定它-我们可能有很好的理由希望它以这种方式工作-再次强调,这是一个选择问题。
但HSV呢?
到目前为止,我们坚持使用原始的RGB颜色空间,但正如许多人匆忙指出的,这对于插值来说可能不太理想:在RGB中,颜色和亮度是绑在一起的,因此在两种不同的全强度颜色之间进行插值通常会穿过一些单调乏味的低强度中间色。
HSV表示中,颜色和强度分别位于不同的维度上,因此我们不会遇到这个问题。为什么不转换并在该空间中进行插值呢?
这立即引起了一个困难 - 或者说是另一个决定。HSV和RGB之间的映射不是双射; 特别地,黑色恰好是我们三个数据点之一,在RGB空间中是一个单点,但在HSV空间中占据整个平面。我们无法在点和平面之间进行插值,因此我们需要选择一个特定的HSV黑点。
这是Patrick巧妙解决方案的基础,其中H和S被特别选择使整个颜色渐变线性。结果看起来像这样:

Patrick's linear inter/extrapolation in HSV

这看起来比之前的尝试更漂亮、更丰富多彩,但有一些问题需要注意。一个重要问题是,在V的情况下,我们仍然在所有三个点拥有确定的数据,但这些数据实际上并不是线性的,因此线性拟合只是一个近似值。这意味着我们在0.3处看到的值并不完全正确。
另一个我认为更大的问题是:那么多的蓝色从哪里来?我们已知的RGB数据点中,B=0。突然引入这么多蓝色似乎有些奇怪,我们似乎没有任何证据。请查看Patrick的HSV插值中RGB分量的图表。

Graph of the RGB components in Patrick's version

蓝色的原因是,当我们切换颜色空间时,我们特意选择了一个模型,在这个模型中,如果你一直从绿色走下去,最终会到达蓝色。同时,我们不得不舍弃其中一个色调数据点,并选择通过线性外推从其他两个数据点来填充它,这意味着我们可以一直继续前进,从绿色到蓝色,再到远方的山丘。

再次强调,这并不是无效的,我们可能有很好的理由以这种方式进行。但在我看来,这比之前的分段线性示例更加超前,因为涉及到外推。

那么在HSV中,这是唯一的方法吗?当然不是。总有更多的选择。

例如,我们可以选择将H和S值设为1.0以最大化线性度,或者我们可以选择将它们设为最小化分段线性插值的变化。碰巧的是,对于S,这两种策略是一致的:两个点都是100,所以我们在最后也将其设为100。这两个线段是共线的。对于H,我们只需在第二个线段上保持不变即可。这样就得到了以下结果:

Piecewise linear interpolation in HSV

这个不像之前那个那么可爱,但在我看来似乎更有可能。再一次地,这主要是审美判断。这并不意味着这就是“正确”的答案,就像它也不会使Patrick或其他任何人的答案变得“错误”一样。正如我在一开始所说的那样,没有“正确”的答案。这都是根据你在特定问题中的需求做出选择的问题——并且知道你已经做出了这些选择以及它们如何影响你的结果。


3
哇,非常非常有趣。我能不能给你这个点赞加五分? - Patrick
@Patrick 谢谢。就像我说的,这是拖延的材料 :) 我想我现在真的应该认真工作了... - walkytalky

8
尝试将其转换为另一种颜色表示,例如HSV(参见http://en.wikipedia.org/wiki/HSL_and_HSV)。
  • 颜色A的色相为0,饱和度为1,亮度为1。
  • 颜色C的色相为?,饱和度为?,亮度为0。

?表示实际上并不重要(因为颜色C只是黑色)。

现在也将颜色B转换为HSV(抱歉,我无法脱口而出),然后选择漂亮的值作为颜色C的色相和饱和度,使得颜色C的色相、亮度和饱和度在HSV空间中处于一条直线上。然后从中减去0.7的颜色。

编辑:使用http://www.csgnetwork.com/csgcolorsel4.html上的RGB-HSV计算器,我计算出以下内容:

  • 颜色A:H:0,V:100,S:100
  • 颜色B:H:113,V:100,S:63

现在我们像这样计算颜色C的H和V:

  • H:113 / 0.3 = 376.6
  • V:100(因为A和B的V都是100)

这给了我们0.7时的颜色:

  • H = 376.66 * 0.7 = 263.66
  • V = 100
  • S = 大约在30左右

不幸的是,这并不完全符合饱和度,但如果您以这种方式进行插值,将会得到非常接近所需颜色的结果。


不过对于V来说,这并不是真的。除非B恰好有V=0.7,否则你仍然需要进行非线性拟合。从第一条线的前30%线性外推H可能很容易使你穿过一堆本质上虚假的颜色变化,最终到达黑色。 - walkytalky
正如您的结果所显示的那样。H 在 0.7 已经超出了范围。而 V = 100->100->100->0 怎么可能算作任何形式的插值呢? - walkytalky
V=100->100->100->0 是一个打字错误。我已经纠正了它。 至于其余部分,插值非常接近(但并不完美,我承认)。 - Patrick
1
@Patrick:在打字错误方面,你的V和S仍然混淆了。有了正确的值,你确实可以使S=100,我甚至可以接受V~30,因为在这种情况下误差并不太大。但是色调外推仍然很荒谬。你提出基于两个数据点,整条线必须在色轮上转一圈半?我在这里做了一个初步的matlab模型链接。这不是拟合数据,而是完全捏造数据。 - walkytalky
@WalkyTalky,没问题。只要我们能互相帮助,每一个意见都是有价值的。 - Patrick
显示剩余5条评论

4
在这个问题中,你需要求出f(0.7)的值,已知f(0.0)=(255, 0, 0),f(0.3)=(20, 160, 0),f(1.0)=(0, 0, 0)。但是,由于f可以是无限多个函数中的任意一个,所以这并没有明确定义。
一些可能的f选择包括:
1.在RGB空间中的一系列线段 2.在HSV或HSL空间中映射后的一系列线段 3.RGB空间中的三次样条曲线。 4.HSV/L空间中的三次样条曲线。
但是,如何定义f应该取决于你正在做什么,或者你的应用程序如何定义颜色停止。

三个点无法完全定义一个三次方程,但你可以使用二次样条插值。 - Joe

3
正确插值RGB的方法是先进行伽马校正,使颜色分量线性化,然后执行插值,最后转换回去。否则由于值是非线性的,将会出现错误。有关更多信息,请参见http://www.4p8.com/eric.brasseur/gamma.html(虽然该文章是关于缩放的,但通常通过插值像素值来完成缩放)。
如果您有sRGB值,则可以使用此处的公式在线性和非线性之间进行转换:http://en.wikipedia.org/wiki/SRGB(使用Csrgb和Clinear公式)
另一方面,这些误差并不是很大,因此取决于您的应用程序是否要紧。
对于插值本身,简单的线性插值就足够了(如其他人在此处所指出的)。请参见http://en.wikipedia.org/wiki/Linear_interpolation以获取具体信息。

0

看起来你正在尝试在错误的颜色空间中进行插值。首先将其转换为HSV,然后你的颜色就会变成:

RGB -> HSV
255,0,0 -> 0, 255,255
20,160,0 -> 80,255,160
0,0,0 -> 0,255,0

所以现在还是有点困惑,但至少我们可以看到 V 值正在插值为零,H 值仍然有些困惑,但在意识到 HSV 被建模为一个圆柱体后,我们可以看到它实际上只是从 0 插值到 256 模 256,所以它最终会回到零。

因此,你的方程式将是(在 HSV 中)

nH = frac*256 mod 256
nS = 255
nV =  (1-frac)*255

如果你将0.7替换为frac,你将在HSV坐标中得到正确的计算结果,如果你需要返回RGB,你应该查看你的库是否提供这样的功能(我知道Java的Color类支持此功能),但如果没有,你可以从这里获取代码。它展示了如何在C中进行颜色空间转换。

祝好运


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