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
- Python 3.11+
- Install OpenVINO (latest version)
- Install Intel DLStreamer (latest version)
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-packagesflag 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 incv2.UMat(frame)on systems with an OpenCL-capable Intel GPU."NPU"-- not applicable; background subtraction is not a neural workload.
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
License
Licensed under the MIT License. See LICENSE for details.

