Background subtraction and optical flow

OpenCV · Advanced · 15 min

Motion is the cheapest strong signal in video: it powers people counters, surveillance and camera stabilisation.

Background subtraction learns what the static scene looks like and flags pixels that differ. cv2.createBackgroundSubtractorMOG2(history, varThreshold, detectShadows) models each pixel as a mixture of Gaussians that adapts over time. Call fg = sub.apply(frame) on every frame; the result is a mask with 255 for foreground and, if shadow detection is on, 127 for shadows. Threshold at 200 to drop shadows, clean up with morphology, then find contours to get moving blobs. createBackgroundSubtractorKNN is an alternative. The model needs some frames to learn, and a moving camera breaks the assumption entirely.

Optical flow estimates how pixels move between two frames.

  • Sparse: cv2.goodFeaturesToTrack picks corners, then cv2.calcOpticalFlowPyrLK(prev_gray, gray, p0, None) follows them, returning the new points, a status array (1 = tracked) and errors. Keep only points with status 1 and re-detect periodically as points are lost. The pyramidal version handles larger motion.
  • Dense: cv2.calcOpticalFlowFarneback gives a (h, w, 2) displacement field for every pixel, useful for motion heatmaps but much slower.

For tracking a specific object, OpenCV also provides tracker classes such as cv2.TrackerMIL_create, while modern pipelines often pair a DNN detector with a lightweight tracker.

Example

import cv2
import numpy as np

sub = cv2.createBackgroundSubtractorMOG2(history=50, detectShadows=False)
bg = np.full((80, 80), 100, np.uint8)
for _ in range(30):
    sub.apply(bg)                               # learn the empty scene
frame = bg.copy()
frame[30:50, 30:50] = 250                       # an object appears
fg = sub.apply(frame)
print(fg[40, 40], fg[5, 5])                     # 255 0

prev = np.zeros((100, 100), np.uint8)
cv2.rectangle(prev, (30, 30), (60, 60), 255, -1)
nxt = np.roll(prev, (2, 3), axis=(0, 1))        # moved 3 px right, 2 px down
p0 = cv2.goodFeaturesToTrack(prev, maxCorners=4, qualityLevel=0.1, minDistance=5)
p1, st, err = cv2.calcOpticalFlowPyrLK(prev, nxt, p0, None)
moves = (p1 - p0)[st.ravel() == 1].reshape(-1, 2)
print(np.round(moves.mean(axis=0)))             # [3. 2.]  (dx, dy)

Remember

  • MOG2/KNN subtractors learn a background model; apply() returns a foreground mask
  • Shadows are marked 127 in MOG2; threshold them away
  • calcOpticalFlowPyrLK tracks sparse points; keep only status == 1
  • Farneback gives dense per-pixel flow at higher cost

Watch out for

  • Using background subtraction with a moving or zooming camera, where everything looks like foreground.

Check yourself

  1. What does the `status` array returned by calcOpticalFlowPyrLK tell you?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.opencv.org/4.x/d4/dee/tutorial_optical_flow.html

Builds on

Unlocks

Nothing builds on this yet.