02 · Photometric

Stop the breathing

8mm exposure pulses several times a second, the whole image subtly brightens and darkens. We pull each frame back onto a smooth curve.

Fixes: exposure flicker Technique: two-pass luminance normalization photometric.py

The film never sat identically in the projector gate, and old dyes respond unevenly to the lamp, so brightness flickers frame to frame. The cure sounds paradoxical: we don't force every frame to the same brightness (that would flatten a real sunset), we pull each frame toward a temporally smoothed target. Slow, intended lighting changes survive; the fast jitter gets averaged away.

raw brightness (flickering) smoothed target (rolling mean) corrected output
Left tiles are a stand-in "frame": as-scanned pulses; corrected holds steady. Widen the window and the target gets smoother; drop it to 1 and there's nothing to smooth against.

The technique: measure once, apply once

It's a clean example of two-pass streaming. First pass: stream the shot and record just one number per frame, its perceptual brightness (green weighed far above blue, per BT.601):

lum = [float(frame.mean(axis=(0,1)) @ _BGR_LUMA) for frame in read_frames(src)]
target = rolling_mean(lum, window=31)          # the smooth curve
gain   = clip(target / lum, 0.5, 2.0)          # per-frame nudge, safety-clamped

Second pass: multiply every pixel of frame i by gain[i]. The same gain hits all three channels, so color is untouched, we only move brightness. The 0.5×-2.0× clamp stops a single freak dark or blown frame from exploding.

The upgrade: flicker is often local

A single gain for the whole frame misses the classic 8mm ailment, a projector lamp hot spot, or one edge of the film fading differently, so the left side breathes while the right sits still. The v2 pipeline measures a 4×4 grid of gains, smooths each independently, and blends them back into a smooth full-frame map.

A drifting lamp hot-spot makes tiles pulse out of sync. One global gain can't calm them; the 4×4 grid evens each tile, while a ±15% clamp stops it inventing a vignette.
The trap we walked up to: if the tiles drift too far apart, you trade flicker for a breathing vignette, a slow brightness gradient sloshing around the frame, subtler but more annoying. So each tile's gain is clamped to within ±15% of the frame's global gain. Calm the local flicker; never invent a spotlight. Measured result: local flicker −24%, no vignette.

Yossi's dials: window: 31 (~1.7 s, wide enough to treat pulsing as noise, narrow enough to follow a real lighting change), tiles: 4, tile_spread: 0.15.