07 · Color

Undo the fade, grade the look

A muddy orange memory becomes a film with real color, measured once per shot, applied identically to every frame (per-frame color would flicker).

Fixes: casts, fading, flat grade Technique: histogram stretch + white balance + LUT color.py

Layer 1: cast removal, one channel at a time

Age fades the three color channels by different amounts, so the whole image drifts toward one muddy tint. The fix: build a separate histogram for Red, Green and Blue, find where each channel's real range sits, and stretch each independently to fill 0-255. A uniform cast gets pulled apart and neutralized, and the folded math becomes three 256-entry lookup tables, so the heavy pass is pure array indexing, zero per-pixel math.

Each channel occupies only part of the range when faded (that's the cast). Stretching each to full 0-255 neutralizes it. The gray card at right shows the effect.

Layer 2: white balance, and where it fails

Gray-world white balance assumes that, averaged over a scene, the world is neutral gray, so it scales the channels until their averages match. Usually fine. It fails badly when one strong color fills the frame. In Yossi's reel: a boy in a bright red shirt.

Gray-world sees all that red, decides the average is "too warm," and shoves the entire image toward the opposite color, green, until the average goes neutral. The shirt calms down… and the boy's skin turns a sickly teal.

Watch the skin and the white collar as you switch modes. Gray-world greens the skin to average out the red shirt; near-neutral reads only the genuinely neutral pixels (the collar) and leaves the shirt, and the skin, alone.

The fix is a smarter measurement: estimate the balance from the near-neutral pixels only, the genuinely low-saturation parts (skin highlights, teeth, a white collar), which is where a real cast actually shows, and leave saturated content alone. On the red shot, the numbers matched the eyes: gray-world crushed the red-over-blue balance from +43 to +15 (neutralizing a genuinely red scene); near-neutral held it at +43, faithful to the film.

if neutral_wb:                       # opt-in
    mask  = saturation(pixels) < 0.15   # only near-grays vote
    gains = graymean / channel_mean(pixels[mask])
else:
    gains = graymean / channel_mean(all_pixels)   # classic gray-world

Layers 3 and 4: blue-shadow fix, film LUT, and vibrance

Old film grows a blue cast in the shadows specifically, so we pull blue down but only in the dark areas (the effect ramps from full at black to zero by mid-gray). Finally the grade: blend a warm film look-up table and a modest saturation boost.

Fixing one stage let us relax another. Once near-neutral balance preserved each shot's real warmth, the warm LUT, which had been quietly compensating for gray-world's cooling, was suddenly too much. So we dialed it from 0.4 down to 0.22. The grade got more honest.
Yossi's dials: clip_percent: 0.5, white_balance: true, neutral_wb: true, blue_shadow: 0.15, lut: film_warm, lut_blend: 0.22, saturation: 1.2. (No contrast here, the Light stage owns tone, so the two never fight.)