VOOZH about

URL: https://dev.to/bmbrick/oklab-vs-rgb-why-your-color-matching-algorithm-is-wrong-2dd0

⇱ OKLab vs RGB: Why Your Color Matching Algorithm is Wrong - DEV Community


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

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

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

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.