--- license: mit license_link: LICENSE library_name: opencv tags: - opencv - intel - motion-detection - background-subtraction - edge-ai - metro - dlstreamer language: - en --- # 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](https://docs.openvino.ai/2026/get-started/install-openvino.html) (latest version) - [Install Intel DLStreamer](https://docs.openedgeplatform.intel.com/2026.0/edge-ai-libraries/dlstreamer/get_started/install/install_guide_ubuntu.html) (latest version) Create and activate a Python virtual environment before running the scripts: ```bash 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: ```bash 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. ```python 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](expected_output_openvino.gif) ### 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. ```python 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](expected_output_dlstreamer.gif) --- ## License Licensed under the MIT License. See [LICENSE](LICENSE) for details. ## References - [OpenCV Background Subtraction Tutorial](https://docs.opencv.org/4.x/d1/dc5/tutorial_background_subtraction.html) - [OpenCV MOG2 Background Subtractor](https://docs.opencv.org/4.x/d7/d7b/classcv_1_1BackgroundSubtractorMOG2.html) - [Intel DLStreamer](https://docs.openedgeplatform.intel.com/2026.0/edge-ai-libraries/dlstreamer/index.html) - [OpenVINO Documentation](https://docs.openvino.ai/)