2016-08-24 19 views
9

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

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

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

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

enter image description here

Это из другого wiki page, которая находится в голландском:

enter image description here

и это от answers к «HSL в RGB цветовое преобразование ":

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

Я проверил все три с несколькими RG B, и они, похоже, создают аналогичные (если не точные) результаты. Мне интересно, они выполняют одно и то же? Получат ли я разные результаты для определенных значений 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]; 
} 
+0

на английском выглядит правильно мне, голландец, которого я не узнаю, и я не понимаю, что он говорит на странице вики. :) – Xotic750

+0

Вот октавная реализация hsv2rgb: http://hg.savannah.gnu.org/hgweb/octave/file/549f8625a61b/scripts/image/hsv2rgb.m – knb

ответ

11

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

RGB

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

RGB Bar Graph

RGB Куб

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

RGB 2D Graphs

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

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 на шестиугольника будет:

Hue Color Position

Расчет Hue

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

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

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

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

Wiki calc

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

Hexagon basics

Во-первых, мы масштабировать значения 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 относительный размер шестиугольника, проходящей через точку ...

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.

Segment position & shift

Далее, нам необходимо сместить положение сегмента, а точнее 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 °, и нам нужно сделать полный поворот.

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

JSFiddle: Test all three methods for rgb(255,71,99)


JSFiddle: Find a color's position in RGB Cube and hue hexagon visually

Работа Расчет Оттенок:

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 
 
}

+1

Привет, я шокирован тем, что вы отвечаете, игнорируется или не видит большинство. Большое спасибо за объяснение. Я занимаюсь этим некоторое время, и ваш ответ самый лучший. –

+0

@AlexanderGurevich. Мне просто нравится разбираться в вещах, и это было хорошее исследование. Я хотел разоблачить его. Я рад, что кто-то нашел это полезным :) – akinuri

3

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

+0

Я очень ценю усилия, но это кажется намного сложнее чем тот, который я уже написал :) Я читал wiki-страницы снова и снова и создавал проекцию rgb cube-to-hexagon в 3d-программном обеспечении. Я очень близок к пониманию * всего * в расчете. Я скоро опубликую свое понимание. (Извините за поздний ответ :) – akinuri

+0

Я не знаю о гораздо более сложном (конечно, во всем, что он может сделать еще несколько вещей, чем ваш пример), но в основном это то же самое, но разбито на более мелкие проверяемые и многоразовые функции чем одна функция. Возможно, я мог бы сделать это в одной строке кода, но это было бы не очень удобочитаемым, многоразовым или поддерживаемым. О, и я использовал ES6, а не только ES3, я знаю, что вы не просили какой-либо код, но мне нужно было отбросить ржавчину, и я подумал, что я могу поделиться своей работой с вами в ответ, а не только с комментарием.:) – Xotic750

+0

Я отправил свой ответ (если вы его еще не видели). Сейчас он неполный. Обновил его один раз. Собираюсь обновить его снова. Я бы хотел, чтобы ваши отзывы (я, возможно, делаю это неправильно, idk :) – akinuri

2

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

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

+0

Я не думаю, что второй будет работать как для положительных, так и для отрицательных значений. Я не вижу никакого сценария, где '(g - b)/c)' больше 6. Оно всегда должно быть <= 1. Таким образом, мода кажется излишним. Только третья формула работает так, как ожидалось. См. Мой обновленный ответ. – akinuri

+0

@akinuri: 'mod' для отрицательных значений дивиденда увеличивает дивиденд на делитель, пока он не будет положительным. Это точно так же, как и третья формула. – Matey

+0

См. Это [jsfiddle] (https://jsfiddle.net/f15dbw06/). 'en_rgb2hue()' использует вычисление мод. Это не дает правильного значения. – akinuri