Background subtraction separates moving pixels from a mostly fixed video scene. In OpenCV, it produces the foreground mask that later motion counters, object trackers, or activity alerts can consume.
The Python script uses cv.createBackgroundSubtractorMOG2 to build an adaptive background model from frames read with cv.VideoCapture. It writes a binary foreground-mask video through cv.VideoWriter and saves the strongest mask frame as a PNG for quick inspection.
Use footage from a stationary camera or a clip where the background stays mostly fixed. Panning cameras, lighting jumps, and long stops by moving objects can become part of the background model, so the run report includes warmup frames, active-frame count, and a peak foreground-pixel count before the mask is used for counting or tracking.
Use a clip from a mostly stationary camera. Replace input/sample-motion.mp4 in later commands if the video uses another path.
from argparse import ArgumentParser from pathlib import Path import cv2 as cv parser = ArgumentParser(description="Subtract the background from a video with OpenCV MOG2.") parser.add_argument("input_video", help="Input video path.") parser.add_argument("mask_video", help="Output video path for the foreground mask.") parser.add_argument("--preview", default="output/foreground-mask-preview.png", help="Output PNG path for the strongest mask frame.") parser.add_argument("--history", type=int, default=80, help="Number of frames kept in the background model.") parser.add_argument("--var-threshold", type=float, default=25.0, help="MOG2 variance threshold for foreground decisions.") parser.add_argument("--warmup", type=int, default=5, help="Frames to ignore while the background model initializes.") args = parser.parse_args() capture = cv.VideoCapture(args.input_video) if not capture.isOpened(): raise SystemExit(f"Could not open video: {args.input_video}") fps = capture.get(cv.CAP_PROP_FPS) or 25.0 width = int(capture.get(cv.CAP_PROP_FRAME_WIDTH)) height = int(capture.get(cv.CAP_PROP_FRAME_HEIGHT)) if width <= 0 or height <= 0: raise SystemExit("Could not read frame size from the input video.") mask_path = Path(args.mask_video) preview_path = Path(args.preview) mask_path.parent.mkdir(parents=True, exist_ok=True) preview_path.parent.mkdir(parents=True, exist_ok=True) fourcc = cv.VideoWriter_fourcc(*"mp4v") writer = cv.VideoWriter(str(mask_path), fourcc, fps, (width, height)) if not writer.isOpened(): raise SystemExit(f"Could not open VideoWriter for: {mask_path}") subtractor = cv.createBackgroundSubtractorMOG2( history=args.history, varThreshold=args.var_threshold, detectShadows=True, ) frame_count = 0 active_frames = 0 peak_pixels = 0 peak_mask = None while True: ok, frame = capture.read() if not ok: break frame_count += 1 raw_mask = subtractor.apply(frame) _, foreground_mask = cv.threshold(raw_mask, 254, 255, cv.THRESH_BINARY) writer.write(cv.cvtColor(foreground_mask, cv.COLOR_GRAY2BGR)) if frame_count > args.warmup: foreground_pixels = cv.countNonZero(foreground_mask) if foreground_pixels: active_frames += 1 if foreground_pixels > peak_pixels: peak_pixels = foreground_pixels peak_mask = foreground_mask.copy() capture.release() writer.release() if frame_count == 0: raise SystemExit(f"No frames were read from: {args.input_video}") if peak_mask is None: peak_mask = foreground_mask if not cv.imwrite(str(preview_path), peak_mask): raise SystemExit(f"Could not write preview image: {preview_path}") print(f"input={args.input_video}") print(f"frames={frame_count} size={width}x{height} fps={fps:.2f}") print(f"warmup_frames={min(args.warmup, frame_count)}") print(f"active_frames={active_frames} peak_foreground_pixels={peak_pixels}") print(f"wrote_video={mask_path}") print(f"wrote_preview={preview_path}")
MOG2 shadow detection can mark shadows with a mid-gray mask value. Thresholding at 255 keeps only foreground pixels in the saved mask video.
$ python3 subtract_background.py input/sample-motion.mp4 output/foreground-mask.mp4 --preview output/foreground-mask-preview.png input=input/sample-motion.mp4 frames=64 size=640x360 fps=12.00 warmup_frames=5 active_frames=59 peak_foreground_pixels=8859 wrote_video=output/foreground-mask.mp4 wrote_preview=output/foreground-mask-preview.png
White pixels are foreground motion after the warmup frames. A mostly white frame usually means the camera moved, the lighting changed sharply, or the background model needs more warmup frames.
$ python3 - <<'PY'
from pathlib import Path
import cv2 as cv
mask_video = Path("output/foreground-mask.mp4")
preview = Path("output/foreground-mask-preview.png")
capture = cv.VideoCapture(str(mask_video))
ok, frame = capture.read()
print(f"mask_video_exists={mask_video.is_file()}")
print(f"preview_exists={preview.is_file()}")
print(f"first_frame_read={ok}")
if ok:
print(f"first_frame_shape={frame.shape[1]}x{frame.shape[0]}")
capture.release()
PY
mask_video_exists=True
preview_exists=True
first_frame_read=True
first_frame_shape=640x360