semmixkolor i percepcja

RGB na kąt barwy (hue)

Kąt barwy można wyznaczyć z RGB na dwa sposoby: po sześciokącie, jak w HSV i HSL, albo geometrycznie, jak w HSI. Dają prawie to samo, ale nie dokładnie to samo. To odwrotność przekształcenia kąt barwy na RGB.

Wzór sześciokątny

Używany w HSV i HSL. Najpierw ustala się, która składowa jest największa: to wyznacza sektor koła o szerokości 120°. Potem liniowo, wewnątrz sektora:

max = max(R, G, B), min = min(R, G, B), d = max - min d = 0 → H nieokreślony (szarość) max = R → H = 60° · ((G - B) / d) (+360°, jeśli wynik ujemny) max = G → H = 60° · ((B - R) / d + 2) max = B → H = 60° · ((R - G) / d + 4)

Dla RGB (200, 80, 40): max = R, więc H = 60° · (0,3137 - 0,1569) / 0,6275 = 60° · 0,25 = 15°.

Wzór geometryczny

Używany w HSI. Barwę rzutuje się na płaszczyznę prostopadłą do osi szarości i mierzy prawdziwy kąt od kierunku czerwieni:

H = atan2( √3 · (G - B), 2R - G - B ) (w stopniach, +360°, jeśli ujemny) ta sama wartość w postaci z HSI: θ = arccos( ½·[(R - G) + (R - B)] / √[(R - G)² + (R - B)·(G - B)] ) H = θ gdy B ≤ G, H = 360° - θ gdy B > G

Dla naszego koloru: H = atan2(1,732 · 0,1569; 1,5686 - 0,3137 - 0,1569) = atan2(0,2716; 1,0980) = 13,9°.

Wersja z atan2 jest wygodniejsza w kodzie: nie wymaga osobnego warunku dla B > G i nie dzieli przez zero, gdy mianownik jest mały.

Jak bardzo się różnią

Oba wzory zgadzają się dokładnie co 60°, na wierzchołkach sześciokąta: dla czerwieni, żółci, zieleni, cyjanu, błękitu i magenty. Między nimi wzór sześciokątny jest liniowym przybliżeniem prawdziwego kąta. Największa różnica wynosi około 1,1° (na przykład w okolicy 253° HSV). Nie zależy od nasycenia ani jasności, tylko od odcienia, bo oba wzory zależą wyłącznie od proporcji różnic między składowymi.

Różnica 1° jest niewidoczna, ale ma znaczenie przy przenoszeniu wartości: 15° z programu graficznego to 13,9° w bibliotece analizy obrazu używającej HSI.

Szarości

Gdy R = G = B, barwa nie ma odcienia. Wzór sześciokątny dzieliłby przez zero, a atan2(0, 0) zwraca 0, co tylko udaje wynik. Typowe rozwiązania:

Kod

// wzór sześciokątny (HSV, HSL), r, g, b w zakresie 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;                 // szarość
  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;
}

// wzór geometryczny (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;
}