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

发布于 2024-08-28 04:34:10 字数 1797 浏览 4 评论 0原文

我试图在 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之间的最短距离,然后在正确的方向(顺时针或逆时针)进行插值。

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


编辑:按照 Jack 的建议,我修改了 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;
}

I'm trying to interpolate between two colours in HSV colour space to produce a smooth colour gradient.

I'm using a linear interpolation, eg:

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

(where p is the percentage, and h1, h2, s1, s2, v1, v2 are the hue, saturation and value components of the two colours)

This produces a good result for s and v but not for h. As the hue component is an angle, the calculation needs to work out the shortest distance between h1 and h2 and then do the interpolation in the right direction (either clockwise or anti-clockwise).

What formula or algorithm should I use?


EDIT: By following Jack's suggestions I modified my JavaScript gradient function and it works well. For anyone interested, here's what I ended up with:

// 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;
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

岁月静好 2024-09-04 04:34:10

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

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

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

因此,您将获得两个值,较大的值决定您应该顺时针还是逆时针旋转。然后,您必须找到一种方法,使插值对色调的模 256 进行运算,因此,如果您从 246 插值到 20,且系数为 >= 0.5f 您应该将色调重置为 0(因为在任何情况下它都会达到 256 并且 hue = Hue%256)。

实际上,如果您在对 0 进行插值时不关心色调,而只是在计算新色调后应用模运算符,那么它应该可以工作。

You should just need to find out which is the shortest path from starting hue to ending hue. This can be done easily since hue values range from 0 to 255.

You can first subtract the lower hue from the higher one, then add 256 to the lower one to check again the difference with swapped operands.

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

So you'll obtain two values, the greater one decides if you should go clockwise or counterclockwise. Then you'll have to find a way to make the interpolation operate on modulo 256 of the hue, so if you are interpolating from 246 to 20 if the coefficient is >= 0.5f you should reset hue to 0 (since it reaches 256 and hue = hue%256 in any case).

Actually if you don't care about hue while interpolating over the 0 but just apply modulo operator after calculating the new hue it should work anyway.

猫性小仙女 2024-09-04 04:34:10

尽管这个答案迟到了,但公认的答案是错误的,即色调应该在 [0, 255] 范围内;通过更清晰的解释和代码也可以实现更多正义。

色调是区间 [0, 360) 内的角度值;一个完整的圆,其中 0 = 360。HSV 颜色空间比 RGB 更容易可视化,并且对人类来说更直观。 HSV 形成一个圆柱体,在许多颜色选择器中显示一个切片,而 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>

Although this answer is late, the accepted one is incorrect in stating that hue should be within [0, 255]; also more justice can be done with clearer explanation and code.

Hue is an angular value in the interval [0, 360); a full circle where 0 = 360. The HSV colour space is easier to visualize and is more intuitive to humans then RGB. HSV forms a cylinder from which a slice is shown in many colour pickers, while RGB is really a cube and isn't really a good choice for a colour picker; most ones which do use it would have to employ more sliders than required for a HSV picker.

The requirement when interpolating hue is that the smaller arc is chosen to reach from one hue to another. So given two hue values, there are four possibilities, given with example angles below:

Δ |  ≤ 180  |  > 180
--|---------|---------
+ |  40, 60 | 310, 10
− |  60, 40 | 10, 310

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

Lets take + as counter-clockwise and as clockwise rotation. If the difference in absolute value exceeds 180 then normalize it by ± 360 to make sure the magnitude is within 180; this also reverses the direction, rightly.

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

Now just divide delta by the required number of steps to get the weight of each loop iteration to add to the start angle during interpolating.

var new_angle = start + (i * delta);

Relevant function excerpted from the complete code that follows:

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>

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文