semmixcolor and perception

Converting HSI and RGB

HSI computes hue geometrically, so the way back to RGB also looks different from HSV and HSL: instead of six 60° sectors there are three 120° ones, with a cosine in the middle. The model itself is described on the HLS and HSI models page.

RGB to HSI

I = (R + G + B) / 3 S = 1 - min(R, G, B) / I (S = 0 if I = 0) H = atan2( √3 · (G - B), 2R - G - B ) (+360° if negative)

The atan2 formula for H gives the same result as the classic arccos form (see RGB to hue angle) and is simpler in code.

HSI to RGB

The wheel splits into three sectors. In each, one component is the smallest and equals I · (1 - S):

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

Example both ways

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%) back, RG sector: 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)

Results out of range

In HSV and HSL every triple of in-range values gives valid RGB. In HSI it does not: HSI with I and S over the full 0..1 range is larger than the RGB cube. Example:

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 does not exist in RGB. Pure red has an HSI intensity of only 1/3, so full saturation cannot be combined with higher intensity. The result has to be clipped to 0..1 (which changes the color) or S reduced. When editing an image in HSI, check this after every change.

Code

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 };
}