RGB转HSL转换

74

我正在创建一个颜色选择器工具,对于HSL滑块,我需要能够将RGB转换为HSL。当我在SO上搜索如何进行转换时,我发现了这个问题HSL到RGB颜色转换

虽然它提供了一个函数来执行从RGB到HSL的转换,但我并没有看到任何关于计算过程究竟是怎么样的解释。为了更好地理解它,我阅读了维基百科上的HSL和HSV

后来,我使用“HSL和HSV”页面中的计算方式重写了“HSL到RGB颜色转换”的函数。

如果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
英文的看起来对我来说是正确的,荷兰语的我不认识,我也不理解维基页面上写了什么。 :) - Xotic750
1
这是hsv2rgb的Octave实现:http://hg.savannah.gnu.org/hgweb/octave/file/549f8625a61b/scripts/image/hsv2rgb.m - knb
4个回答

272
我一直在阅读几个维基页面,检查不同的计算,并创建RGB立方体投影到六边形的可视化。我想发布我对这种转换的理解。因为我发现这种用几何形状表示颜色模型的转换很有趣,所以我会尽可能详细地介绍。首先,让我们从RGB开始。
RGB
好吧,这并不需要太多的解释。在其最简单的形式中,您有三个值,R、G和B,范围为[0,255]。例如,51,153,204。我们可以使用条形图来表示它:

RGB Bar Graph

RGB 立方体

我们也可以在三维空间中表示颜色。我们有三个值 RGB,分别对应于 XYZ。这三个值都在 [0,255] 的范围内,形成了一个立方体。但在创建 RGB 立方体之前,让我们先在二维空间中工作。R、G、B 的两种组合给出了:RG、RB、GB。如果我们将它们绘制在平面上,就会得到以下结果:

RGB 2D Graphs

这些是RGB立方体的前三个面。如果我们将它们放在一个三维空间中,就会得到一个半个立方体。

RGB Cube Sides

如果您查看上面的图表,通过混合两种颜色,我们在(255,255)处得到一种新颜色,这些颜色是黄色、品红色和青色。同样,这些的两种组合给出了YM、YC和MC。这些是立方体的缺失面。一旦我们添加它们,我们就得到了一个完整的立方体:

RGB Cube

而在这个立方体中,51,153,204 的位置是:

RGB Cube Color Position

将RGB立方体投影到六边形上

现在我们有了RGB立方体,让我们将其投影到一个六边形上。首先,在x轴上倾斜45度,然后在y轴上倾斜35.264度。在第二次倾斜后,黑色角落位于底部,白色角落位于顶部,并且它们都经过z轴。

RGB Cube Tilt

如您所见,当我们从顶部看立方体时,可以看到我们想要的六边形外观和正确的色调顺序。但我们需要将其投影到一个真正的六边形上。我们所做的是绘制一个大小与立方体顶部视图相同的六边形。六边形的所有角落都对应于立方体的角落和颜色,而白色的立方体顶角被投影到六边形的中心。黑色被省略。如果我们将每种颜色映射到六边形上,我们就可以得到右侧的外观。

Cube to Hexagon Projection

51,153,204 在六边形上的位置将是:

Hue Color Position

计算色调

在我们进行计算之前,让我们定义一下什么是色调。

色调大致上是指向投影中某个点的向量的角度,其中红色为0°。

这是HSL和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

接下来,找到R、G、Bmaxmin值。

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是通过某一点的六边形的相对大小...
C = OP / OP'
C = M - m
C = 0.8- 0.2 = 0.6

现在,我们有了R、G、B和C值。如果我们检查条件,当51,153,204时,if M = B返回true。因此,我们将使用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。我们需要通过4(即240°)来移动(增加)点P₁。移动后,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°

如果 M = R ,由于我们将线段的一端放在R(0°)处,如果线段长度为正,则无需将其移动到R。 P₁的位置将是正的。但是,如果线段长度为负,则需要将其移动6个单位,因为负值意味着角度位置大于180°,我们需要进行完整的旋转。
因此,荷兰维基解决方案 hue = (g - b) / c;和Eng维基解决方案 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
}


20
你好,我很震惊你的回答被大多数人忽略或者未被看到。非常感谢你的解释,我已经研究了一段时间,你的回答是最好的。 - Tech Wizard
2
@AlexanderGurevich 我喜欢深入了解事物,这是一项很好的研究。我想公开它。我很高兴有人发现它有用 :) - akinuri
1
哇,太棒了的回答!如果可以的话,我会给它点赞多次:D - Florian Blum
1
直到2021年,这仍然是非常美妙的。感谢您的工作。 - peter_the_oak
2
@Binajmen,你可以查看问题中的rgb2hsl函数。所有三个色调计算(针对M=R)都在那里。只需删除前两个即可。 - akinuri
显示剩余10条评论

10

这个页面提供了一种函数,可以在颜色空间之间进行转换,包括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 + "%)";
}

第3、4和5行的代码并没有生成1的分数。它将数字缩放为比例。如果有什么问题,那就是计算255的分数。 - agiopnl

3

接着我的评论,英文版本看起来正确,但我不确定荷兰语版本中发生了什么,因为我不理解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>


我非常感谢你的努力,但这似乎比我已经写的更复杂 :) 我一遍又一遍地阅读维基页面,并在3D软件中创建RGB立方体到六边形的投影。我离完全理解计算中的所有内容非常接近了。我很快会发布我的理解。(抱歉回复晚了 :) - akinuri
1
我不知道更复杂的情况(当然,在所有能做的事情上,它可以比你的示例多做一些),但基本上它与单个函数相同,只是将其分解为较小的可测试和可重用函数。我可能可以在一行代码中完成它,但那样不太易读、可重用或可维护。哦,我使用的是ES6而不仅仅是ES3,我知道你没有要求任何代码,但我需要清除一些生锈,我想我不妨在答案中分享我的尝试,而不仅仅是评论。 :) - Xotic750
还没有完成。我会添加最后一部分,其中我将谈论计算。我对这个转换的事情非常喜欢,所以我想彻底了解它。我相信它对未来的访问者会有用。 - akinuri
1
我刚刚更新了我的回答,发现 hue = ((g - b) / c) % 6; 会导致错误的值。它不能处理负值。而且我没有看到任何情况下 (g - b) / c) 大于6。它应该始终小于等于1。所以为什么要取模?请查看这个 fiddle 测试。 - akinuri
1
在我的答案中,您将看到对负数的检查,并添加360。我没有仔细研究过理论或数学,只是假设它是正确的,因为我在几个来源中看到了它,并且数字符合预期。 - Xotic750
显示剩余2条评论

3
在HSL中,色调就像圆形中的角度。相关的值位于0到360的区间内。然而,计算可能会产生负值,这就是为什么这三个公式不同的原因。它们最终做的事情是相同的,只是对0到360之外的值处理方式不同。或者更准确地说,是0到6之间的值,然后乘以60变成0到360。 不对负值进行任何处理,并假定随后的代码可以处理负H值。 使用运算符将值适合0到6的区间内 通过加上+6来处理负值,使它们变成正数
你可以看到这些只是表面差异。第二个或第三个公式都能很好地工作。

1
我不认为第二种方法适用于正数和负数。我没有看到任何情况下(g - b) / c)大于6。它应该永远<= 1。因此,模数似乎是多余的。只有第三个公式按预期工作。请查看我的更新答案。 - akinuri
1
@akinuri:对于被除数为负数的情况,mod 运算会让被除数增加一个除数直到其变成正数。这与第三个公式完全相同。 - Matey
1
请查看此jsfiddleen_rgb2hue()使用模运算,但它并不能给出正确的值。 - akinuri
1
我的错。显然,关于如何处理“%”的负值有两个世界。感谢您纠正我。 - Matey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接