semmixcolor and perception

RGB to hue angle

The hue angle can be computed from RGB in two ways: around the hexagon, as in HSV and HSL, or geometrically, as in HSI. They give almost the same result, but not exactly. This is the inverse of hue angle to RGB.

The hexagonal formula

Used in HSV and HSL. First find the largest component: that picks a 120° sector of the wheel. Then interpolate linearly within it:

max = max(R, G, B), min = min(R, G, B), d = max - min d = 0 → H undefined (gray) max = R → H = 60° · ((G - B) / d) (+360° if negative) max = G → H = 60° · ((B - R) / d + 2) max = B → H = 60° · ((R - G) / d + 4)

For RGB (200, 80, 40): max = R, so H = 60° · (0.3137 - 0.1569) / 0.6275 = 60° · 0.25 = 15°.

The geometric formula

Used in HSI. The color is projected onto the plane perpendicular to the gray axis and the true angle from the red direction is measured:

H = atan2( √3 · (G - B), 2R - G - B ) (in degrees, +360° if negative) the same value in HSI form: θ = arccos( ½·[(R - G) + (R - B)] / √[(R - G)² + (R - B)·(G - B)] ) H = θ if B ≤ G, H = 360° - θ if B > G

For our color: H = atan2(1.732 · 0.1569, 1.5686 - 0.3137 - 0.1569) = atan2(0.2716, 1.0980) = 13.9°.

The atan2 version is handier in code: it needs no separate branch for B > G and does not divide by zero when the denominator is small.

How much they differ

The two formulas agree exactly every 60°, at the corners of the hexagon: red, yellow, green, cyan, blue and magenta. In between, the hexagonal formula is a linear approximation of the true angle. The largest difference is about 1.1° (for example around 253° HSV). It does not depend on saturation or brightness, only on hue, because both formulas depend only on the ratios of the differences between components.

A 1° difference is invisible, but it matters when moving values around: 15° from a graphics program is 13.9° in an image analysis library that uses HSI.

Grays

When R = G = B the color has no hue. The hexagonal formula would divide by zero, and atan2(0, 0) returns 0, which only pretends to be a result. Common approaches:

Code

// hexagonal formula (HSV, HSL), r, g, b in 0..1
function hexHue(r, g, b) {
  const max = Math.max(r, g, b), min = Math.min(r, g, b), d = max - min;
  if (d < 1e-9) return NaN;                 // gray
  let h;
  if (max === r) h = (g - b) / d;
  else if (max === g) h = (b - r) / d + 2;
  else h = (r - g) / d + 4;
  h *= 60;
  return h < 0 ? h + 360 : h;
}

// geometric formula (HSI)
function geoHue(r, g, b) {
  const h = Math.atan2(Math.sqrt(3) * (g - b), 2 * r - g - b) * 180 / Math.PI;
  return h < 0 ? h + 360 : h;
}