Motion Detection

Property Value
Category Motion Analytics (classical computer vision)
Base Model Not applicable -- uses classical background subtraction
Source Framework OpenCV
Supported Precisions Not applicable
Inference Engine OpenCV (CPU) / GStreamer decode via DLStreamer
Hardware CPU, GPU (OpenCV UMat optional)
Detected Class(es) Generic foreground motion regions

Overview

Motion Detection is a Metro Analytics use case that flags moving regions in a video stream without requiring a deep-learning model. It uses the OpenCV MOG2 adaptive background subtractor to separate moving foreground pixels from a learned background, then groups them into bounding boxes.

A neural detector such as YOLO26 is the best choice when you need to know what is moving (person, vehicle, etc.). For raw "something changed in the frame" triggering, classical background subtraction is the most efficient and reliable choice, so this use case intentionally avoids a model.

Typical Metro deployments include:

  • Idle-camera Triggering -- wake heavier analytics only when motion is present.
  • Perimeter and After-hours Monitoring -- alert on any movement in a restricted area.
  • Bandwidth Reduction -- record or stream only frames that contain motion.
  • Pre-filter for Detection -- gate an expensive YOLO26 pipeline behind a cheap motion check.

Prerequisites

Create and activate a Python virtual environment before running the scripts:

python3 -m venv .venv --system-site-packages
source .venv/bin/activate

Note: The --system-site-packages flag is required so the virtual environment can access the system-installed OpenVINO and DLStreamer Python packages.


Getting Started

Download the Sample Video

This use case does not export or quantize a model. Run the provided script to download the sample test video:

chmod +x export_and_quantize.sh
./export_and_quantize.sh

The script downloads test_video.mp4 into the current directory.

OpenCV Sample

The sample below reads test_video.mp4, applies MOG2 background subtraction, removes shadows and noise, groups foreground pixels into bounding boxes, and writes the annotated result to output_opencv.mp4. It prints one line per frame with the number of motion regions found.

import cv2
import numpy as np

INPUT_VIDEO = "test_video.mp4"
MIN_AREA = 500  # ignore motion blobs smaller than this many pixels

cap = cv2.VideoCapture(INPUT_VIDEO)
fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))

bg = cv2.createBackgroundSubtractorMOG2(
    history=200, varThreshold=25, detectShadows=True)
writer = cv2.VideoWriter(
    "output_opencv.mp4", cv2.VideoWriter_fourcc(*"mp4v"), fps, (width, height))
kernel = np.ones((3, 3), np.uint8)

frame_idx = 0
motion_frames = 0
while True:
    ok, frame = cap.read()
    if not ok:
        break
    frame_idx += 1

    fg = bg.apply(frame)
    # MOG2 marks shadows as 127; keep only strong foreground (255).
    fg = cv2.threshold(fg, 200, 255, cv2.THRESH_BINARY)[1]
    fg = cv2.morphologyEx(fg, cv2.MORPH_OPEN, kernel)

    contours, _ = cv2.findContours(
        fg, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    regions = [c for c in contours if cv2.contourArea(c) >= MIN_AREA]
    if regions:
        motion_frames += 1
    for c in regions:
        x, y, w, h = cv2.boundingRect(c)
        cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 0, 255), 2)
    status_text = "Motion Detected" if regions else "No Motion"
    status_color = (0, 0, 255) if regions else (0, 255, 0)
    cv2.putText(frame, status_text, (10, 30),
                cv2.FONT_HERSHEY_SIMPLEX, 0.9, status_color, 2)
    cv2.putText(frame, f"Motion regions: {len(regions)}", (10, 60),
                cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2)
    print(f"Frame {frame_idx}: motion regions={len(regions)}", flush=True)
    writer.write(frame)

cap.release()
writer.release()
print(f"Motion detected in {motion_frames} frames", flush=True)

Device targets:

  • "CPU" -- default for OpenCV background subtraction.
  • "GPU" -- enable OpenCV transparent API by wrapping frames in cv2.UMat(frame) on systems with an OpenCL-capable Intel GPU.
  • "NPU" -- not applicable; background subtraction is not a neural workload.

Expected Output

OpenVINO expected output

DLStreamer Sample

The sample below uses the DLStreamer GStreamer decode stack (decodebin3 ! videoconvert) to pull frames into Python via appsink, applies the same MOG2 background subtraction, and encodes the annotated result to output_dlstreamer.mp4. Using appsink keeps the pipeline headless-safe and avoids VA-API zero-copy elements that fail over SSH.

import gi

gi.require_version("Gst", "1.0")
from gi.repository import Gst

import numpy as np

Gst.init([])

# Import cv2 after Gst.init to avoid GStreamer re-initialization conflicts.
import cv2

INPUT_VIDEO = "test_video.mp4"
MIN_AREA = 500

# Decode with the DLStreamer/GStreamer stack and hand BGR frames to OpenCV.
pipeline_str = (
    f"filesrc location={INPUT_VIDEO} ! decodebin3 ! videoconvert ! "
    "video/x-raw,format=BGR ! "
    "appsink name=sink emit-signals=false sync=false"
)
pipeline = Gst.parse_launch(pipeline_str)
sink = pipeline.get_by_name("sink")
pipeline.set_state(Gst.State.PLAYING)

bg = cv2.createBackgroundSubtractorMOG2(
    history=200, varThreshold=25, detectShadows=True)
kernel = np.ones((3, 3), np.uint8)
writer = None
frame_idx = 0
motion_frames = 0

while True:
    sample = sink.emit("pull-sample")
    if sample is None:
        break
    buf = sample.get_buffer()
    caps = sample.get_caps().get_structure(0)
    width = caps.get_value("width")
    height = caps.get_value("height")

    ok, mapinfo = buf.map(Gst.MapFlags.READ)
    if not ok:
        continue
    frame = np.ndarray((height, width, 3), dtype=np.uint8,
                       buffer=mapinfo.data).copy()
    buf.unmap(mapinfo)
    frame_idx += 1

    fg = bg.apply(frame)
    fg = cv2.threshold(fg, 200, 255, cv2.THRESH_BINARY)[1]
    fg = cv2.morphologyEx(fg, cv2.MORPH_OPEN, kernel)
    contours, _ = cv2.findContours(
        fg, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    regions = [c for c in contours if cv2.contourArea(c) >= MIN_AREA]
    if regions:
        motion_frames += 1
    for c in regions:
        x, y, w, h = cv2.boundingRect(c)
        cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 0, 255), 2)
    status_text = "Motion Detected" if regions else "No Motion"
    status_color = (0, 0, 255) if regions else (0, 255, 0)
    cv2.putText(frame, status_text, (10, 30),
                cv2.FONT_HERSHEY_SIMPLEX, 0.9, status_color, 2)

    if writer is None:
        writer = cv2.VideoWriter(
            "output_dlstreamer.mp4", cv2.VideoWriter_fourcc(*"mp4v"),
            30.0, (width, height))
    writer.write(frame)
    print(f"Frame {frame_idx}: motion regions={len(regions)}", flush=True)

pipeline.set_state(Gst.State.NULL)
if writer:
    writer.release()
print(f"Motion detected in {motion_frames} frames", flush=True)

The decode stack runs on the CPU; to offload decode to an Intel GPU, install the DLStreamer VA-API plugins and prepend vaapidecodebin in environments that support it (not recommended on headless or SSH systems).

Expected Output

DLStreamer expected output


License

Licensed under the MIT License. See LICENSE for details.

References

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Collection including Intel/motion-detection