Background subtraction separates moving pixels from a mostly fixed video scene. In OpenCV, the resulting foreground mask can feed motion counters, object trackers, or activity alerts without retaining the original frame colors.
The Python program uses cv.VideoCapture for input, cv.createBackgroundSubtractorMOG2 for the adaptive background model, and cv.VideoWriter for the mask video. It also saves the strongest post-settling mask as a PNG so the detected shape can be inspected without playing the output clip.
A stationary camera produces the clearest separation. Camera movement, sudden lighting changes, or objects that stop for long periods can shift the model, so the program reports its settling period, active-frame count, and peak foreground-pixel count before the mask is used elsewhere.
Footage from a mostly stationary camera produces the clearest mask. The run command accepts another source path when needed.
from argparse import ArgumentParser from pathlib import Path import cv2 as cv parser = ArgumentParser(description="Subtract a video background 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) parser.add_argument("--var-threshold", type=float, default=25.0) parser.add_argument("--warmup", type=int, default=12) 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) writer = cv.VideoWriter( str(mask_path), cv.VideoWriter_fourcc(*"mp4v"), fps, (width, height), ) if not writer.isOpened(): raise SystemExit(f"Could not create mask video: {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
MOG2 uses 127 for detected shadows and 255 for foreground by default. The processing loop thresholds at 254 so the saved mask keeps only definite foreground pixels.
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}")
$ python3 subtract_background.py input/sample-motion.mp4 output/foreground-mask.mp4 input=input/sample-motion.mp4 frames=64 size=640x360 fps=12.00 warmup_frames=12 active_frames=52 peak_foreground_pixels=7144 wrote_video=output/foreground-mask.mp4 wrote_preview=output/foreground-mask-preview.png
White pixels are foreground motion after the model-settling frames. A mostly white preview indicates camera movement, a sharp lighting change, or too short a settling period.
$ python3 - <<'PY'
from pathlib import Path
import cv2 as cv
import numpy as np
mask_video = Path("output/foreground-mask.mp4")
preview_path = Path("output/foreground-mask-preview.png")
capture = cv.VideoCapture(str(mask_video))
output_frames = int(capture.get(cv.CAP_PROP_FRAME_COUNT))
ok, first_frame = capture.read()
capture.release()
preview = cv.imread(str(preview_path), cv.IMREAD_GRAYSCALE)
if preview is None:
raise SystemExit(f"Could not read preview: {preview_path}")
print(f"mask_video_exists={mask_video.is_file()}")
print(f"preview_exists={preview_path.is_file()}")
print(f"output_frames={output_frames}")
print(f"first_frame_read={ok}")
if ok:
print(f"first_frame_shape={first_frame.shape[1]}x{first_frame.shape[0]}")
print(f"preview_nonzero_pixels={cv.countNonZero(preview)}")
print(f"preview_values={np.unique(preview).tolist()}")
PY
mask_video_exists=True
preview_exists=True
output_frames=64
first_frame_read=True
first_frame_shape=640x360
preview_nonzero_pixels=7144
preview_values=[0, 255]