DEV Community

BMBrick
BMBrick

Posted on

OKLab vs RGB: Why Your Color Matching Algorithm is Wrong

The Problem with RGB

If you've ever tried to find the "closest" color from a limited palette, you probably used Euclidean distance in RGB space:

function rgbDistance(c1, c2) {
  return Math.sqrt(
    (c1.r - c2.r) ** 2 +
    (c1.g - c2.g) ** 2 +
    (c1.b - c2.b) ** 2
  );
}
Enter fullscreen mode Exit fullscreen mode

This looks reasonable. It's fast, it's simple, and it's completely wrong.

Why RGB Distance Lies

RGB is not a perceptually uniform color space. A distance of 50 units between two reds looks very different from a distance of 50 units between two blues. The human eye is much more sensitive to green variations than blue variations, but RGB treats them equally.

The result: algorithms that pick "mathematically closest" colors that look obviously wrong to humans. Green skin tones. Muddy hair. Flat shadows.

Enter OKLab

OKLab was created by Bjรถrn Ottosson in 2020. It's a perceptually uniform color space designed specifically so that equal numerical distances correspond to equal perceived color differences.

// Convert sRGB to OKLab
function srgbToOklab(r, g, b) {
  // Linearize sRGB
  let lr = r <= 0.04045 ? r / 12.92 : Math.pow((r + 0.055) / 1.055, 2.4);
  let lg = g <= 0.04045 ? g / 12.92 : Math.pow((g + 0.055) / 1.055, 2.4);
  let lb = b <= 0.04045 ? b / 12.92 : Math.pow((b + 0.055) / 1.055, 2.4);

  // Linear sRGB to LMS
  let l = 0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb;
  let m = 0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb;
  let s = 0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb;

  // Cube root
  let l_ = Math.cbrt(l);
  let m_ = Math.cbrt(m);
  let s_ = Math.cbrt(s);

  return [
    0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
    1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
    0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_
  ];
}
Enter fullscreen mode Exit fullscreen mode

The Results

I tested both approaches on a set of portrait photos converted to LEGO mosaics (50-color palette):

Metric RGB OKLab
Skin tone accuracy Poor (green shift) Good
Hair detail preservation Low High
Subject identity Lost in small sizes Preserved
Perceptual error (CIEDE2000) 12.3 avg 4.1 avg

The difference is dramatic. Faces that look like green blobs in RGB look like actual people in OKLab.

Beyond OKLab: Material Awareness

OKLab alone isn't enough for LEGO mosaics. LEGO bricks aren't paint โ€” they have material properties:

  • Matte bricks absorb light uniformly
  • Transparent bricks transmit light, appearing brighter
  • Metallic bricks have specular highlights
  • Glitter bricks sparkle unpredictably

A matte red brick and a transparent red brick look completely different under the same lighting, even if their "color" is similar.

My engine models this by adding a material penalty term:

function matchColor(pixel, palette) {
  let best = Infinity;
  let bestBrick = null;

  for (const brick of palette) {
    const labDist = oklabDistance(pixel.lab, brick.lab);
    const materialPenalty = pixel.material !== brick.material ? 0.15 : 0;
    const total = labDist + materialPenalty;

    if (total < best) {
      best = total;
      bestBrick = brick;
    }
  }

  return bestBrick;
}
Enter fullscreen mode Exit fullscreen mode

Try It Yourself

The tool is live at bmbrick.com. Upload any photo and see the difference OKLab makes.

Currently free during our launch period.


This is part 2 of a series on color quantization for LEGO mosaics. Part 1 covers the full pipeline architecture.

Top comments (0)