File size: 8,561 Bytes
f51e1ae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
---
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/)