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.
Top comments (0)