Как интерполировать значения оттенков в цветовом пространстве HSV?

Я пытаюсь интерполировать между двумя цветами в цветовом пространстве 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;
}

2 ответа

Решение

Вам просто нужно выяснить, какой кратчайший путь от начального оттенка до конечного оттенка. Это можно сделать легко, так как значения оттенков варьируются от 0 до 255.

Вы можете сначала вычесть более низкий оттенок из более высокого, а затем добавить 256 к более низкому, чтобы еще раз проверить разницу с переставленными операндами.

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

Таким образом, вы получите два значения, большее из них решает, следует ли вам идти по часовой стрелке или против часовой стрелки. Затем вам нужно будет найти способ заставить интерполяцию работать по модулю 256 оттенка, так что если вы интерполируете из 246 в 20 если коэффициент >= 0.5f Вы должны сбросить оттенок до 0 (так как он достигает 256 и hue = hue%256 в любом случае).

На самом деле, если вы не заботитесь о оттенке во время интерполяции по 0, а просто применяете оператор по модулю после вычисления нового оттенка, он все равно должен работать.

Хотя этот ответ запаздывает, принятый неверен, утверждая, что оттенок должен быть в пределах [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>

Другие вопросы по тегам