semmixkolor i percepcja

Przekształcenie HSI i RGB

HSI liczy odcień geometrycznie, więc i powrót do RGB wygląda inaczej niż w HSV i HSL: zamiast sześciu sektorów po 60° są trzy po 120°, a w środku pojawia się cosinus. Opis modelu jest na stronie modele HLS i HSI.

RGB na HSI

I = (R + G + B) / 3 S = 1 - min(R, G, B) / I (S = 0, gdy I = 0) H = atan2( √3 · (G - B), 2R - G - B ) (+360°, jeśli ujemny)

Wzór na H z atan2 daje ten sam wynik co klasyczna postać z arccos (zob. RGB na kąt barwy), a jest prostszy w kodzie.

HSI na RGB

Koło dzieli się na trzy sektory. W każdym jedna składowa jest najmniejsza i równa I · (1 - S):

sektor RG, 0° ≤ H < 120°: B = I · (1 - S) R = I · (1 + S · cos H / cos(60° - H)) G = 3I - (R + B) sektor GB, 120° ≤ H < 240°: H' = H - 120° R = I · (1 - S) G = I · (1 + S · cos H' / cos(60° - H')) B = 3I - (R + G) sektor BR, 240° ≤ H < 360°: H' = H - 240° G = I · (1 - S) B = I · (1 + S · cos H' / cos(60° - H')) R = 3I - (G + B)

Przykład w obie strony

RGB (200, 80, 40), #c85028:

R = 0,7843 G = 0,3137 B = 0,1569 I = (0,7843 + 0,3137 + 0,1569) / 3 = 0,4183 S = 1 - 0,1569 / 0,4183 = 0,625 H = atan2(1,732 · 0,1569; 2 · 0,7843 - 0,3137 - 0,1569) = 13,9° HSI = (13,9°; 62,5%; 41,8%) z powrotem, sektor RG: B = 0,4183 · 0,375 = 0,1569 R = 0,4183 · (1 + 0,625 · cos 13,9° / cos 46,1°) = 0,4183 · (1 + 0,625 · 0,9707 / 0,6934) = 0,4183 · 1,8749 = 0,7843 G = 3 · 0,4183 - (0,7843 + 0,1569) = 0,3137 RGB = (200, 80, 40)

Wyniki spoza zakresu

W HSV i HSL każda trójka wartości z zakresu daje poprawne RGB. W HSI tak nie jest: przestrzeń HSI z I i S w pełnym zakresie 0..1 jest większa niż sześcian RGB. Przykład:

H = 0°, S = 1, I = 0,6 B = 0,6 · 0 = 0 R = 0,6 · (1 + 1 · cos 0° / cos 60°) = 0,6 · 3 = 1,8 G = 1,8 - 1,8 = 0

R = 1,8 nie istnieje w RGB. Czysta czerwień ma w HSI tylko I = 1/3, więc pełnego nasycenia nie da się połączyć z większą intensywnością. Wynik trzeba obciąć do 0..1 (co zmienia barwę) albo zmniejszyć S. Przy edycji obrazu w HSI warto to sprawdzać po każdej zmianie.

Kod

const DEG = Math.PI / 180;

function rgbToHsi(r, g, b) {
  const i = (r + g + b) / 3;
  const s = i === 0 ? 0 : 1 - Math.min(r, g, b) / i;
  let h = Math.atan2(Math.sqrt(3) * (g - b), 2 * r - g - b) / DEG;
  if (h < 0) h += 360;
  return { h: s === 0 ? 0 : h, s, i };
}

function hsiToRgb(h, s, i) {
  h = ((h % 360) + 360) % 360;
  const sector = Math.floor(h / 120);          // 0: RG, 1: GB, 2: BR
  const hh = (h - 120 * sector) * DEG;
  const low = i * (1 - s);
  const high = i * (1 + (s * Math.cos(hh)) / Math.cos(60 * DEG - hh));
  const mid = 3 * i - (low + high);
  if (sector === 0) return { r: high, g: mid, b: low };
  if (sector === 1) return { r: low, g: high, b: mid };
  return { r: mid, g: low, b: high };
}