DEV Community

BMBrick
BMBrick

Posted on • Originally published at bmbrick.com

How I Built a Perceptual Color Quantization Engine for LEGO Mosaics

The Problem

Converting a photo into a LEGO mosaic sounds simple: resize the image, find the closest LEGO color for each pixel, done.

Except it looks terrible. Skin tones turn green. Hair becomes a muddy blob. The algorithm picks colors that are mathematically close in RGB but perceptually wrong.

The Solution: OKLab Color Space

I switched from RGB to OKLab, a perceptually uniform color space designed in 2020. In OKLab, equal numerical distances correspond to equal perceived color differences.

This alone was a massive improvement, but it wasn't enough.

Material-Aware Matching

LEGO bricks aren't paint. A matte red brick and a transparent red brick look completely different under the same light. My engine models each brick's material properties (matte, transparent, metallic, glitter) and weights the color distance accordingly.

// Simplified: material-aware distance
function colorDistance(pixel, brick) {
  const labDist = oklabDistance(pixel, brick.color);
  const materialPenalty = pixel.material !== brick.material ? 0.15 : 0;
  return labDist + materialPenalty;
}
Enter fullscreen mode Exit fullscreen mode

Spatial Stabilization

Quantized images have a speckle problem: isolated pixels of wrong colors create noise. I added a spatial stabilization pass that:

  1. Splits the image into 4x4 blocks
  2. Runs a coarse quantization per block with a "block anchor" weight
  3. Applies a despeckle pass that replaces isolated single-pixel colors

The Fidelity Pipeline

The full pipeline:

  1. Resize to target grid (48x48 or 64x64 studs)
  2. Convert to OKLab
  3. Quantize with material-aware matching
  4. Spatial stabilization (block anchor + despeckle)
  5. Edge preservation check
  6. Render 4-layer Canvas preview

Results

The tool is live at www.bmbrick.com. Free to try, free (during launch period) for the full parts list.

Faces look like faces. Fur looks like fur. It's not perfect, but it's a massive improvement over naive RGB matching.


Built with vanilla JS, Canvas API, and Web Workers. No framework, no bundler.

Top comments (0)