R&D LAB Computational Design Shipped Image instrument

Tessera-Mosaic

Image in, paintable sheet out

Any image is broken into flat colour regions, every region gets a number, and out comes a printable vector sheet with a legend, without the image ever leaving the tab.

  • 96colours at most, free from 12 up
  • 12stages in the chain
  • 0,8 slongest compute time on this page
  • 0network requests
A painting sheet with numbered regions and a legend of swatches, numbers, names and hex values

The printable sheet, here on the procedurally drawn sample the application starts with when no file is given: 46 regions, 25 colours. Hairline outlines, a numeral in every region, and under it the legend with swatch, number, nearest colour name, hex value and share of the surface.

Why this is published

The step from an image to something a person actually holds is not a filter, it is a chain of decisions under constraints: segment, merge, simplify, label. Tessera-Mosaic is our evidence that we build such a chain and check it against its own constraints, rather than showing a picture at the end and hoping.

A paint-by-numbers sheet looks like a filter and is not one. A number in a region assumes the region is big enough to paint, not so thin that a brush will not fit, that it has a spot where the numeral fits whole, and that the colour beside it is distinguishable from its neighbours. Tessera-Mosaic is the chain of steps that satisfies those four conditions at once, and three views in which the result can be judged before any paint is bought.

A photograph has no regions

It has gradients, noise and edges that begin nowhere in particular. A painting sheet needs the opposite: closed regions, each big enough for a brush, each in a colour you can buy and find again. Between the two there is no setting, there is a chain.

Four conditions have to hold at once or the sheet is useless. A region needs enough area. It must not be so narrow that no brush fits. It needs a spot where the numeral stands whole. And its colour has to stay distinguishable from its neighbours. Each condition on its own is easy, together they are the problem.

The sheet beside this is computed from a public-domain landscape photograph, 914 x 1280 pixels, dropped to 714 x 1000 working pixels. Out came 239 regions in 24 colours, 10.9 m of outline, 0.8 s of compute. The footer names every one of those numbers before anybody prints.

A painting sheet computed from a landscape photograph, with 239 regions and 24 colours
239 regions, 24 colours, closest pair 5.9 dE apart. The legend under it carries every colour with number, name and hex value.

The whole chain runs in a worker thread

Twelve stages, read as six steps

  1. 01 Into a colour space where distance means something sRGB goes to CIELAB. Only there does a small distance between two colours also mean that an eye struggles to tell them apart, and only there is the later palette warning a real number rather than a feeling.
  2. 02 Noise out, edges kept A separable bilateral filter with radius 0 to 4 smooths surfaces and leaves edges standing, so that the next step does not promote grain to a colour of its own.
  3. 03 Compute the palette Weighted k-means in Lab, k-means++ seeding, 32 Lloyd iterations, and two seeds anchored at the 1st and 99th lightness percentile so that highlight and shadow are not averaged away. Assignment is exact and undithered: a dither here would not be an effect, it would be an unpaintable region.
  4. 04 Pixels become regions A three by three majority filter votes speckle away, then 4-connected components are labelled. From here on the chain no longer works with colours, it works with areas.
  5. 05 Merge what is too small or too thin Union-find to fixpoint, with the distance transform re-run between rounds. Too thin is the condition most tools miss: a long narrow band passes every area test and still takes no brush.
  6. 06 Border, outline, numeral Borders are built as crack edges and chains, each chain smoothed once (Douglas-Peucker, Chaikin, Douglas-Peucker), the outlines assembled, the colours renumbered by painted area, and only then are the numerals placed.

The pipeline builds a mosaic

Screen recording, about fifteen seconds: from silhouette to finished mosaic of a public-domain painting, tile and palette counters running. No sound.

The one decision

A floating point number decided whether an image was paintable

Three of the five palette tiers did not deliver what their labels said, for a long time. Rich 40, Deep 56 and Full 72 returned a handful of regions and a fraction of the colours asked for, and the more colours were asked for the worse it got. The same sheet that gave 24 colours and 574 regions at Medium came back at Deep with 8 colours and 9 regions.

The cause was one line in the merge. The array holding each region's thickness is a Float32Array, the minimum radius it is tested against is a double. When a region absorbed a neighbour, the thickness of the union was unknown, so the code wrote the minimum radius into it as a claim: this region counts as paintable now. A double stored in a Float32Array is rounded, and about half of all values round down.

When it rounded down, the union was still undersized by its own test. So it absorbed again, and again, until the picture was a few dozen blobs. Which way it rounded depends on the exact value of the minimum radius, and that value is derived from palette size and paper format, so it differs for every combination. The Medium value rounded up and worked. Rich, Deep and Full shared a value that rounded down.

The evidence was uncomfortably clear: a four millionths change in the minimum radius flipped the same sheet between 574 and 79 regions, and comparing the value against its own Float32 rounding predicted which branch you got on twelve of twelve samples.

The decision was then not to repair the comparison but to remove it. The claim now sits beside the region as a flag of its own, the thickness array keeps the honest maximum, and a region that has already merged skips the thinness test. No floating point number decides it any more. Since then every tier delivers its exact palette with no retry at all: Rich 40 with 633 regions, Deep 56 with 618, Full 72 with 874. A sheet from that image now takes 1.0 to 2.2 s instead of 0.6 to 1.0 s, because far more regions survive.

A number meant as a claim was being read back as a measurement.

What 24 colours make of a photograph

The posterised version of the same landscape photograph, built from flat colour areas
The same decomposition shown as a picture instead of a sheet. Clicking a legend row isolates that colour across the whole picture.

This view exists for one question: is the palette any good, before it is bought. It is the same 239 regions and 24 colours as the sheet further up, only without numerals and without hairlines.

Every palette entry knows its distance to its nearest neighbour. That single number drives the readout under the slider, the shortfall retry, and the refusal when an image cannot carry the colours asked of it. Here 5.9 dE separate the closest pair, and the same photograph also carries all 72 colours, then with 3.7 dE. A flat vector graphic, by contrast, saturates around 50 to 60 colours and says so.

What goes in, what comes out

The photograph

  • 914 x 1280 pixels, dropped to 714 x 1000 for the computation.
  • Millions of colour values, none of them named.
  • Gradients without a border: where the foliage ends is a question of threshold.
  • No smallest feature. A branch may be one pixel wide.
  • Nothing in it says how long it would take to paint.

The sheet

  • 239 closed regions, exactly one numeral in each.
  • 24 colours, each with swatch, nearest name, hex value and share of the surface.
  • Every region at least 2.4 mm across on A4, or it has been merged away.
  • 10.9 m of outline, 0.8 s of compute, an estimated 32 minutes by hand.
  • One A4 page as vector, with not a single external reference.

The same frame, two palettes

Fewer colours does not mean fewer regions

The same sheet with only fifteen colours and 273 regions

15 colours273 regions, closest pair 7.9 dE, an estimated 36 minutes by hand. That is more regions than at 24 colours, not fewer: a coarse palette frays the border between foliage and sky instead of simplifying it.

The other two views

Filling colour by colour, one of five orderings: colour by colour, background first, dark to light, by number, or scattered. The brush sweep grows outward from the numeral. Scrubbing backwards repaints pixel-identically to playing forwards, and that is not a nicety: a timeline that shows different pictures forwards and backwards is an animation pretending to be a state.
The preview showing the posterised sample image, a landscape of flat colour areas
The preview on the procedural sample the application starts with when no file is given: 42 regions, 24 colours, closest pair 6.0 dE.
The same sheet half painted, with the timeline visible
The same sheet at 9.5 of 45.0 seconds, currently on number 5. At the end no white is left, and that is measured rather than asserted.

Where this fits

  • Anything that has to translate an image into something manufacturable: cutting, applique, inlay, tile, vinyl.
  • Products where a customer submits their own photograph and expects a physical result that is actually feasible.
  • Any problem whose constraint is a minimum size and a minimum width, not only a number of colours.
  • Tools that may upload nothing for data protection reasons and still have to run in a browser.

Image credits: every capture shows our own application. Five of them show a result computed from a public-domain photograph: Limestone quarry landscape by Rosendahl, via Wikimedia Commons. The rest use the application's own procedurally drawn sample image.

Why this is published

What this means for your project

More from the lab

Need to turn an image into something manufacturable, under real constraints? That is our kind of problem.

Get in touch

Grace Hopper

“The most damaging phrase in the language is: it's always been done that way.”