Search…

Video Understanding: Flow Networks, Interpolation, Stabilisation

In this series (30 parts)
  1. Computer Vision Roadmap: From Basics to Real Projects
  2. What is Computer Vision? From Pixels to Decisions
  3. A Short History of Computer Vision: 1545 to Now
  4. Math for CV Beginners: Vectors, Matrices, Convolutions
  5. Image Fundamentals: Color, Histograms, Noise, Filtering
  6. How Images and Video Are Stored: Colour, JPEG, Frames
  7. OpenCV Setup + First 10 Tasks in Python
  8. Vision Metrics: Accuracy, Precision, Recall, mAP, IoU
  9. CV Project Workflow: Dataset, Baseline, Error Iteration
  10. Intensity Transforms and Frequency-Domain Filtering
  11. Edge Detection and Thresholding That Actually Work
  12. Morphology, Contours, and Shape Analysis for Real Images
  13. Feature Matching (SIFT/ORB) and Image Stitching
  14. Camera Calibration and Perspective Correction
  15. Epipolar Geometry, Stereo Vision, and Depth Estimation
  16. Optical Flow and Motion Tracking in Video
  17. HOG, HOF and MBH: Descriptors Before Deep Learning
  18. Your First Image Classifier (PyTorch + Transfer Learning)
  19. Data Pipelines and Augmentation for Vision Models
  20. CNN Architectures Explained: From LeNet to ResNet
  21. YOLO Detection Pipeline: Data to Inference
  22. Semantic and Instance Segmentation: U-Net to Mask R-CNN
  23. Vision Transformers (ViT) and When to Use Them
  24. CV Explainability: Grad-CAM, Failures, Bias Checks
  25. 3D Vision Basics: SfM, Point Clouds, and Pose Estimation
  26. Multimodal Vision: CLIP, Embeddings, and Retrieval Systems
  27. Video Understanding: Flow Networks, Interpolation, Stabilisation
  28. Real-Time CV Systems: Tracking, Latency, Streaming
  29. Deploying CV Models: ONNX, TensorRT, Edge, and APIs
  30. Responsible CV: Privacy, Fairness, Security, Governance

Run your image detector on every frame of a video and you have a working system that is worse than it needs to be. It is slow, because you are paying full inference cost 30 times a second on frames that are 98% identical. And it is unstable, because a box that appears in frames 1 and 3 but not in frame 2 produces a flicker no amount of confidence tuning fixes.

Video is not a stack of independent images. This post is about the things you can only do once you accept that.

Prerequisites: optical flow for the classical methods, and YOLO for per-frame detection.

The shared premise

Every technique here rests on one observation, so it is worth stating precisely.

At 30 frames per second, consecutive frames are separated by 33 milliseconds. In that time a person walking at 1.4 m/s moves about 4.6 cm. On a camera resolving 1 cm per pixel at that distance, that is roughly 5 pixels.

Everything else in the frame is unchanged.

Two consequences follow. Redundancy is an opportunity — you can reuse computation. And change is information — the difference between frames tells you things a single frame never could, including depth from parallax, object boundaries from differential motion, and of course what is actually happening.

Learned optical flow

Classical flow methods — Lucas–Kanade and Farnebäck — solve an optimisation per frame pair using hand-designed assumptions: brightness stays constant, motion is locally smooth. They break precisely where those assumptions do, which is on large displacements, at occlusions, and on textureless regions.

FlowNet in 2015 asked whether a network could just learn the mapping. Two architectures came out of it, and the contrast between them is genuinely instructive.

FlowNetSimple

Stack the two frames into a 6-channel input and run a standard encoder–decoder.

This works. It is also asking a lot. The network must learn, from data alone, that channels 0–2 and channels 3–5 are the same scene at different times and that the task is to match patches between them. Nothing in the architecture says so.

FlowNetCorr

The second design processes each frame separately, then introduces an explicit correlation layer.

correlation layer A layer that computes the similarity between a feature at position x in one frame and features at every candidate displaced position in the other, producing a cost volume that directly encodes how well each possible displacement matches.

For a feature vector at position x in frame 1, it computes a dot product against the feature at x + d in frame 2, for every displacement d within a search radius. If the radius is 20 pixels in each direction, that is 41 × 41 = 1681 similarity scores per position.

The decoder’s job then becomes reading off which displacement scored best, and cleaning up ambiguity — which is far easier than discovering the concept of matching from scratch.

FlowNetSimpleFlowNetCorrClassical (Farnebäck)
Input handlingStack both framesSeparate towers, then correlateSolve per pixel
MatchingMust be learned implicitlyBuilt into the architectureExplicit, hand-designed
Training dataRequiredRequiredNone
Large displacementLimitedUp to the search radiusPoor without pyramids
Textureless regionsFills in plausibly from contextFills in plausibly from contextFails — no gradient to match
OcclusionLearns to interpolateLearns to interpolateProduces garbage
CostOne forward passHigher — cost volume is largeModerate CPU
Two ways to give a network the same job, plus the classical baseline

The general lesson generalises well beyond flow: encoding known structure in the architecture beats hoping the network discovers it. Convolution encodes translation equivariance. Skip connections encode that a layer should be able to pass its input through. Correlation encodes that this is a matching problem.

Frame interpolation

Given frames at times t and t+1, synthesise the frame at t+0.5.

The naive approach is to average the two frames. This is always wrong, and it is instructive to see why on actual numbers. Here is one row across a bright object moving right by 4 pixels:

Frame t (1×8)
20
20
200
200
20
20
20
20
Frame t+1 (1×8)
20
20
20
20
20
20
200
200
Averaged — two ghosts (1×8)
20
20
110
110
20
20
110
110
Flow-warped — one object (1×8)
20
20
20
20
200
200
20
20

Averaging produces two half-strength copies of the object, one at each original position and nothing in between. That is the ghosting you see in cheap slow motion. Warping along the flow field moves the object to where it actually was at the intermediate time, producing one object at full intensity.

The real pipeline:

The refinement stage exists for one specific reason. When an object moves, it uncovers background that was hidden in frame t and, if it moved far enough, may be hidden again in t+1. Those pixels have no source in either frame. Warping cannot produce them because there is nothing to warp. Only a generative step can fill them, and it is guessing.

disocclusion A region that becomes visible because something moved away from in front of it. In interpolation these pixels may have no correct source in either input frame, so they must be synthesised.
Approach Handles occlusion Cost per frame Typical use
Frame averaging No — ghosts Negligible Never, for motion
Frame duplication N/A — judders Negligible Simple frame-rate matching
Flow-based warping Poorly Moderate Basic slow motion
Flow + learned refinement Yes High Production slow motion, frame-rate upconversion
Direct kernel prediction Yes High Alternative to explicit flow

Interpolation methods by what they do at occlusion boundaries

Video stabilisation

Shaky footage has two motions mixed together: intentional camera movement, and unwanted shake. Stabilisation separates them.

Concretely, with horizontal camera position tracked over eight frames:

Frame Measured x Smoothed x Correction to apply
1 0 0.0 0.0
2 -3.2 0.6 +3.8
3 4.1 1.3 -2.8
4 -2.8 2.0 +4.8
5 5.6 2.9 -2.7
6 1.2 3.7 +2.5
7 6.9 4.6 -2.3
8 3.0 5.4 +2.4

The measured path oscillates wildly; the smoothed path drifts steadily right, which is the intended pan. The correction column is what gets applied as a warp.

The unavoidable cost: warping a frame leaves empty regions at the edges, so the output must be cropped. Stronger smoothing means larger corrections means a bigger crop. Typical loss is 5–15% of each dimension, and there is no way around it other than generating the missing borders, which is a guess.

Video denoising

This is where video’s advantage is starkest, and the reason is a single statistical fact.

Sensor noise is independent between frames. The signal is not — it is nearly the same frame to frame. So averaging N aligned frames reduces the noise standard deviation by sqrt(N) while leaving the signal alone.

Frames averaged Noise reduction factor SNR gain (dB) Motion blur risk
1 1.00× 0 None
2 1.41× 3.0 Low
4 2.00× 6.0 Moderate
8 2.83× 9.0 High
16 4.00× 12.0 Severe

Temporal averaging: sqrt(N) noise reduction. The right column is why you cannot simply keep increasing N.

Nothing an image denoiser does comes close to a free 6 dB. A single-image denoiser must infer what is noise and what is texture, and it gets that wrong on fine detail — which is why denoised photos look plastic. Temporal averaging needs no such inference.

The catch is in the last column. If anything in the scene moves, naive averaging smears it. So real video denoisers align first:

1. Estimate flow from each neighbouring frame to the reference frame
2. Warp neighbours onto the reference using that flow
3. Average, but reject any pixel whose warped value differs too much
   from the reference — that means the alignment failed
4. Fall back to spatial denoising for rejected pixels

Step 3 is what makes it work in practice. Alignment fails at occlusions and on fast motion, and blindly averaging a misaligned pixel produces a ghost far worse than the noise you were removing.

Detection that uses time

Back to the opening problem. Running a full detector on every frame is wasteful. Here is the structure that fixes it.

scale-time lattice A framework that runs an expensive detector only on sparse keyframes, then propagates those detections forward and backward in time and across scales using cheap networks, choosing where to spend compute based on how uncertain each region is.

Two things make this better than it sounds.

Propagation is genuinely cheap. Moving a box from one frame to the next using flow is a handful of arithmetic operations, against tens of GFLOPs for a detector forward pass.

Propagation runs both directions. Frame 4 is reached from keyframe 5 going backwards as well as from keyframe 1 going forwards. Backward propagation from a nearby keyframe is far more accurate than forward propagation from a distant one, so interior frames are not simply the worst case.

Temporal information also fixes the flicker problem directly. Detections that survive across frames are more trustworthy than one-frame detections:

history = {}   # track_id → list of recent confidences

def stabilise(track_id, conf, window=5, threshold=0.5):
    h = history.setdefault(track_id, [])
    h.append(conf)
    if len(h) > window:
        h.pop(0)
    # Report only if the object was confident in most recent frames
    return sum(c > threshold for c in h) >= len(h) // 2 + 1

This one change removes most of the flicker that per-frame detection produces, and it costs nothing.

Two harder video tasks

Worth knowing about, because they show where the field goes next.

Person search from a single portrait

Given one photograph of a person, find every appearance of them in hours of footage. It is not face recognition — the face is often not visible — and it is not tracking, because appearances are separated by long gaps.

Challenge Why it is hard Typical handling
Clothing changes Appearance features become useless Face and gait cues when available
Viewpoint changes Front-facing query, rear-facing footage Viewpoint-invariant embeddings
Scale variation 60 px tall at one camera, 400 at another Multi-scale feature extraction
Long gaps No continuity to exploit Re-identification embeddings and matching
Crowds Many similar-looking candidates Ranked retrieval, not classification

Person search combines detection, re-identification and retrieval, and the failure modes are different from each

This is a good moment to note that this capability is exactly the one covered in responsible computer vision. It is technically interesting and it is also the core of a surveillance system, so deploying it is a decision about more than accuracy.

Story understanding

Given a full film, answer “why did that character do that?” This needs identity tracking across scenes, dialogue, causal reasoning over long time spans, and knowledge the video never states. Current models handle clips of seconds to minutes. Feature-length narrative reasoning is open.

Putting it together

import cv2
import numpy as np

cap = cv2.VideoCapture("clip.mp4")
KEYFRAME_EVERY = 5

prev_gray, tracks, idx = None, [], 0

while True:
    ok, frame = cap.read()
    if not ok:
        break
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

    if idx % KEYFRAME_EVERY == 0:
        tracks = run_detector(frame)          # expensive path
    elif prev_gray is not None:
        flow = cv2.calcOpticalFlowFarneback(
            prev_gray, gray, None, 0.5, 3, 15, 3, 5, 1.2, 0)
        tracks = propagate(tracks, flow)      # cheap path

    prev_gray = gray
    idx += 1

def propagate(boxes, flow):
    out = []
    for (x, y, w, h, cls, conf) in boxes:
        # Median flow inside the box is robust to a few bad vectors
        region = flow[y:y+h, x:x+w]
        if region.size == 0:
            continue
        dx = float(np.median(region[..., 0]))
        dy = float(np.median(region[..., 1]))
        # Decay confidence with distance from the keyframe
        out.append((int(x+dx), int(y+dy), w, h, cls, conf * 0.97))
    return out

Two details carry most of the value. Using the median of the flow inside the box, not the mean, means a handful of wild vectors at the box edge cannot drag the whole box away. And decaying confidence on each propagation step means a stale track fades out on its own rather than persisting forever after the object has left.

Basic stabilisation:

transforms = []
prev_pts = cv2.goodFeaturesToTrack(prev_gray, 200, 0.01, 30)

for each frame:
    pts, status, _ = cv2.calcOpticalFlowPyrLK(prev_gray, gray, prev_pts, None)
    good_prev = prev_pts[status == 1]
    good_new  = pts[status == 1]
    m, _ = cv2.estimateAffinePartial2D(good_prev, good_new)
    transforms.append((m[0, 2], m[1, 2], np.arctan2(m[1, 0], m[0, 0])))

trajectory = np.cumsum(transforms, axis=0)
smoothed = moving_average(trajectory, window=30)
correction = smoothed - trajectory      # apply this as a warp per frame

Practice task

About an hour.

  1. Take a 10-second clip. Run a detector on every frame and record the total time and the boxes per frame.
  2. Now run it every 5th frame with flow propagation between. Compare total time and compare the boxes on the intermediate frames against the per-frame result. Compute how many you lost.
  3. Plot detection confidence for one object over time in both versions. The propagated version should be visibly smoother — that smoothness is the flicker you removed.
  4. Add temporal smoothing to the per-frame version using the snippet above. Count how many one-frame false positives it eliminates.
  5. Take a shaky handheld clip. Track features, plot the raw trajectory against a 30-frame moving average, and measure how much crop the largest correction demands.
  6. Shoot a static scene in low light. Average 8 aligned frames and compare noise against a single frame. Then average 8 frames of a scene with someone walking through it, without alignment, and see the smear.

Step 6 shows both halves of temporal denoising in five minutes.

Summary

Video’s defining property is that consecutive frames are nearly identical, and every technique here exploits that in one of two ways. Redundancy means computation can be reused: run the expensive detector on keyframes and propagate cheaply between them, for roughly a quarter of the compute at a couple of percent accuracy cost. Change means information: differences between frames reveal motion, boundaries and depth that no single frame contains.

Learned flow networks replaced hand-designed optimisation, and FlowNetCorr’s correlation layer illustrates a broader principle — encoding known structure in the architecture beats hoping the network finds it. Interpolation warps along flow rather than averaging, because averaging always ghosts, and it needs a generative step for pixels that neither input frame contains. Stabilisation separates intended camera motion from shake and always costs a crop. Denoising gets a sqrt(N) noise reduction for free from temporal averaging, provided you align first and reject pixels where alignment failed.

The single cheapest improvement to any per-frame system is temporal confidence smoothing. It removes most flicker and costs nothing.

What comes next

Real-time computer vision systems takes these ideas and turns them into a system with a latency budget, covering pipeline design, frame dropping, and where the time actually goes.

If the person-search discussion raised questions, responsible computer vision deals with the part that is not a technical problem.

Test your understanding
Your traffic camera system runs YOLO on every frame at 30 fps and cannot keep up. You switch to detecting every 5th frame with flow propagation. Throughput is fine, but you now miss motorcycles that weave rapidly between lanes. What is happening?
Test your understanding
You add video stabilisation before your existing detection pipeline to help with a shaky pole-mounted camera. Detection accuracy drops and your distance measurements are now wrong. Why?

Frequently asked questions

Should I use learned optical flow or a classical method?
Start classical. Farnebäck and Lucas–Kanade run on CPU, need no training data, and are good enough for slow, well-textured scenes — which covers most fixed-camera work. Move to a learned method when you have measured the classical one failing, which happens on large displacements, textureless regions, and occlusion boundaries. Sports, drone footage and fast pans are the usual triggers. If you do go learned, budget for fine-tuning on real footage from your domain, because the public models are trained largely on synthetic data.
How do I choose a keyframe interval?
Measure rather than guess. Run full per-frame detection on a representative clip to get a ground-truth reference, then run with intervals of 2, 5 and 10 and compare mAP against that reference. Plot accuracy against throughput and pick the knee. The right value depends almost entirely on motion speed relative to frame rate: a 30 fps camera watching pedestrians tolerates an interval of 10, while the same camera on a motorway may not tolerate 2. Better still, make it adaptive — shorten the interval when propagated confidence starts falling.
Can I use frame interpolation to make a low-frame-rate camera work like a high-frame-rate one?
For human viewing, yes, and it looks good. For measurement or detection, no. Interpolated frames contain no information that was not already in the neighbours, so any event that happened between the real frames is not recovered — it is invented plausibly. If an object appeared and vanished within the gap, interpolation will not show it. And interpolation artifacts cluster at occlusion boundaries, which is exactly where object edges are, so a detector run on interpolated frames sees synthetic edge structure. Use it for playback, not for analysis.
Why does temporal averaging reduce noise but not signal?
Because sensor noise is independent between frames while the signal is not. Averaging N independent random values with standard deviation σ gives a result with standard deviation σ/sqrt(N). The signal is nearly the same in every frame, so averaging leaves it essentially unchanged. Eight frames gives 2.83× less noise, or about 9 dB, which no single-image denoiser can approach because a single-image denoiser has to infer what is noise and what is texture. The requirement is alignment: if the content moved and you did not compensate, you average different things and get a smear.
How much crop does stabilisation cost?
Typically 5 to 15% of each dimension, and it scales directly with how aggressively you smooth. The mechanism is simple — warping a frame to compensate for shake leaves empty regions along the edges, and the crop must be large enough to exclude the worst-case correction across the whole clip. Stronger smoothing means larger corrections means a bigger crop. Some systems generate the missing borders from neighbouring frames, which works when the border content was visible at some point nearby and is a guess otherwise. Plan for the crop in your framing rather than discovering it afterwards.
Start typing to search across all content
navigate Enter open Esc close