// 03 RESTRICTED
mosaic
A photo-mosaic builder in Rust. Every source photo is decoded, cropped and downscaled once into a flat tile cache, so a render never decodes an image: it slices tiles straight out of the file. Tiles match on 4×4 grids of CIELAB means, and the nearest-neighbour search is pruned by an exact lightness bound rather than brute-forced — 120,000 cells in 0.09 s on 16 cores. It caps and spaces repeats, says which colours a library lacks before you render, and fetches openly licensed photos from Openverse, three museums and NASA to fill the gaps. A WebAssembly build renders in the browser. Each plate here is the same 462 public-domain nature photographs — canyons, sunsets, meadows, night skies — rebuilding a painting from the Art Institute of Chicago. Select one for full resolution.
01 Van Gogh, Self-Portrait
02 Gifford, Mist Rising at Sunset
03 Monet, Beach at Sainte-Adresse
04 Hokusai, Ejiri
Why it is fast
Nothing is decoded at render time. Indexing decodes, EXIF-rotates, centre-crops and Lanczos-downscales every source photo once, then writes the finished tiles to a single flat file beside their feature vectors. A render reads that file in one pass and slices tiles straight out of the buffer — no decode, no per-tile allocation, no resample. Different targets, different grids, different settings: the originals are never touched again.
The search is pruned, not brute-forced
Tiles are sorted by mean lightness. Because the sum of squared per-cell differences is at least n times the squared difference of the means, a tile's mean lightness alone is an exact lower bound on its full distance. The search walks outward from the target's lightness and stops the moment that bound passes the current K-th best — exactly as correct as scanning every tile, and on a large library it touches a small fraction of them. The test suite asserts the two agree.
Measured
- 120,000 cells: 0.09 s on sixteen cores, 0.96 s on one.
- 607,500 cells: 0.45 s on sixteen cores, 4.68 s on one.
- Indexing 2,900 photos: 0.06 s. Re-indexing them unchanged: 0.02 s.
Match and stitch, excluding PNG encoding, against a 2,900-tile library on a sixteen-core M-series laptop.
Matching
Each tile reduces to a grid of CIELAB cell means — 4×4 by default, so a 48-dimensional vector. A grid of one is plain average-colour matching; higher grids keep internal structure, so a tile with a bright corner lands where the target has a bright corner. Distance is CIE76 per cell with separate weights for lightness and chroma, and both sides of the comparison go through the same box average, so the metric is consistent by construction rather than by luck.
Not repeating itself
Left alone, the matcher will use the same twenty tiles for the whole picture, so a cap on uses and a minimum distance between repeats rein it in. Cells are served in descending regret — the gap between their best and second-best option — so the cells that would suffer most from being denied their first choice are served first. Constraints that turn out to be unsatisfiable are reported, not quietly violated.