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.

Steps to subtract a video background with OpenCV:

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

    Footage from a mostly stationary camera produces the clearest mask. The run command accepts another source path when needed.

  2. Create subtract_background.py with the argument parser and input-video checks.
    subtract_background.py
    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.")
  3. Append the background-subtraction initialization block after the frame-size check.
    subtract_background.py
    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.

  4. Append the frame-processing loop after the MOG2 initialization block.
    subtract_background.py
    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()
  5. Append the output finalization and run-report block after the frame loop.
    subtract_background.py
    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}")
  6. Run the completed script against the input clip.
    $ 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
  7. Inspect the strongest foreground-mask preview.

    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.

  8. Verify the generated foreground-mask artifacts with an independent Python check.
    $ 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]