在HSV颜色空间中如何插值色相值?

18

我正在尝试在HSV颜色空间内插两个颜色,以产生平滑的颜色渐变。

我正在使用线性插值,例如:

h = (1 - p) * h1 + p * h2
s = (1 - p) * s1 + p * s2
v = (1 - p) * v1 + p * v2

(其中p是百分比,h1、h2、s1、s2、v1、v2是两种颜色的色调、饱和度和亮度组件)

这会对s和v产生良好的效果,但对h不起作用。由于色调组件是一个角度,计算需要找出h1和h2之间的最短距离,然后朝着正确的方向(顺时针或逆时针)进行插值。

我应该使用什么公式或算法?


编辑:通过遵循杰克的建议,我修改了我的JavaScript渐变函数,并且它运行良好。对于任何感兴趣的人,这就是我最终得到的结果:

// create gradient from yellow to red to black with 100 steps
var gradient = hsbGradient(100, [{h:0.14, s:0.5, b:1}, {h:0, s:1, b:1}, {h:0, s:1, b:0}]); 

function hsbGradient(steps, colours) {
  var parts = colours.length - 1;
  var gradient = new Array(steps);
  var gradientIndex = 0;
  var partSteps = Math.floor(steps / parts);
  var remainder = steps - (partSteps * parts);
  for (var col = 0; col < parts; col++) {
    // get colours
    var c1 = colours[col], 
        c2 = colours[col + 1];
    // determine clockwise and counter-clockwise distance between hues
    var distCCW = (c1.h >= c2.h) ? c1.h - c2.h : 1 + c1.h - c2.h;
        distCW = (c1.h >= c2.h) ? 1 + c2.h - c1.h : c2.h - c1.h;
     // ensure we get the right number of steps by adding remainder to final part
    if (col == parts - 1) partSteps += remainder; 
    // make gradient for this part
    for (var step = 0; step < partSteps; step ++) {
      var p = step / partSteps;
      // interpolate h, s, b
      var h = (distCW <= distCCW) ? c1.h + (distCW * p) : c1.h - (distCCW * p);
      if (h < 0) h = 1 + h;
      if (h > 1) h = h - 1;
      var s = (1 - p) * c1.s + p * c2.s;
      var b = (1 - p) * c1.b + p * c2.b;
      // add to gradient array
      gradient[gradientIndex] = {h:h, s:s, b:b};
      gradientIndex ++;
    }
  }
  return gradient;
}

1
这是作业吗?http://www.uio.no/studier/emner/matnat/ifi/INF3320/h08/undervisningsmateriale/oblig1.pdf - Jack
1
哈哈!不是的,尽管我确实也做了那个练习,它帮助我构思了这个问题 :) 我正在使用JavaScript和SVG绘制图表,并且需要一种方法来在任意给定的HSV颜色列表之间生成漂亮的渐变。除了色调插值外,其他都已经实现了。 - nick
2个回答

11

你只需要找出从起始色调到结束色调的最短路径。这很容易做到,因为色调值的范围是0到255。

您可以首先从较高的色调中减去较低的色调,然后将256加到较低的色调中,以再次检查交换操作数的差异。

int maxCCW = higherHue - lowerHue;
int maxCW = (lowerHue+256) - higherHue;

你需要获得两个值,最大值决定你是顺时针旋转还是逆时针旋转。然后你需要找到一种方法,使插值操作在色相的模256上运行,因此如果你正在将 246 插值到 20 ,如果系数 >= 0.5f,则应重置色相为0(因为它达到了256并且无论如何都会执行 hue = hue%256 )。

实际上,如果在计算新的色相后不考虑插值过程中的色相值大于0的情况,只需应用模运算符也可以使它正常工作。


谢谢Jack,你真的帮了我很大的忙。我设法让我的代码工作了。我将编辑我的问题以包含我最终得到的代码。请随意提出改进建议。 - nick
需要注意的一点是,在我使用的颜色模型中,色相值都在0到1之间,因此我没有像你建议的那样使用模运算。 - nick

10
尽管这个答案来晚了,但被接受的那个是错误的,因为它声明色调(hue)应该在[0, 255]之间;更好的解释和代码可以让我们做得更好。
色调(Hue)是一个角度值,在区间[0,360)中;一个完整的圆是360,而0也等同于360。HSV颜色空间更容易可视化,对于人类来说比RGB更直观。HSV形成一个圆柱体,在许多颜色选择器中显示其切片,而RGB实际上是一个立方体,不是一个很好的颜色选择器选择;大多数使用RGB的颜色选择器需要使用比HSV选择器所需更多的滑动条。
插值色调的要求是选择较小的弧从一个色调到另一个色调。因此,给定两个色调值,有四种可能性,下面是示例角度:
Δ |  ≤ 180  |  > 180
--|---------|---------
+ |  40, 60 | 310, 10
− |  60, 40 | 10, 310

if Δ = 180 then both +/− rotation are valid options

让我们把+解释为逆时针旋转,把解释为顺时针旋转。如果差的绝对值超过180,则通过± 360进行规范化,以确保大小在180以内;这也会正确地反转方向。

var d = h2 - h1;
var delta = d + ((Math.abs(d) > 180) ? ((d < 0) ? 360 : -360) : 0);

现在只需将 delta 除以所需的步数,即可得到每个循环迭代的权重,用于在插值期间添加到起始角度。
var new_angle = start + (i * delta);

从下面的完整代码中摘取相关函数:

function interpolate(h1, h2, steps) {
  var d = h2 - h1;
  var delta = (d + ((Math.abs(d) > 180) ? ((d < 0) ? 360 : -360) : 0)) / (steps + 1.0);
  var turns = [];
  for (var i = 1; d && i <= steps; ++i)
    turns.push(((h1 + (delta * i)) + 360) % 360);
  return turns;
}

"use strict";

function interpolate(h1, h2, steps) {
  var d = h2 - h1;
  var delta = (d + ((Math.abs(d) > 180) ? ((d < 0) ? 360 : -360) : 0)) / (steps + 1.0);
  var turns = [];
  for (var i = 1; d && i <= steps; ++i)
    turns.push(((h1 + (delta * i)) + 360) % 360);
  return turns;
}

function get_results(h1, h2, steps) {
  h1 = norm_angle(h1);
  h2 = norm_angle(h2);
  var r = "Start: " + h1 + "<br />";
  var turns = interpolate(h1, h2, steps);
  r += turns.length ? "Turn: " : "";
  r += turns.join("<br />Turn: ");
  r += (turns.length ? "<br />" : "") + "Stop: " + h2;
  return r;
}

function run() {
  var h1 = get_angle(document.getElementById('h1').value);
  var h2 = get_angle(document.getElementById('h2').value);
  var steps = get_num(document.getElementById('steps').value);
  var result = get_results(h1, h2, steps);

  document.getElementById('res').innerHTML = result;
}

function get_num(s) {
  var n = parseFloat(s);
  return (isNaN(n) || !isFinite(n)) ? 0 : n;
}

function get_angle(s) {
  return get_num(s) % 360;
}

function norm_angle(a) {
  a %= 360;
  a += (a < 0) ? 360 : 0;
  return a;
}
<h1 id="title">Hue Interpolation</h1>
Angle 1
<input type="text" id="h1" />
<br />Angle 2
<input type="text" id="h2" />
<br />
<br />Intermediate steps
<input type="text" id="steps" value="5" />
<br />
<br/>
<input type="submit" value="Run" onclick="run()" />
<p id="res"></p>


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