Image Processing 5. Color Models

3 min read June 15, 2025 #image processing #javascript

Working with the RGB model isn't always convenient. For example, printers don't follow the RGB model, because they use white paper and consume less ink for light tones. This is why multiple models exist to solve various tasks. We'll discuss the main color models here. And to keep things interesting, we'll play around with some sliders.

logo5.jpg

RGB Correction#

RGB correction is used when you need to slightly adjust a photo if it has too much of an unwanted tint. For instance, incorrect white balance can give a photo a blue or orange tint.

The RGB correction filter is implemented very simply. There're three sliders with a range from -255 to 255 inclusive, one for each color component.

<input id="rangeR" type="range" min="-255" max="255" value="0" />
<input id="rangeG" type="range" min="-255" max="255" value="0" />
<input id="rangeB" type="range" min="-255" max="255" value="0" />

We simply iterate through the pixel array, split each pixel into three components, and add the corresponding value. It's important not to forget to clamp the result to the 0..255 range.

let deltaR = parseInt($("#rangeR").val());
let deltaG = parseInt($("#rangeG").val());
let deltaB = parseInt($("#rangeB").val());
for (let i = 0; i < dst.length; i++) {
  let r = src[i] & 0xFF;
  let g = (src[i] >> 8) & 0xFF;
  let b = (src[i] >> 16) & 0xFF;
  
  r += deltaR;
  g += deltaG;
  b += deltaB;
  if (r > 255) r = 255;
  else if (r < 0) r = 0;
  if (g > 255) g = 255;
  else if (g < 0) g = 0;
  if (b > 255) b = 255;
  else if (b < 0) b = 0;
  
  dst[i] = (src[i] & 0xFF000000) | (b << 16) | (g << 8) | r;
}

Original on the left vs R=41 G=-7 B=-18:

index.jpg rgb_41_-7_-18.jpg

Brightness#

If you adjust all three values simultaneously, you get the Brightness filter.

Original on the left vs brightness=-45:

index.jpg br_-45.jpg

CMYK Color Model#

CMYK (cyan, magenta, yellow, black) is used in printing. Printers use white paper and mix cyan, magenta, and yellow to get the required color. To conserve colored ink, a fourth component K (black intensity) is used.

Values will now be specified in percentages.

let deltaC = parseInt($("#rangeC").val()) / 100.0;
let deltaM = parseInt($("#rangeM").val()) / 100.0;
let deltaY = parseInt($("#rangeY").val()) / 100.0;
let deltaK = parseInt($("#rangeK").val()) / 100.0;
for (let i = 0; i < dst.length; i++) {
  let r = src[i] & 0xFF;
  let g = (src[i] >> 8) & 0xFF;
  let b = (src[i] >> 16) & 0xFF;
  
  // RGB to CMYK
  let k = 1 - (Math.max(r, g, b) / 255.0);
  let c = (1 - (r / 255.0) - k) / (1 - k);
  let m = (1 - (g / 255.0) - k) / (1 - k);
  let y = (1 - (b / 255.0) - k) / (1 - k);
  
  c += deltaC;
  m += deltaM;
  y += deltaY;
  k += deltaK;
  if (c > 1) c = 1;
  else if (c < 0) c = 0;
  if (m > 1) m = 1;
  else if (m < 0) m = 0;
  if (y > 1) y = 1;
  else if (y < 0) y = 0;
  if (k > 1) k = 1;
  else if (k < 0) k = 0;
  
  // CMYK to RGB
  r = Math.floor(255 * (1 - c) * (1 - k));
  g = Math.floor(255 * (1 - m) * (1 - k));
  b = Math.floor(255 * (1 - y) * (1 - k));
  
  dst[i] = (src[i] & 0xFF000000) | (b << 16) | (g << 8) | r;
}

For clarity, I suggest first setting all components to -100. This results in a white "sheet". Now, if you gradually increase the black intensity (K), a grayscale image will appear. Here is an example with C=M=Y=-100, K=0:

cmyk_-100_-100_-100_0.jpg

Gradually adding "ink" produces a color image. C=-12 M=-43 Y=K=0:

cmyk_-12_-43_0_0.jpg

C=-24 M=-18 Y=3 K=-28:

cmyk_-24_-18_3_-28.jpg

HSL Color Model#

The HSL model is based on more human-intuitive color perception and uses the following components: hue, saturation, and lightness. Indeed, it's easier to say "make the photo a bit brighter and slightly more saturated" than "increase red and green while reduce the blue a bit."

Besides HSL, there's a similar HSV (or HSB) model: hue, saturation, value (or brightness). The difference is that the maximum value of the third component in HSL gives white, while in HSB/HSV it gives a bright color.

hsvl.png

HSB/HSV can be represented as a cone. Hue is defined as an angle on the circumference, saturation as the distance from the center, and brightness as the height. The cone's apex represents black.

HSV_cone.png

function rgbToHsl(r, g, b) {
  let max = Math.max(r, g, b);
  let min = Math.min(r, g, b);
  let l = (max + min) / 2;

  if (max == min) {
    return [0, 0, l];
  }
  let h, s;
  let d = max - min;
  if (l > 0.5) {
    s = d / (2 - max - min);
  } else {
    s = d / (max + min);
  }
  switch (max) {
    case r:
      h = (g - b) / d + (g < b ? 6 : 0);
      break;
    case g:
      h = (b - r) / d + 2;
      break;
    case b:
      h = (r - g) / d + 4;
      break;
  }
  h /= 6;
  return [h, s, l];
}

function hue2rgb(p, q, t) {
  if (t < 0) t += 1;
  else if (t > 1) t -= 1;
  if (t < 1/6) return p + (q - p) * 6 * t;
  if (t < 1/2) return q;
  if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
  return p;
}

function hslToRgb(h, s, l) {
  if (s == 0) {
    return [l, l, l];
  }
  let q = l < 0.5 ? l * (1 + s) : l + s - l * s;
  let p = 2 * l - q;

  let r = hue2rgb(p, q, h + 1/3);
  let g = hue2rgb(p, q, h);
  let b = hue2rgb(p, q, h - 1/3);
  return [r, g, b];
}

Original:

city.jpg

H=0 S=67 L=-3:

city_hsl_0_67_-3.png

H=75 S=-39 L=0:

city_hsl_75_39_0.png