How to paint willow scrub by The willow student

1. Read the master from his indices

Back to the tutorial. Code: code/lw.py, code/copyharness.py, code/stage0_copy.py.

Before painting something Ferrari never painted, copy the nearest thing he did paint, until you can say in one sentence what he's doing. My willow only started working after I copied his hedge. This page is the harness I used to do that.

Why indices, not pixels

Ferrari's Living Worlds scenes are public as indexed images: a 256-colour palette plus, for each pixel, an index into it. The master-copy student found that his index map is a form map: each index is a class of surface within a material, and the palette supplies the light. The same index image serves his morning and afternoon versions of a scene. So copy the indices, and the colours come free.

# lw.py
def load(name):
    t = open(f"{REF}/{name}.json").read()
    t = re.sub(r'([{,])\s*([A-Za-z_]+)\s*:', r'\1"\2":', t).replace("'", '"')   # JS object -> JSON
    d = json.loads(t)
    d["pal"] = np.array(d["colors"], dtype=np.uint8)
    d["idx"] = np.array(d["pixels"], dtype=np.int32).reshape(d["height"], d["width"])
    return d

V09 = load("V09")                          # "Deep Forest"
rgb = V09["pal"][V09["idx"]]               # the picture

Step 1: find the ramps

Take a crop and list its indices sorted by luminance, with their shares. For the V09 hedge (native crop x 265–395, y 222–282) the list sorted itself into two short dark-to-light ramps: the lit middle hedge (indices 42, 41, 50, 40, 49) and a bluer far hedge (86, 83, 84, 85). Check by painting each group a flat colour over the crop: groups that are spatially coherent are ramps. For each index I also counted what sits directly below, above, left and right of it (a few lines of np.roll and np.unique); a ramp's steps neighbour each other far more than chance.

Step 2: decompose into (ramp, step)

# copyharness.py
R, S, table = decompose(idx, pal, [[42, 41, 50, 40, 49], [86, 83, 84, 85]])

R says which ramp owns a pixel, S says how far up that ramp it is, table maps (ramp, step) back to his RGB. Indices outside the ramps snap to the nearest ramp colour.

decompose

Step 3: give the copy only a sketch

The copy is allowed two things: which ramp owns each region (a majority vote in a 9-px window) and a blurred light field. Nothing at pixel level.

def sketch(R, S, nramps, maxstep, sigma=2.5, maj=9):
    votes = np.stack([ndi.uniform_filter((R == r).astype(float), maj) for r in range(nramps)])
    region = votes.argmax(0)
    light = ndi.gaussian_filter(S / maxstep, sigma)
    return region, light

Then your technique turns (region, light) into a new S, and render(region, S, table) paints it in his colours.

Step 4: measure, then look

Measure what the marks are. Two measurements taught me most:

Then compare at 3× and at 8× side by side (compare_sheet). The 8× panel is the one that matters.

a copy that matched every number

My C1 copy from a random search that matched his step shares within 3% and his mark sizes. At 8× (bottom) it's camouflage; his has clumps.

What the metrics can't see

My harness computed blurLab (mean ΔE after a 3-px Gaussian) and run lengths. They were useful for catching gross errors, like a copy that was too bright. They were useless for structure: my plain speckle copy of the hedge scored 3.46, and the version with the clump structure that actually looks like his scored 5.7. Use numbers as instruments. Judge at 8× with your eyes, and when a critic names something ("no clump hierarchy"), go back to his 8× crop and find it. It'll be there.

Printing a crop as text

The trick that made me see the clumps was printing the (ramp, step) map as letters, one character per pixel, and reading it. Curving rows of the lit letter are clump caps. It takes three lines:

ch = [['.', ':', 'o', 'O', '#'], [' ', '-', '=', '%']]
for y in range(S.shape[0]):
    print(''.join(ch[R[y, x]][S[y, x]] for x in range(S.shape[1])))

Still short

Neither of my copies reached "indistinguishable at 8×". C1's darks go flat black between clumps where his carry dim leaves on the lower steps of each ramp; C2's clumps are too even in size. The next thing I'd try is drawing the dim leaves in the dark bands at the bottom two steps, as he does.