web-dev-qa-db-ja.com

RGBからHSLへの変換

カラーピッカーツールを作成しています。HSLスライダーの場合、RGBをHSLに変換できる必要があります。 SOで変換を行う方法を検索したところ、この質問 HSLからRGBカラーへの変換 が見つかりました。

RGBからHSLへの変換を行う機能を提供しますが、計算で実際に何が行われているかについての説明はありません。よりよく理解するために、ウィキペディアで HSLおよびHSV を読みました。

後で、「HSLおよびHSV」ページの計算を使用して、「HSLからRGB色への変換」から関数を書き直しました。

Rが最大値の場合、色相の計算にこだわっています。 「HSLおよびHSV」ページの計算を参照してください。

enter image description here

これは別の wikiページ からのもので、オランダ語です:

enter image description here

これは answers から「HSLからRGBカラーへの変換」からです。

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

いくつかのRGB値を使用して3つすべてをテストしましたが、(正確ではないにしても)同様の結果が得られるようです。私が疑問に思っているのは、彼らが同じことを実行しているということですか?特定の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];
}
23
akinuri

私はいくつかのwikiページを読んでさまざまな計算をチェックし、六角形へのRGBキューブ投影の視覚化を作成してきました。そして、この変換についての私の理解を投稿したいと思います。この変換(幾何学的図形を使用したカラーモデルの表現)が興味深いと思うので、できる限り徹底的になるようにします。まず、RGBから始めましょう。

RGB

まあ、これは本当に多くの説明を必要としません。最も単純な形式では、R、G、Bの3つの値が[0,255]の範囲にあります。たとえば、_51,153,204_。棒グラフを使用して表現できます。

RGB Bar Graph

RGBキューブ

3D空間で色を表現することもできます。 RGBに対応する3つの値XYZがあります。 3つの値はすべて_[0,255]_の範囲にあり、その結果キューブになります。しかし、RGBキューブを作成する前に、まず2D空間で作業しましょう。 R、G、Bの2つの組み合わせにより、RG、RB、GBが得られます。これらを平面上にグラフ化すると、次のようになります。

RGB 2D Graphs

これらは、RGBキューブの最初の3つの側面です。それらを3D空間に配置すると、半立方体になります。

RGB Cube Sides

上記のグラフを確認すると、2つの色を混ぜることで、(255,255)の新しい色が得られます。これらは黄色、マゼンタ、シアンです。繰り返しますが、YM、YC、MCの2つの組み合わせがあります。これらは、キューブの欠けている側面です。それらを追加すると、完全なキューブが得られます。

RGB Cube

そして、このキューブ内の_51,153,204_の位置:

RGB Cube Color Position

RGB Cubeの六角形への投影

RGBキューブができたので、六角形に投影しましょう。まず、キューブをxで45°傾け、次にyで35.264°傾けます。 2番目の傾斜の後、黒い角が下に、白い角が上にあり、両方ともz軸を通過します。

RGB Cube Tilt

ご覧のとおり、立方体を上から見ると、正しい色相の順序で六角形の外観が得られます。しかし、これを実際の六角形に投影する必要があります。私たちがしているのは、立方体の上面図と同じサイズの六角形を描くことです。六角形のすべての角は立方体の角と色に対応し、白色の立方体の上部の角は六角形の中心に投影されます。黒は省略されます。そして、すべての色を六角形にマッピングすると、見た目が正しくなります。

Cube to Hexagon Projection

そして、六角形上の_51,153,204_の位置は次のようになります。

Hue Color Position

色相の計算

計算を行う前に、色相を定義しましょう。

色相は、おおよそ、投影のある点に対するベクトルの角度で、赤は0°です。

...色相は、その六角形のエッジの周囲にあるポイントの距離です。

これは HSLおよびHSV wikiページからの計算です。この説明ではこれを使用します。

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
_

次に、_R, G, B_のmaxおよびmin値を見つけます

_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
_

これで、RGB、およびCの値が得られました。条件をチェックすると、_if M = B_は_51,153,204_に対してtrueを返します。したがって、H'= (R - G) / C + 4を使用します。

六角形をもう一度確認しましょう。 _(R - G) / C_は、BPセグメントの長さを示します。

_segment = (R - G) / C = (0.2 - 0.6) / 0.6 = -0.6666666666666666
_

このセグメントを内側の六角形に配置します。六角形の開始点は0°のR(赤)です。セグメントの長さが正の場合、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₁_の位置は正になります。しかし、セグメントの長さが負の場合、負の値はangular位置が180°より大きいことを意味し、完全に回転する必要があるため、6だけシフトする必要があります。

そのため、オランダ語のWikiソリューションhue = (g - b) / c;もEng wikiソリューションhue = ((g - b) / c) % 6;も、セグメント長が負の場合は機能しません。 SO answer hue = (g - b) / c + (g < b ? 6 : 0);]のみが負の値と正の値の両方で機能します。

JSFiddle:rgb(255,71,99)の3つのメソッドすべてをテストする


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
}_
65
akinuri

私のコメントを続けると、英語版は正しいように見えますが、WIKIページが理解できないため、オランダ語版で何が起こっているのかわかりません。

以下は、英語のWIKIページから作成したES6バージョンと、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>
2
Xotic750

HSLの色相は、円の中の角度のようなものです。このような角度に関連する値は、0..360間隔にあります。ただし、負の値が計算から出てくる場合があります。そのため、これら3つの式は異なります。これらは最終的に同じことを行い、0..360間隔以外の値を異なる方法で処理します。または、正確には、最終的に60から0..360を掛けた0..6間隔

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を追加して正の値にすることで負の値を処理します

これらは表面的な違いにすぎないことがわかります。 2番目または3番目の式のいずれかが正常に機能します。

2
Matey