Painter

Concepts

Palette Histogram Match

A free +0.55 on the VLM judge from a CPU postprocess that runs in under 100ms

Even with a good seed and a careful pipeline, the painted side of a tile drifts a little in colour. Not enough to look broken, but enough to read as "two photos pasted together" instead of one image. A standard image-processing trick — match the colour distribution of the output to the seed — closes most of that gap, for free, on the CPU.

Drag the slider

Slide from 0% (raw FLUX output) to 100% (full histogram match against the seed). Watch the histogram on the right migrate toward the seed's shape, and the tile preview drift back toward the seed palette.

Seed (the target)
Output (varies with slider)
RGB histograms — seed vs output
R seed (line) · output (bars)
G seed (line) · output (bars)
B seed (line) · output (bars)
100%
Palette similarity to seed
0.955 Bhattacharyya · 1.0 = identical
Stylised VLM lift
+0.55 illustrative — see iter-8 below

How the trick works

For each colour channel (R, G, B), build a cumulative distribution of pixel values in the seed and in the output. Then remap every pixel in the output to the value that has the same cumulative position in the seed. The rendering content stays put — only the tone curve shifts.

1

Build CDFs

Per channel, count pixel intensities into bins and accumulate. Two CDFs: one for the seed strip, one for the output tile.

2

Map values

For every output value v, look up its CDF position in the output, then find the seed value with the same CDF position. Replace v with that seed value.

3

Recombine

Three remapped channels stacked back into an RGB image. No model call, no GPU, no learnable parameters. skimage.match_histograms does this in one line.

Why this is different from a flat colour grade

A flat colour transform (LUT, white balance, saturation) shifts every pixel by the same rule — it can't fix mid-tone drift without crushing highlights. Histogram matching is per-percentile: pixels that were in the bottom 20% of green in the output become the bottom 20% of green in the seed, regardless of their absolute value. That preserves local contrast while rebuilding global tone.

Real numbers from iter-8

Stacked on top of the iter-5b stack with no other changes. Some biomes barely move; others — particularly tightly-toned ones like dungeon stone — shift dramatically.

Dungeon
8.2
10.0
+1.8
Forest path
5.4
6.3
+0.9
Castle wall
4.8
5.7
+0.9
River shore
5.9
6.4
+0.5
Aggregate lift, iter-7c → iter-8
+0.55 across the board. Largest single-postprocess gain of this batch, at zero GPU cost.

When it doesn't help

  • Tiles where the output and seed share the same palette already (no drift to fix).
  • Tiles where the structural error is the dominant problem — palette match makes a wrong-shape river the right colour, but it's still the wrong shape.
  • Style transfers across biome boundaries (matching a desert tile's histogram to a forest seed produces unnatural greens).
How this is built (technical)

Postprocess uses skimage.exposure.match_histograms(output, reference, channel_axis=-1) against the seed strip cropped from the anchor. Runs CPU-side after image generation, before VLM evaluation. End-to-end cost on a 1024² tile is ~80ms on a single core. Implemented in scripts/spikes/palette_match.py; results visualised per-biome in scripts/spikes/_research/palette_match_visual_analysis.md.