Amplify motion¶
Breathing, a structure settling, the small sway of something that looks still. The motion pipeline makes these visible by amplifying how much the picture's detail shifts over time.
Which filter to use¶
The motion pipelines differ only in how they pick out the frequencies to amplify, and the choice matters more than it looks.
| Filter | Choose when | Cost |
|---|---|---|
Two running averages (motion preset) |
You want a broad band and the simplest behaviour, or you work frame by frame | Cheapest; needs only the previous frame |
| Butterworth | You want a defined band with a gentle edge, still frame by frame | Cheap; needs only the previous frame |
| Fourier | You want a sharply defined band and have the whole clip | Needs every frame at once |
The motion preset uses the first — the authors' choice for the sleeping-child
clip.
The two parameters that matter¶
alpha — how much to amplify. Raise it until the movement is clear, then
stop. Past that, the picture tears into ripples and haloes: the method assumes
the movement is small enough that shifting the image is nearly the same as
scaling its detail, and that assumption fails first at edges.
lambda_c — the spatial cutoff, in pixels. Detail finer than this is
amplified less, on purpose: fine detail is where the small-movement assumption
breaks and where the sensor noise is. Raise it to reduce artefacts (and amplify
less); lower it to do the reverse.
gentle = vidmag.magnify("clip.mp4", preset="motion", alpha=5, lambda_c=32)
strong = vidmag.magnify("clip.mp4", preset="motion", alpha=25, lambda_c=8)
Shimmering ripples mean too much alpha for the lambda_c in use. Raise
lambda_c before lowering alpha.
Colour¶
chrom_attenuation scales how much the colour channels are amplified relative
to brightness. The motion preset sets it to 0.1, so colour is amplified about
a tenth as much. Colour noise is very visible and rarely carries the movement,
so keeping it low is usually right.
A clip too long to fit in memory¶
Every pipeline here takes the whole clip at once. For something long, work in overlapping chunks and discard the overlap:
import numpy as np
import vidmag
from vidmag.io.video import load_video
video, info = load_video("long.mp4")
frames = np.clip(np.rint(video * 255), 0, 255).astype(np.uint8)
chunk, overlap, pieces = 300, 30, []
for start in range(0, len(frames), chunk):
block = frames[max(0, start - overlap) : start + chunk]
out = vidmag.magnify(block, preset="motion", fps=info.fps)
pieces.append(out[overlap:] if start else out)
result = np.concatenate(pieces)
The overlap exists because the filters need history: without it each chunk restarts from nothing and the joins show.