8mm exposure pulses several times a second, the whole image subtly brightens and darkens. We pull each frame back onto a smooth curve.
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.
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.
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.
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.