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

Я создаю инструмент «Палитра цветов», и для ползунка HSL мне нужно иметь возможность преобразовывать RGB в HSL. Когда я искал в SO способ преобразования, я нашел этот вопрос Преобразование цветов HSL в RGB < / а>.

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

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

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

введите описание изображения здесь

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

введите описание изображения здесь

и это из ответов на «Преобразование цвета 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];
}

person akinuri    schedule 24.08.2016    source источник
comment
Английский мне кажется правильным, голландский я не узнаю и не понимаю, что там написано на вики-странице. :)   -  person Xotic750    schedule 24.08.2016
comment
Вот октавная реализация hsv2rgb: hg.savannah .gnu.org / hgweb / octave / file / 549f8625a61b / scripts /   -  person knb    schedule 18.09.2016


Ответы (4)


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

RGB

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

Гистограмма RGB

RGB куб

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

2D-графики RGB

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

Стороны куба RGB

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

RGB Cube

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

Положение цвета куба RGB

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

Теперь, когда у нас есть RGB-куб, давайте спроецируем его на шестиугольник. Сначала мы наклоняем куб на 45 ° на x, а затем на 35,264 ° на y. После второго наклона черный угол находится внизу, а белый угол - вверху, и оба они проходят через ось z.

Угол наклона куба RGB

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

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

А положение 51,153,204 на шестиугольнике будет следующим:

Положение цветового тона

Расчет оттенка

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

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

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

Это расчет со страницы вики HSL и HSV. Мы будем использовать его в этом объяснении.

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 (цветность). Цветность определяется как:

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

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

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

Теперь у нас есть значения R, G, B и C. Если мы проверим условия, if M = B вернет истину для 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, потому что отрицательное значение означает, что угловое положение больше 180 °, и нам нужно сделать полный поворот.

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

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


JSFiddle: определение положения цвета в кубе RGB и визуальное изменение оттенка шестиугольника

Расчет рабочего оттенка:

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
}

person akinuri    schedule 25.08.2016
comment
Привет, я шокирован тем, что ваш ответ игнорируется или не принимается большинством. Большое вам спасибо за объяснение, я уже некоторое время изучаю его, и ваш ответ самый лучший - person Tech Wizard; 17.01.2017
comment
@AlexanderGurevich Мне просто нравится разбираться в вещах, и это было хорошее исследование. Я хотел рассказать об этом открыто. Рад, что кому-то это пригодилось :) - person akinuri; 18.01.2017
comment
Вау, потрясающий ответ! Я бы проголосовал за него несколько раз, если бы это было возможно: D - person Florian Blum; 19.12.2018
comment
Работаем ли мы с RGB как есть? или нам нужно сначала его линеаризовать? - person Ivan Rubinson; 10.03.2021
comment
Тем не менее, в 2021 году это так замечательно. Спасибо вам за вашу работу. - person peter_the_oak; 03.04.2021

Эта страница предоставляет функцию преобразования между цветовыми пространствами, включая 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 + "%)";
}
person Crashalot    schedule 17.10.2019

Оттенок в 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, чтобы сделать их положительными

Вы видите, что это просто косметические отличия. Вам подойдет вторая или третья формула.

person Matey    schedule 24.08.2016
comment
Не думаю, что второй подойдет как для положительных, так и для отрицательных значений. Я не вижу сценария, в котором (g - b) / c) больше 6. Оно всегда должно быть ‹= 1. Так что мод кажется избыточным. Только третья формула работает должным образом. Смотрите мой обновленный ответ. - person akinuri; 29.08.2016
comment
@akinuri: mod для отрицательных значений дивиденда увеличивает дивиденд на делитель, пока он не станет положительным. Это в точности то же самое, что и третья формула. - person Matey; 13.09.2016
comment
См. Этот jsfiddle. en_rgb2hue() использует расчет мода. Он не дает правильного значения. - person akinuri; 13.09.2016
comment
Виноват. Очевидно, есть два мира относительно того, как % должен обрабатывать отрицательные значения. Спасибо, что поправили меня. - person Matey; 14.09.2016

Продолжая мой комментарий, английская версия выглядит правильно, но я не уверен, что происходит в голландской версии, поскольку я не понимаю страницу 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's 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>

person Xotic750    schedule 24.08.2016
comment
Я действительно ценю усилия, но это кажется намного более сложным, чем то, что я уже написал :) Я снова и снова читал вики-страницы и создавал проекцию rgb-куб в шестиугольник в 3D-программе. Я действительно близок к пониманию всего в расчетах. Я скоро опубликую свое понимание. (Извините за задержку с ответом :) - person akinuri; 25.08.2016
comment
Я не знаю ничего более сложного (конечно, всего он может делать еще несколько вещей, чем ваш пример), но в основном это то же самое, но разбито на более мелкие тестируемые и повторно используемые функции, а не на одну функцию. Я, вероятно, мог бы сделать это в одной строке кода, но это было бы не очень удобно для чтения, повторного использования или сопровождения. О, и я использовал ES6, а не только ES3, я знаю, что вы не просили никакого кода, но мне нужно было стереть немного ржавчины, и я подумал, что могу также поделиться с вами своими вознями в ответе, а не просто в комментарии. :) - person Xotic750; 25.08.2016
comment
Я опубликовал свой ответ (если вы его еще не видели). Сейчас это неполно. Обновил один раз. Собираюсь обновить его снова. Я хотел бы получить ваш отзыв (возможно, я делаю это неправильно, иди) - person akinuri; 26.08.2016
comment
Это очень красивое описание того, что нужно вычислить, но пока не отвечает на фактический вопрос, который вы разместили. Я как бы думаю, что, возможно, ваши вопросы и ответы больше подходят для mathoverflow.net, поскольку на самом деле, похоже, у него не так много дел с аспектом программирования, но в большей степени имеет отношение к математике. - person Xotic750; 27.08.2016
comment
Это еще не сделано. Добавлю последнюю часть, где расскажу о расчете. Мне так понравилась эта конверсия, что я хотел быть с ней основательно. Уверен, это будет полезно для будущих посетителей. - person akinuri; 27.08.2016
comment
Я только что закончил обновлять свой ответ и заметил, что hue = ((g - b) / c) % 6; приводит к неверным значениям. Он не обрабатывает отрицательные значения. Также я не вижу сценария, в котором (g - b) / c) больше 6. Оно всегда должно быть ‹= 1. Так зачем делать мод? См. Этот тест fiddle. - person akinuri; 29.08.2016
comment
В моем ответе вы увидите проверку на отрицательный результат и добавите 360. Я не так внимательно изучал теорию или математику, я просто предположил, что это правильно, поскольку я видел это в нескольких источниках, и цифры приходят как и ожидалось. - person Xotic750; 29.08.2016