Optical Flow

Optical flow estimates the apparent 2D motion field between nearby frames. It is lower-level than temporal action recognition: instead of naming the action, it asks how local image intensity is moving. That motion field can be fed to two-stream models, used as a cue in person tracking and track aggregation, or treated as one component of broader spatial and temporal modelling.

Brightness constancy

Brightness constancy assumes a point keeps the same intensity as it moves:

First-order expansion gives the optical-flow constraint

One pixel gives one equation for two unknowns, so Lucas-Kanade solves a local least-squares problem over a window:

Horn-Schunck instead adds a global smoothness penalty so neighboring flow vectors prefer to vary slowly. The shared intuition is simple: image gradients say which displacement would explain the observed temporal change, while extra spatial assumptions resolve the aperture ambiguity.

Worked example

This snippet creates two shifted frames and estimates a constant optical-flow vector by solving the brightness-constancy equations on a local mask.

import warnings
import numpy as np
from scipy.ndimage import shift
 
warnings.filterwarnings("ignore", category=RuntimeWarning)
np.random.seed(9)
h, w = 40, 40
y, x = np.mgrid[0:h, 0:w]
frame1 = np.exp(-((x-19)**2 + (y-21)**2) / 70.0) + 0.35*np.sin(x/3.5) + 0.2*np.cos(y/4.0)
true_u, true_v = 0.70, -0.40
frame2 = shift(frame1, shift=(true_v, true_u), order=1, mode="nearest")
Ix = (np.roll(frame1, -1, axis=1) - np.roll(frame1, 1, axis=1)) / 2
Iy = (np.roll(frame1, -1, axis=0) - np.roll(frame1, 1, axis=0)) / 2
It = frame2 - frame1
mask = np.zeros_like(frame1, dtype=bool)
mask[6:-6, 6:-6] = True
A = np.column_stack([Ix[mask], Iy[mask]])
b = -It[mask]
flow, *_ = np.linalg.lstsq(A, b, rcond=None)
res = A @ flow - b
print("true_flow_u_v", np.round([true_u, true_v], 3).tolist())
print("estimated_u_v", np.round(flow, 3).tolist())
print("mean_abs_constraint_residual", round(float(np.mean(np.abs(res))), 5))

Observed output:

true_flow_u_v [0.7, -0.4]
estimated_u_v [0.681, -0.402]
mean_abs_constraint_residual 0.00548

The local linearization recovers the synthetic subpixel shift closely. It works here because the texture has gradients in multiple directions; a flat patch or a single straight edge would make the least-squares system poorly conditioned.

The visual picture is a field of small displacement arrows. A uniform right-and-up shift has similar arrows across textured regions, but reliable estimation still depends on local gradients:

Optical flow estimates a displacement field between adjacent video frames; textured regions support stable arrows while flat regions are ambiguous.

Caveats

Brightness constancy breaks under lighting changes, specularities, motion blur, and occlusion. Large displacements need pyramids or learned matching because the first-order approximation is local. Flow is also apparent image motion, not necessarily physical 3D motion: camera movement, parallax, and rolling shutter can dominate the field.

References