Преобразование RGB в HSL

Я создаю инструмент Color Picker и для слайдера HSL мне нужно иметь возможность конвертировать RGB в HSL. Когда я искал SO для способа сделать преобразование, я нашел этот вопрос преобразование цвета HSL в RGB.

Хотя он предоставляет функцию для преобразования из RGB в HSL, я не вижу объяснения тому, что действительно происходит в расчете. Чтобы лучше это понять, я прочитал HSL и HSV в Википедии.

Позже я переписал функцию из "Преобразование цветов HSL в RGB", используя расчеты со страницы "HSL и HSV".

Я застрял в расчете оттенка, если R является максимальным значением. См. расчет на странице "HSL и HSV":

enter image description here

Это из другой вики-страницы на голландском языке:

enter image description here

и это из ответов на "Преобразование цветов HSL в RGB":

case r: h = (g - b) / d + (g < b ? 6 : 0); break; // d = max-min = c

Я протестировал все три с несколькими значениями RGB, и они, похоже, дают схожие (если не точные) результаты. Что мне интересно, они выполняют одно и то же? Получу ли я другие результаты для некоторых конкретных значений RGB? Какой из них мне следует использовать?

hue = (g - b) / c;                   // dutch wiki
hue = ((g - b) / c) % 6;             // eng wiki
hue = (g - b) / c + (g < b ? 6 : 0); // SO answer

function rgb2hsl(r, g, b) {
    // see https://en.wikipedia.org/wiki/HSL_and_HSV#Formal_derivation
    // convert r,g,b [0,255] range to [0,1]
    r = r / 255,
    g = g / 255,
    b = b / 255;
    // get the min and max of r,g,b
    var max = Math.max(r, g, b);
    var min = Math.min(r, g, b);
    // lightness is the average of the largest and smallest color components
    var lum = (max + min) / 2;
    var hue;
    var sat;
    if (max == min) { // no saturation
        hue = 0;
        sat = 0;
    } else {
        var c = max - min; // chroma
        // saturation is simply the chroma scaled to fill
        // the interval [0, 1] for every combination of hue and lightness
        sat = c / (1 - Math.abs(2 * lum - 1));
        switch(max) {
            case r:
                // hue = (g - b) / c;
                // hue = ((g - b) / c) % 6;
                // hue = (g - b) / c + (g < b ? 6 : 0);
                break;
            case g:
                hue = (b - r) / c + 2;
                break;
            case b:
                hue = (r - g) / c + 4;
                break;
        }
    }
    hue = Math.round(hue * 60); // °
    sat = Math.round(sat * 100); // %
    lum = Math.round(lum * 100); // %
    return [hue, sat, lum];
}

Ответ 1

Я читал несколько страниц вики и проверял разные вычисления и создавал визуализацию проекции куба RGB на шестиугольник. И я хотел бы изложить свое понимание этого обращения. Поскольку я считаю, что это преобразование (представления цветовых моделей с использованием геометрических фигур) интересно, я постараюсь быть настолько основательным, насколько я могу быть. Сначала начните с RGB.

RGB

Ну, это не нуждается в большом объяснении. В простейшей форме у вас есть 3 значения, R, G и B в диапазоне [0,255]. Например, 51,153,204. Мы можем представить его с помощью гистограммы:

RGB Bar Graph

RGB Cube

Мы также можем представлять цвет в трехмерном пространстве. Мы имеем три значения R, G, B, что соответствует X, Y и Z. Все три значения находятся в диапазоне [0,255], что приводит к кубу. Но прежде чем создать куб RGB, сначала разрешите работать в 2D-пространстве. Две комбинации R, G, B дают нам: RG, RB, GB. Если бы мы нарисовали их на плоскости, мы получили бы следующее:

2D-графики RGB

Это первые три стороны куба RGB. Если мы разместим их на трехмерном пространстве, это даст полукуб:

RGB Cube Sides

Если вы проверите приведенный выше график, смешав два цвета, мы получим новый цвет в (255,255), а это желтый, пурпурный и голубой. Опять же, две комбинации из них дают нам: YM, YC и MC. Это недостающие стороны куба. Как только мы добавим их, мы получим полный куб:

RGB Cube

И положение 51,153,204 в этом кубе:

RGB Cube Color Position

Проекция куба RGB на шестиугольник

Теперь, когда у нас есть RGB Cube, проецируем его на шестиугольник. Сначала мы наклоняем куб на 45 ° на X, а затем 35.264 ° на Y. После второго наклона черный угол находится внизу, а белый угол - вверху, и оба они проходят через ось Z.

RGB Cube Tilt

Как вы можете видеть, мы получим шестиугольник, который мы хотим, с правильным тоном, когда мы смотрим на куб сверху. Но нам нужно проецировать это на реальный шестиугольник. То, что мы делаем, это рисовать шестиугольник, который имеет тот же размер с видом сверху куба. Все углы шестиугольника соответствуют углам куба и цветам, а верхний угол белого куба проецируется в центр шестиугольника. Черный пропущен. И если мы сопоставим каждый цвет с шестиугольником, мы получим правильный взгляд.

Cube to Hexagon Projection

И положение 51,153,204 в шестиугольнике будет:

Цветовая позиция оттенка

Вычисление оттенка

Прежде чем сделать расчет, определите, какой оттенок есть.

Оттенок - это примерно угол вектора к точке проекции, красный при 0 °.

<суб >

... оттенок - это то, как далеко вокруг этого шестиугольника оканчивается точка.

Это расчет на странице wiki Wiki calc

Изучите шестиугольник и положение 51,153,204 на нем.

Основы шестиугольника

Сначала мы масштабируем значения R, G, B, чтобы заполнить интервал [0,1].

R = R / 255    R =  51 / 255 = 0.2
G = G / 255    G = 153 / 255 = 0.6
B = B / 255    B = 204 / 255 = 0.8

Затем найдите значения max и min R, G, B

M = max(R, G, B)    M = max(0.2, 0.6, 0.8) = 0.8
m = min(R, G, B)    m = min(0.2, 0.6, 0.8) = 0.2

Затем вычислите C (chroma). Хрома определяется как:

... chroma - это примерно расстояние от начала координат.

суб >

Chroma - относительный размер шестиугольника, проходящего через точку...

C = OP / OP'
C = M - m
C = 0.8- 0.2 = 0.6

Теперь мы имеем значения R, G, B и C. Если мы проверим условия, if M = B возвращает значение true для 51,153,204. Итак, мы будем использовать H'= (R - G) / C + 4.

Позвольте снова проверить шестиугольник. (R - G) / C дает длину сегмента BP.

segment = (R - G) / C = (0.2 - 0.6) / 0.6 = -0.6666666666666666

Мы поместим этот сегмент на внутренний шестиугольник. Исходной точкой шестиугольника является R (красный) при 0 °. Если длина сегмента положительная, она должна быть на RY, если она отрицательная, она должна быть на RM. В этом случае он отрицательный -0.6666666666666666 и находится на краю RM.

Позиция и сдвиг сегмента

Затем нам нужно сдвинуть положение сегмента, или, скорее, P₁, наберет B (потому что M = B). Синий находится в 240°. Шестигранник имеет 6 сторон. Каждой стороне соответствует 60°. 240 / 60 = 4. Нам нужно сдвинуть (приращение) P₁ на 4 (что составляет 240 °). После сдвига P₁ будет в P, и мы получим длину RYGCP.

segment = (R - G) / C = (0.2 - 0.6) / 0.6 = -0.6666666666666666
RYGCP   = segment + 4 = 3.3333333333333335

Окружность шестиугольника 6, что соответствует 360°. 53,151,204 Расстояние до составляет 3.3333333333333335. Если мы умножим 3.3333333333333335 на 60, мы получим его положение в градусах.

H' = 3.3333333333333335
H  = H' * 60 = 200°

В случае if M = R, поскольку мы поместим один конец отрезка в R (0 °), нам не нужно сдвигать отрезок на R, если длина сегмента положительна. Положение P₁ будет положительным. Но если длина сегмента отрицательная, нам нужно сдвинуть ее на 6, потому что отрицательное значение означает, что позиция angular больше 180 °, и нам нужно сделать полный оборот.

Таким образом, ни голландское wiki-решение hue = (g - b) / c;, ни англо-wiki-решение hue = ((g - b) / c) % 6; не будут работать для отрицательной длины сегмента. Только ответ SO hue = (g - b) / c + (g < b ? 6 : 0); работает как для отрицательных, так и для положительных значений.

JSFiddle: проверьте все три метода для rgb (255,71,99)


JSFiddle: визуально найдите положение цвета в RGB Cube и оттенок шестиугольника

Рабочий расчет оттенков:

console.log(rgb2hue(51,153,204));
console.log(rgb2hue(255,71,99));
console.log(rgb2hue(255,0,0));
console.log(rgb2hue(255,128,0));
console.log(rgb2hue(124,252,0));

function rgb2hue(r, g, b) {
  r /= 255;
  g /= 255;
  b /= 255;
  var max = Math.max(r, g, b);
  var min = Math.min(r, g, b);
  var c   = max - min;
  var hue;
  if (c == 0) {
    hue = 0;
  } else {
    switch(max) {
      case r:
        var segment = (g - b) / c;
        var shift   = 0 / 60;       // R° / (360° / hex sides)
        if (segment < 0) {          // hue > 180, full rotation
          shift = 360 / 60;         // R° / (360° / hex sides)
        }
        hue = segment + shift;
        break;
      case g:
        var segment = (b - r) / c;
        var shift   = 120 / 60;     // G° / (360° / hex sides)
        hue = segment + shift;
        break;
      case b:
        var segment = (r - g) / c;
        var shift   = 240 / 60;     // B° / (360° / hex sides)
        hue = segment + shift;
        break;
    }
  }
  return hue * 60; // hue is in [0,6], scale it up
}

Ответ 2

Продолжая свой комментарий, английская версия выглядит правильно, но я не уверен, что происходит в голландской версии, так как я не понимаю страницу WIKI.

Вот версия ES6, которую я сделал с английской страницы WIKI, наряду с некоторыми примерами данных, которые, по-видимому, соответствуют примерам WIKI (дают или принимают цифровую точность Javascript). Надеюсь, он может пригодиться при создании вашей собственной функции.

// see: https://en.wikipedia.org/wiki/RGB_color_model
// see: https://en.wikipedia.org/wiki/HSL_and_HSV

// expects R, G, B, Cmax and chroma to be in number interval [0, 1]
// returns undefined if chroma is 0, or a number interval [0, 360] degrees
function hue(R, G, B, Cmax, chroma) {
  let H;
  if (chroma === 0) {
    return H;
  }
  if (Cmax === R) {
    H = ((G - B) / chroma) % 6;
  } else if (Cmax === G) {
    H = ((B - R) / chroma) + 2;
  } else if (Cmax === B) {
    H = ((R - G) / chroma) + 4;
  }
  H *= 60;
  return H < 0 ? H + 360 : H;
}

// returns the average of the supplied number arguments
function average(...theArgs) {
  return theArgs.length ? theArgs.reduce((p, c) => p + c, 0) / theArgs.length : 0;
}

// expects R, G, B, Cmin, Cmax and chroma to be in number interval [0, 1]
// type is by default 'bi-hexcone' equation
// set 'luma601' or 'luma709' for alternatives
// see: https://en.wikipedia.org/wiki/Luma_(video)
// returns a number interval [0, 1]
function lightness(R, G, B, Cmin, Cmax, type = 'bi-hexcone') {
  if (type === 'luma601') {
    return (0.299 * R) + (0.587 * G) + (0.114 * B);
  }
  if (type === 'luma709') {
    return (0.2126 * R) + (0.7152 * G) + (0.0772 * B);
  }
  return average(Cmin, Cmax);
}

// expects L and chroma to be in number interval [0, 1]
// returns a number interval [0, 1]
function saturation(L, chroma) {
  return chroma === 0 ? 0 : chroma / (1 - Math.abs(2 * L - 1));
}

// returns the value to a fixed number of digits
function toFixed(value, digits) {
  return Number.isFinite(value) && Number.isFinite(digits) ? value.toFixed(digits) : value;
}

// expects R, G, and B to be in number interval [0, 1]
// returns a Map of H, S and L in the appropriate interval and digits
function RGB2HSL(R, G, B, fixed = true) {
  const Cmin = Math.min(R, G, B);
  const Cmax = Math.max(R, G, B);
  const chroma = Cmax - Cmin;
  // default 'bi-hexcone' equation
  const L = lightness(R, G, B, Cmin, Cmax);
  // H in degrees interval [0, 360]
  // L and S in interval [0, 1]
  return new Map([
    ['H', toFixed(hue(R, G, B, Cmax, chroma), fixed && 1)],
    ['S', toFixed(saturation(L, chroma), fixed && 3)],
    ['L', toFixed(L, fixed && 3)]
  ]);
}

// expects value to be number in interval [0, 255]
// returns normalised value as a number interval [0, 1]
function colourRange(value) {
  return value / 255;
};

// expects R, G, and B to be in number interval [0, 255]
function RGBdec2HSL(R, G, B) {
  return RGB2HSL(colourRange(R), colourRange(G), colourRange(B));
}

// converts a hexidecimal string into a decimal number
function hex2dec(value) {
  return parseInt(value, 16);
}

// slices a string into an array of paired characters
function pairSlicer(value) {
  return value.match(/../g);
}

// prepend '0 to the start of a string and make specific length
function prePad(value, count) {
  return ('0'.repeat(count) + value).slice(-count);
}

// format hex pair string from value
function hexPair(value) {
  return hex2dec(prePad(value, 2));
}

// expects R, G, and B to be hex string in interval ['00', 'FF']
// without a leading '#' character
function RGBhex2HSL(R, G, B) {
  return RGBdec2HSL(hexPair(R), hexPair(G), hexPair(B));
}

// expects RGB to be a hex string in interval ['000000', 'FFFFFF']
// with or without a leading '#' character
function RGBstr2HSL(RGB) {
  const hex = prePad(RGB.charAt(0) === '#' ? RGB.slice(1) : RGB, 6);
  return RGBhex2HSL(...pairSlicer(hex).slice(0, 3));
}

// expects value to be a Map object
function logIt(value) {
  console.log(value);
  document.getElementById('out').textContent += JSON.stringify([...value]) + '\n';
};

logIt(RGBstr2HSL('000000'));
logIt(RGBstr2HSL('#808080'));
logIt(RGB2HSL(0, 0, 0));
logIt(RGB2HSL(1, 1, 1));
logIt(RGBdec2HSL(0, 0, 0));
logIt(RGBdec2HSL(255, 255, 254));
logIt(RGBhex2HSL('BF', 'BF', '00'));
logIt(RGBstr2HSL('008000'));
logIt(RGBstr2HSL('80FFFF'));
logIt(RGBstr2HSL('8080FF'));
logIt(RGBstr2HSL('BF40BF'));
logIt(RGBstr2HSL('A0A424'));
logIt(RGBstr2HSL('411BEA'));
logIt(RGBstr2HSL('1EAC41'));
logIt(RGBstr2HSL('F0C80E'));
logIt(RGBstr2HSL('B430E5'));
logIt(RGBstr2HSL('ED7651'));
logIt(RGBstr2HSL('FEF888'));
logIt(RGBstr2HSL('19CB97'));
logIt(RGBstr2HSL('362698'));
logIt(RGBstr2HSL('7E7EB8'));
<pre id="out"></pre>

Ответ 3

Оттенок в HSL подобен углу в круге. Соответствующие значения для такого угла находятся в интервале 0..360. Однако отрицательные значения могут возникнуть из расчета. И почему эти три формулы разные. Они делают то же самое в конце, они просто обрабатывают по-разному значения за пределами интервала 0..360. Или, если быть точным, интервал 0..6, который затем умножается на 60 - 0..360

hue = (g - b) / c; // dutch wiki ничего не делает с отрицательными значениями и предполагает, что последующий код может обрабатывать отрицательные значения H.

hue = ((g - b) / c) % 6; // eng wiki использует оператор % для соответствия значениям внутри интервала 0..6

hue = (g - b) / c + (g < b ? 6 : 0); // SO answer заботится о отрицательных значениях, добавляя +6, чтобы сделать их положительными

Вы видите, что это просто косметические различия. Любая вторая или третья формула будет отлично работать для вас.

Ответ 4

Эта страница предоставляет функцию для преобразования между цветовыми пространствами, включая RGB в HSL.

function RGBToHSL(r,g,b) {
  // Make r, g, and b fractions of 1
  r /= 255;
  g /= 255;
  b /= 255;

  // Find greatest and smallest channel values
  let cmin = Math.min(r,g,b),
      cmax = Math.max(r,g,b),
      delta = cmax - cmin,
      h = 0,
      s = 0,
      l = 0;

  // Calculate hue
  // No difference
  if (delta == 0)
    h = 0;
  // Red is max
  else if (cmax == r)
    h = ((g - b) / delta) % 6;
  // Green is max
  else if (cmax == g)
    h = (b - r) / delta + 2;
  // Blue is max
  else
    h = (r - g) / delta + 4;

  h = Math.round(h * 60);

  // Make negative hues positive behind 360°
  if (h < 0)
      h += 360;

  // Calculate lightness
  l = (cmax + cmin) / 2;

  // Calculate saturation
  s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));

  // Multiply l and s by 100
  s = +(s * 100).toFixed(1);
  l = +(l * 100).toFixed(1);

  return "hsl(" + h + "," + s + "%," + l + "%)";
}