Building blocks¶
The primitives the pipelines are made of. Each has the same name and argument order on every backend, so a chain written against one runs on another.
See use the building blocks for worked examples.
On the processor¶
vidmag.cpu.ops
¶
The same building blocks as :mod:vidmag.cuda.ops, computed with NumPy.
This module adds no arithmetic of its own. It is a naming layer: the functions
here forward to the implementations in :mod:vidmag.cpu.pyramids and
:mod:vidmag.cpu.filters, under the names and argument order the GPU operations
use. Two reasons that is worth having.
A caller can write one chain of operations and choose the backend separately,
because vidmag.cpu.ops.blur_dn and vidmag.cuda.ops.blur_dn take the same
arguments in the same order.
And the conformance tests can run the same list of operations against every backend, comparing each against this one. That makes this module the reference the GPU is checked against, which is the role the whole project already gives the NumPy code.
level_sizes(height, width, levels)
¶
The (height, width) of each pyramid level, finest first.
Source code in src/vidmag/cpu/ops.py
bgr_u8_to_ntsc(frames)
¶
Convert (T, H, W, 3) 8-bit blue-green-red frames to NTSC, float32.
The per-frame helper takes 8-bit input and does the channel reversal and the divide by 255 itself, so frames are handed to it unchanged.
Source code in src/vidmag/cpu/ops.py
ntsc_to_bgr_u8(frames)
¶
blur_dn(frames, levels)
¶
build_lpyr(frames, levels)
¶
Build a Laplacian pyramid: one band per scale, finest first.
Returned bands are shaped (3, T, height, width) to match the GPU layout, which keeps each colour channel's planes together.
Source code in src/vidmag/cpu/ops.py
recon_lpyr(bands, height, width)
¶
Sum a Laplacian pyramid back, returning one plane per channel.
Bands arrive shaped (channel, time, height, width), matching the GPU
layout. The per-frame helper wants the opposite: a list of levels each
shaped (height, width, channel), plus the table of level dimensions it
calls pind. Both are rebuilt here.
Source code in src/vidmag/cpu/ops.py
ideal_bandpass(frames, fl, fh, sampling_rate)
¶
Keep only frequencies strictly between fl and fh, along time.
Source code in src/vidmag/cpu/ops.py
butter_bandpass(frames, fl, fh, sampling_rate, order=1)
¶
First-order Butterworth bandpass, along time.
Source code in src/vidmag/cpu/ops.py
iir_bandpass(frames, r1, r2)
¶
The difference of two exponential moving averages, along time.
apply_gain(frames, gain_y, gain_i, gain_q)
¶
Scale the three NTSC channels independently.
The pipelines, derived from the primitives¶
vidmag.backend.generic
¶
The four magnification pipelines, written once against :class:Ops.
A backend only has to supply the primitive operations in
:mod:vidmag.backend.ops — colour conversion, blur and downsample, pyramid build
and reconstruct, three temporal filters, gain, upsample, quantize. The four
pipelines then come from here for free, which is what makes adding hardware
support a bounded job rather than a rewrite.
A backend may still replace any of these with its own version, and the two that matter for speed do: the hand-written CUDA code fuses stages and collapses kernel launches, which nothing expressed as a sequence of separate operations can match. The functions here are the correct-by-construction fallback, and the thing every new backend is measured against before it earns an override.
The arithmetic is the same as :mod:vidmag.cpu.magnify; only the spelling
differs, because here every step goes through the protocol rather than calling
NumPy directly.
color_gdown_ideal_core(ops, frames_bgr_u8, fps, *, alpha, level, fl, fh, chrom_attenuation=1.0, sampling_rate=None)
¶
Amplify colour change: blur down, bandpass over time, scale, add back.
This is the pipeline that makes a pulse visible. Reducing the resolution first is what suppresses noise: a heartbeat changes a whole region of skin together, so averaging over a region keeps the signal and discards most of what is random.
Source code in src/vidmag/backend/generic.py
motion_lpyr_ideal_core(ops, frames_bgr_u8, fps, *, alpha, lambda_c, fl, fh, chrom_attenuation=0.0, sampling_rate=None, exaggeration_factor=EXAGGERATION_FACTOR)
¶
Amplify motion, selecting the frequency band with a Fourier transform.
Needs the whole clip, because the transform runs over all of time at once.
Source code in src/vidmag/backend/generic.py
motion_lpyr_butter_core(ops, frames_bgr_u8, fps, *, alpha, lambda_c, fl, fh, chrom_attenuation=0.0, sampling_rate=None, order=1, exaggeration_factor=EXAGGERATION_FACTOR)
¶
Amplify motion, selecting the band with a Butterworth filter.
Runs forward in time only, so it also works on frames as they arrive.
Source code in src/vidmag/backend/generic.py
motion_lpyr_iir_core(ops, frames_bgr_u8, fps, *, alpha, lambda_c, r1, r2, chrom_attenuation=0.1, exaggeration_factor=EXAGGERATION_FACTOR)
¶
Amplify motion, selecting the band by subtracting two running averages.
The cheapest of the three and the only one that needs no history beyond the previous frame, which is what makes it the one a live stream can use.
Source code in src/vidmag/backend/generic.py
bind(ops)
¶
Give a set of primitive operations all four pipelines.
This is what makes supporting new hardware a bounded job: implement the
operations in :mod:vidmag.backend.ops, call this, and the backend is
complete.
Source code in src/vidmag/backend/generic.py
Video reading and writing¶
vidmag.io.video
¶
Video I/O helpers.
Loads a video into a single float32 array of shape (T, H, W, C) with values
in [0, 1] and writes one back out. Keeping the whole clip in memory is fine
for the baseline (the EVM temporal filters need random access to all frames
anyway) and makes the algorithm easy to read; the CUDA port will stream frames
through device memory instead.
VideoInfo
dataclass
¶
load_video(path)
¶
Load a video as a float32 array of shape (T, H, W, C) in [0, 1].
Color videos come back as 3-channel BGR (OpenCV's native order, so we can
hand frames straight back to save_video without permuting channels).
Grayscale videos come back as (T, H, W, 1) so downstream code can assume
a trailing channel axis unconditionally.
Source code in src/vidmag/io/video.py
save_video(frames, path, fps, *, codec='libx264')
¶
Write a float32 array in [0, 1] back to an H.264 MP4.
frames is (T, H, W, C) with C in {1, 3}. Single-channel arrays are
broadcast to 3 channels for the encoder. Values are clipped to the valid
range and converted to uint8 (BGR), then handed to :func:encode_video.
Source code in src/vidmag/io/video.py
rgb_to_yiq(rgb)
¶
Convert RGB float in [0, 1] to YIQ, matching MATLAB rgb2ntsc.
Uses the exact transform matrix documented for MATLAB's rgb2ntsc so the
luminance/chrominance split is identical to the MIT reference. The row order
is (Y, I, Q). Input shape (..., 3).
Source code in src/vidmag/io/video.py
yiq_to_rgb(yiq)
¶
Inverse of :func:rgb_to_yiq, matching MATLAB ntsc2rgb.