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.

Steps to subtract a video background with OpenCV:

  1. Place the source video at input/sample-motion.mp4.

    Use a clip from a mostly stationary camera. Replace input/sample-motion.mp4 in later commands if the video uses another path.

  2. Save the background-subtraction script as subtract_background.py.
    subtract_background.py
    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.

  3. Run the script against the input clip.
    $ 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
  4. Review the strongest foreground-mask preview.

    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.

  5. Verify that the saved mask video can be reopened.
    $ 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