Sync loitering-detection from metro-analytics-catalog
Browse files- README.md +41 -92
- expected_output_dlstreamer.gif +2 -2
README.md
CHANGED
|
@@ -127,7 +127,7 @@ only -- no Python polygon math required.
|
|
| 127 |
A typical surveillance-zone configuration on a 1280x720 source might be:
|
| 128 |
|
| 129 |
```text
|
| 130 |
-
roi=
|
| 131 |
LOITERING_SECONDS = 5.0 # dwell threshold, in seconds (demo value)
|
| 132 |
```
|
| 133 |
|
|
@@ -141,141 +141,90 @@ Per-person dwell time is measured at the bottom-center of the bounding box
|
|
| 141 |
|
| 142 |
### DLStreamer Sample
|
| 143 |
|
| 144 |
-
|
| 145 |
|
| 146 |
```bash
|
| 147 |
source /opt/intel/openvino_2026/setupvars.sh
|
| 148 |
source /opt/intel/dlstreamer/scripts/setup_dls_env.sh
|
| 149 |
-
export PYTHONPATH=/opt/intel/dlstreamer/python:
|
| 150 |
-
/opt/intel/dlstreamer/gstreamer/lib/python3/dist-packages:${PYTHONPATH:-}
|
| 151 |
```
|
| 152 |
|
| 153 |
-
|
| 154 |
|
| 155 |
```python
|
| 156 |
from collections import defaultdict
|
| 157 |
-
|
| 158 |
import gi
|
| 159 |
-
|
| 160 |
gi.require_version("Gst", "1.0")
|
| 161 |
-
gi.require_version("GstVideo", "1.0")
|
| 162 |
from gi.repository import Gst
|
| 163 |
-
from gstgva import VideoFrame
|
| 164 |
|
| 165 |
Gst.init(None)
|
| 166 |
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
LOITERING_SECONDS = 5.0
|
| 171 |
|
| 172 |
-
|
| 173 |
-
f"filesrc location={
|
| 174 |
-
f"videoconvert ! "
|
| 175 |
f"gvaattachroi roi={ROI} ! "
|
| 176 |
-
f"gvadetect inference-region=1 model={
|
| 177 |
-
f"threshold=0.5 ! queue ! "
|
| 178 |
f"gvatrack tracking-type=short-term-imageless ! queue ! "
|
| 179 |
-
f"
|
| 180 |
-
f"
|
| 181 |
-
f"gvawatermark ! videoconvert ! video/x-raw,format=I420 ! "
|
| 182 |
-
f"openh264enc ! h264parse ! "
|
| 183 |
-
f"mp4mux ! filesink name=sink location=output_dlstreamer.mp4"
|
| 184 |
)
|
| 185 |
-
pipeline = Gst.parse_launch(pipeline_str)
|
| 186 |
-
|
| 187 |
-
STALE_TIMEOUT = 2.0 # seconds of absence before clearing dwell state
|
| 188 |
-
dwell_state: dict[int, float] = defaultdict(float)
|
| 189 |
-
last_seen: dict[int, float] = {}
|
| 190 |
-
flagged: set[int] = set()
|
| 191 |
|
|
|
|
|
|
|
|
|
|
| 192 |
|
| 193 |
def on_buffer(pad, info):
|
| 194 |
buf = info.get_buffer()
|
| 195 |
-
|
| 196 |
-
frame = VideoFrame(buf, caps=caps)
|
| 197 |
-
|
| 198 |
now = buf.pts / Gst.SECOND if buf.pts != Gst.CLOCK_TIME_NONE else 0.0
|
| 199 |
-
seen_ids: set[int] = set()
|
| 200 |
|
| 201 |
for region in frame.regions():
|
| 202 |
-
# gvaattachroi attaches a frame-level ROI region; skip it.
|
| 203 |
if region.label() != "person":
|
| 204 |
continue
|
| 205 |
-
|
| 206 |
-
if
|
| 207 |
continue
|
| 208 |
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
foot_y = int(rect.y + rect.h)
|
| 212 |
-
seen_ids.add(object_id)
|
| 213 |
-
|
| 214 |
-
# gvadetect inference-region=1 already constrains detections to the
|
| 215 |
-
# gvaattachroi zone, so every tracked person here is "in zone".
|
| 216 |
-
prev = last_seen.get(object_id, now)
|
| 217 |
-
dwell_state[object_id] += now - prev
|
| 218 |
-
last_seen[object_id] = now
|
| 219 |
-
|
| 220 |
-
if (
|
| 221 |
-
dwell_state[object_id] >= LOITERING_SECONDS
|
| 222 |
-
and object_id not in flagged
|
| 223 |
-
):
|
| 224 |
-
flagged.add(object_id)
|
| 225 |
-
print(
|
| 226 |
-
f"LOITERING id={object_id} "
|
| 227 |
-
f"dwell={dwell_state[object_id]:.1f}s "
|
| 228 |
-
f"anchor=({foot_x},{foot_y})",
|
| 229 |
-
flush=True,
|
| 230 |
-
)
|
| 231 |
-
|
| 232 |
-
# Clean up stale tracks after STALE_TIMEOUT seconds of absence.
|
| 233 |
-
# Keep flagged entries to prevent duplicate alerts when a person
|
| 234 |
-
# briefly disappears (occlusion / tracker jitter) and reappears.
|
| 235 |
-
for stale in list(dwell_state):
|
| 236 |
-
if stale not in seen_ids:
|
| 237 |
-
elapsed_since = now - last_seen.get(stale, now)
|
| 238 |
-
if elapsed_since > STALE_TIMEOUT:
|
| 239 |
-
dwell_state.pop(stale, None)
|
| 240 |
-
last_seen.pop(stale, None)
|
| 241 |
|
| 242 |
-
|
|
|
|
|
|
|
|
|
|
| 243 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
|
| 245 |
-
|
| 246 |
-
sink_pad = sink.get_static_pad("sink")
|
| 247 |
-
sink_pad.add_probe(Gst.PadProbeType.BUFFER, on_buffer)
|
| 248 |
|
|
|
|
| 249 |
pipeline.set_state(Gst.State.PLAYING)
|
| 250 |
-
|
| 251 |
-
bus.timed_pop_filtered(
|
| 252 |
-
Gst.CLOCK_TIME_NONE,
|
| 253 |
-
Gst.MessageType.EOS | Gst.MessageType.ERROR,
|
| 254 |
-
)
|
| 255 |
pipeline.set_state(Gst.State.NULL)
|
| 256 |
```
|
| 257 |
|
| 258 |
-
Expected output
|
| 259 |
-
(exact track IDs and anchor coordinates may vary between runs due to
|
| 260 |
-
tracker non-determinism):
|
| 261 |
|
| 262 |
```text
|
| 263 |
-
LOITERING id=26 dwell=5.0s
|
| 264 |
-
LOITERING id=27 dwell=5.0s
|
| 265 |
-
LOITERING id=29 dwell=5.0s anchor=(90,322)
|
| 266 |
...
|
| 267 |
```
|
| 268 |
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
The annotated video is saved to `output_dlstreamer.mp4` with green bounding boxes and
|
| 272 |
-
track IDs drawn by `gvawatermark` around every detected person.
|
| 273 |
-
|
| 274 |
-
> **Known warning:** The `openh264enc` element prints
|
| 275 |
-
> `[OpenH264] this = 0x..., Error:CWelsH264SVCEncoder::EncodeFrame(), cmInitParaError.`
|
| 276 |
-
> on the first frame. This is a benign initialization message — the output
|
| 277 |
-
> video is encoded correctly. The warning comes from the OpenH264 library's
|
| 278 |
-
> internal logging and does not indicate a real error.
|
| 279 |
|
| 280 |
#### Expected Output
|
| 281 |
|
|
|
|
| 127 |
A typical surveillance-zone configuration on a 1280x720 source might be:
|
| 128 |
|
| 129 |
```text
|
| 130 |
+
roi=0,200,300,400 # ROI for gvaattachroi (x_min,y_min,x_max,y_max)
|
| 131 |
LOITERING_SECONDS = 5.0 # dwell threshold, in seconds (demo value)
|
| 132 |
```
|
| 133 |
|
|
|
|
| 141 |
|
| 142 |
### DLStreamer Sample
|
| 143 |
|
| 144 |
+
Set up the environment:
|
| 145 |
|
| 146 |
```bash
|
| 147 |
source /opt/intel/openvino_2026/setupvars.sh
|
| 148 |
source /opt/intel/dlstreamer/scripts/setup_dls_env.sh
|
| 149 |
+
export PYTHONPATH=/opt/intel/dlstreamer/python:/opt/intel/dlstreamer/gstreamer/lib/python3/dist-packages:${PYTHONPATH:-}
|
|
|
|
| 150 |
```
|
| 151 |
|
| 152 |
+
Run loitering detection:
|
| 153 |
|
| 154 |
```python
|
| 155 |
from collections import defaultdict
|
| 156 |
+
import ctypes
|
| 157 |
import gi
|
|
|
|
| 158 |
gi.require_version("Gst", "1.0")
|
|
|
|
| 159 |
from gi.repository import Gst
|
| 160 |
+
from gstgva import Tensor, VideoFrame
|
| 161 |
|
| 162 |
Gst.init(None)
|
| 163 |
|
| 164 |
+
libgst = ctypes.CDLL("libgstreamer-1.0.so.0")
|
| 165 |
+
libgst.gst_structure_new_empty.argtypes = [ctypes.c_char_p]
|
| 166 |
+
libgst.gst_structure_new_empty.restype = ctypes.c_void_p
|
| 167 |
+
|
| 168 |
+
MODEL = "yolo26n_openvino_model/yolo26n.xml"
|
| 169 |
+
VIDEO = "VIRAT_S_000101.mp4"
|
| 170 |
+
ROI = "0,200,300,400"
|
| 171 |
LOITERING_SECONDS = 5.0
|
| 172 |
|
| 173 |
+
pipeline = Gst.parse_launch(
|
| 174 |
+
f"filesrc location={VIDEO} ! decodebin3 ! videoconvert ! "
|
|
|
|
| 175 |
f"gvaattachroi roi={ROI} ! "
|
| 176 |
+
f"gvadetect inference-region=1 model={MODEL} device=GPU threshold=0.5 ! queue ! "
|
|
|
|
| 177 |
f"gvatrack tracking-type=short-term-imageless ! queue ! "
|
| 178 |
+
f"gvafpscounter ! identity name=probe ! gvawatermark ! videoconvert ! video/x-raw,format=I420 ! "
|
| 179 |
+
f"openh264enc ! h264parse ! mp4mux ! filesink location=output_dlstreamer.mp4"
|
|
|
|
|
|
|
|
|
|
| 180 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
|
| 182 |
+
dwell = defaultdict(float)
|
| 183 |
+
last_seen = {}
|
| 184 |
+
flagged = set()
|
| 185 |
|
| 186 |
def on_buffer(pad, info):
|
| 187 |
buf = info.get_buffer()
|
| 188 |
+
frame = VideoFrame(buf, caps=pad.get_current_caps())
|
|
|
|
|
|
|
| 189 |
now = buf.pts / Gst.SECOND if buf.pts != Gst.CLOCK_TIME_NONE else 0.0
|
|
|
|
| 190 |
|
| 191 |
for region in frame.regions():
|
|
|
|
| 192 |
if region.label() != "person":
|
| 193 |
continue
|
| 194 |
+
oid = region.object_id()
|
| 195 |
+
if oid <= 0:
|
| 196 |
continue
|
| 197 |
|
| 198 |
+
dwell[oid] += now - last_seen.get(oid, now)
|
| 199 |
+
last_seen[oid] = now
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
|
| 201 |
+
# Show dwell time on the bounding box (rendered by gvawatermark)
|
| 202 |
+
t = Tensor(libgst.gst_structure_new_empty(b"dwell"))
|
| 203 |
+
t.set_label(f" {dwell[oid]:.1f}s")
|
| 204 |
+
region.add_tensor(t)
|
| 205 |
|
| 206 |
+
if dwell[oid] >= LOITERING_SECONDS and oid not in flagged:
|
| 207 |
+
flagged.add(oid)
|
| 208 |
+
rect = region.rect()
|
| 209 |
+
print(f"LOITERING id={oid} dwell={dwell[oid]:.1f}s pos=({int(rect.x + rect.w/2)},{int(rect.y + rect.h)})")
|
| 210 |
|
| 211 |
+
return Gst.PadProbeReturn.OK
|
|
|
|
|
|
|
| 212 |
|
| 213 |
+
pipeline.get_by_name("probe").get_static_pad("src").add_probe(Gst.PadProbeType.BUFFER, on_buffer)
|
| 214 |
pipeline.set_state(Gst.State.PLAYING)
|
| 215 |
+
pipeline.get_bus().timed_pop_filtered(Gst.CLOCK_TIME_NONE, Gst.MessageType.EOS | Gst.MessageType.ERROR)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
pipeline.set_state(Gst.State.NULL)
|
| 217 |
```
|
| 218 |
|
| 219 |
+
Expected output:
|
|
|
|
|
|
|
| 220 |
|
| 221 |
```text
|
| 222 |
+
LOITERING id=26 dwell=5.0s pos=(147,341)
|
| 223 |
+
LOITERING id=27 dwell=5.0s pos=(122,337)
|
|
|
|
| 224 |
...
|
| 225 |
```
|
| 226 |
|
| 227 |
+
The annotated video is saved to `output.mp4`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
|
| 229 |
#### Expected Output
|
| 230 |
|
expected_output_dlstreamer.gif
CHANGED
|
Git LFS Details
|
|
Git LFS Details
|