vagheshpatel commited on
Commit
1e28edb
·
verified ·
1 Parent(s): bfa4716

Sync perimeter-breach-detection from metro-analytics-catalog

Browse files
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ expected_output_dlstreamer.gif filter=lfs diff=lfs merge=lfs -text
37
+ expected_output_openvino.gif filter=lfs diff=lfs merge=lfs -text
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) Intel Corporation.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE
README.md ADDED
@@ -0,0 +1,417 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ license_link: LICENSE
4
+ library_name: openvino
5
+ pipeline_tag: object-detection
6
+ tags:
7
+ - openvino
8
+ - intel
9
+ - yolo
10
+ - yolo26
11
+ - perimeter-breach-detection
12
+ - intrusion-detection
13
+ - zone-analytics
14
+ - tracking
15
+ - gstanalytics
16
+ - gvaanalytics
17
+ - edge-ai
18
+ - metro
19
+ - dlstreamer
20
+ language:
21
+ - en
22
+ ---
23
+
24
+ # Perimeter Breach Detection
25
+
26
+ | Property | Value |
27
+ |---|---|
28
+ | **Category** | Object Detection + Tracking + Zone Analytics (GstAnalytics) |
29
+ | **Base Model** | [YOLO26](https://docs.ultralytics.com/models/yolo26/) (Ultralytics) |
30
+ | **Source Framework** | PyTorch (Ultralytics) |
31
+ | **Supported Precisions** | FP32, FP16, INT8 (mixed-precision) |
32
+ | **Inference Engine** | OpenVINO |
33
+ | **Hardware** | CPU, GPU, NPU |
34
+ | **Detected Class** | `person` (COCO class 0) |
35
+
36
+ ---
37
+
38
+ ## Overview
39
+
40
+ Perimeter Breach Detection is a Metro Analytics use case that flags people who breach a secured perimeter marked by yellow and black safety tape.
41
+ It is built on [YOLO26](https://docs.ultralytics.com/models/yolo26/) for person detection, paired with a multi-object tracker that assigns persistent IDs across frames.
42
+ The restricted region is a polygon that traces the safety tape: in the bundled sample video the yellow and black tape forms a diagonal boundary across the floor, and the default zone covers the keep-out side of that boundary.
43
+ A person whose center falls inside the polygon is reported as a perimeter breach.
44
+ The model is a quantized (INT8) state-of-the-art detector; smaller variants run at high FPS on edge hardware.
45
+
46
+ Typical Metro deployments include:
47
+
48
+ - **Fence-Line Protection** -- trigger when a person crosses a fence or barrier around depots, yards, or substations.
49
+ - **Secured-Transportation Facilities** -- monitor taped-off loading docks, platforms, and maintenance bays that must stay clear.
50
+ - **Work-Zone Safety** -- alert when a worker or bystander enters a taped hazard area near equipment or track work.
51
+ - **Restricted-Area Enforcement** -- raise alerts when anyone enters a standoff boundary marked with safety tape.
52
+
53
+ Available variants: `yolo26n`, `yolo26s`, `yolo26m`, `yolo26l`, `yolo26x`.
54
+ Smaller variants (`yolo26n`, `yolo26s`) are recommended for high-FPS edge deployment.
55
+
56
+ ---
57
+
58
+ ## Prerequisites
59
+
60
+ - Python 3.11+
61
+ - [Install OpenVINO](https://docs.openvino.ai/2026/get-started/install-openvino.html) (latest version)
62
+ - [Install Intel DLStreamer](https://docs.openedgeplatform.intel.com/2026.0/edge-ai-libraries/dlstreamer/get_started/install/install_guide_ubuntu.html) (latest version)
63
+
64
+ Create and activate a Python virtual environment before running the scripts:
65
+
66
+ ```bash
67
+ python3 -m venv .venv --system-site-packages
68
+ source .venv/bin/activate
69
+ ```
70
+
71
+ > **Note:** The `--system-site-packages` flag is required so the virtual
72
+ > environment can access the system-installed OpenVINO and DLStreamer Python
73
+ > packages.
74
+
75
+ ---
76
+
77
+ ## Getting Started
78
+
79
+ ### Download and Quantize Model
80
+
81
+ Run the provided script to download, export to OpenVINO IR, and optionally quantize:
82
+
83
+ ```bash
84
+ chmod +x export_and_quantize.sh
85
+ ./export_and_quantize.sh
86
+ ```
87
+
88
+ This exports the default **yolo26n** model in **FP16** precision.
89
+
90
+ #### Optional: Select a Different Variant or Precision
91
+
92
+ ```bash
93
+ ./export_and_quantize.sh yolo26n FP32 # full-precision
94
+ ./export_and_quantize.sh yolo26n INT8 # quantized
95
+ ./export_and_quantize.sh yolo26s # larger variant, default FP16
96
+ ```
97
+
98
+ Replace `yolo26n` with any variant (`yolo26s`, `yolo26m`, `yolo26l`, `yolo26x`).
99
+ The second argument selects the precision (`FP32`, `FP16`, `INT8`); the default is **FP16**.
100
+
101
+ The script performs the following steps:
102
+
103
+ 1. Installs dependencies (`openvino`, `ultralytics`, `opencv-python`; adds `nncf` for INT8).
104
+ 2. Downloads the sample worker-zone video (`worker-zone-detection.mp4`) into the current directory.
105
+ 3. Downloads the PyTorch weights and exports to OpenVINO IR.
106
+ 4. *(INT8 only)* Quantizes the model using NNCF post-training quantization.
107
+
108
+ Output files:
109
+
110
+ - `yolo26n_openvino_model/` -- FP32 or FP16 OpenVINO IR model directory.
111
+ - `yolo26n_perimeter_int8.xml` / `yolo26n_perimeter_int8.bin` -- INT8 quantized model *(only when `INT8` is selected)*.
112
+
113
+ #### Precision / Device Compatibility
114
+
115
+ | Precision | CPU | GPU | NPU |
116
+ |---|---|---|---|
117
+ | FP32 | Yes | Yes | No |
118
+ | FP16 | Yes | Yes | Yes |
119
+ | INT8 | Yes | Yes | Yes |
120
+
121
+ > **Note:** The INT8 calibration uses frames from the bundled sample video.
122
+ > For production accuracy, replace it with a representative set of frames from
123
+ > the target deployment site.
124
+
125
+ ### OpenVINO Sample
126
+
127
+ The sample below runs YOLO26 inference on the sample video and flags any person whose center falls inside the restricted zone as a breach.
128
+ The zone is the `RESTRICTED_ZONE` polygon, which traces the yellow and black safety tape in the sample video's `1920x1080` pixel space: the tape runs as a diagonal boundary from `(1577, 0)` to `(434, 1079)`, and the polygon covers the keep-out side of that line.
129
+ To adapt the perimeter to a different camera, edit the `RESTRICTED_ZONE` points.
130
+ YOLO26 is end-to-end (NMS-free), so no manual non-maximum suppression is needed.
131
+ Breaching people are drawn with a red box and a `BREACH` label; the annotated result is written to `output_openvino.mp4`.
132
+ Change the `device` string to run on CPU, GPU, or NPU.
133
+
134
+ ```python
135
+ import cv2
136
+ import numpy as np
137
+ import openvino as ov
138
+
139
+ PERSON_CLASS_ID = 0
140
+ CONF_THRESHOLD = 0.4
141
+ INPUT_SIZE = 640
142
+ INPUT_VIDEO = "worker-zone-detection.mp4"
143
+
144
+ # Restricted zone traced from the yellow-and-black safety tape in the sample
145
+ # video (1920x1080). The tape runs diagonally from (1577, 0) to (434, 1079);
146
+ # this polygon covers the keep-out side of that boundary. Edit these points to
147
+ # retrace the tape for a different camera.
148
+ RESTRICTED_ZONE = np.array([[0, 0], [1577, 0], [434, 1079], [0, 1080]], dtype=np.int32)
149
+
150
+ core = ov.Core()
151
+ model = core.read_model("yolo26n_openvino_model/yolo26n.xml")
152
+
153
+ # Change device to "GPU" or "NPU" to run on integrated GPU or NPU.
154
+ compiled = core.compile_model(model, "CPU")
155
+
156
+ cap = cv2.VideoCapture(INPUT_VIDEO)
157
+ fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
158
+ w0 = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
159
+ h0 = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
160
+
161
+ zone = RESTRICTED_ZONE
162
+
163
+ writer = cv2.VideoWriter(
164
+ "output_openvino.mp4", cv2.VideoWriter_fourcc(*"mp4v"), fps, (w0, h0)
165
+ )
166
+
167
+ sx, sy = w0 / INPUT_SIZE, h0 / INPUT_SIZE
168
+ frame_idx = 0
169
+ while True:
170
+ ok, frame = cap.read()
171
+ if not ok:
172
+ break
173
+ frame_idx += 1
174
+
175
+ blob = cv2.resize(frame, (INPUT_SIZE, INPUT_SIZE))
176
+ blob = cv2.cvtColor(blob, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
177
+ blob = blob.transpose(2, 0, 1)[np.newaxis, ...] # NCHW
178
+
179
+ # YOLO26 end-to-end output: [1, 300, 6] = [x1, y1, x2, y2, confidence, class_id]
180
+ output = compiled([blob])[compiled.output(0)][0]
181
+ mask = (output[:, 4] >= CONF_THRESHOLD) & (output[:, 5].astype(int) == PERSON_CLASS_ID)
182
+
183
+ # Draw the translucent restricted zone first, then the person boxes on top.
184
+ overlay = frame.copy()
185
+ cv2.fillPoly(overlay, [zone], (0, 0, 255))
186
+ cv2.addWeighted(overlay, 0.25, frame, 0.75, 0, frame)
187
+ cv2.polylines(frame, [zone], True, (0, 255, 255), 2)
188
+
189
+ breaches = 0
190
+ for det in output[mask]:
191
+ x1, y1 = int(det[0] * sx), int(det[1] * sy)
192
+ x2, y2 = int(det[2] * sx), int(det[3] * sy)
193
+ center = (int((x1 + x2) / 2), int((y1 + y2) / 2))
194
+ inside = cv2.pointPolygonTest(zone, center, False) >= 0
195
+ color = (0, 0, 255) if inside else (0, 255, 0)
196
+ cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)
197
+ if inside:
198
+ breaches += 1
199
+ cv2.putText(
200
+ frame, "BREACH", (x1, max(y1 - 6, 12)),
201
+ cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2,
202
+ )
203
+
204
+ cv2.putText(
205
+ frame, f"Breaches: {breaches}", (10, 30),
206
+ cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 255), 2,
207
+ )
208
+ if breaches:
209
+ print(f"frame {frame_idx}: perimeter breach - {breaches} person(s) inside zone", flush=True)
210
+ writer.write(frame)
211
+
212
+ cap.release()
213
+ writer.release()
214
+ print("Saved: output_openvino.mp4")
215
+ ```
216
+
217
+ **Device targets:**
218
+
219
+ - `"CPU"` -- default, works on all Intel platforms.
220
+ - `"GPU"` -- Intel integrated or discrete GPU.
221
+ - `"NPU"` -- Intel NPU (different throughput profile; validate with `benchmark_app -d NPU`).
222
+
223
+ #### Expected Output
224
+
225
+ ![OpenVINO expected output](expected_output_openvino.gif)
226
+
227
+ `output_openvino.mp4` shows the restricted perimeter shaded in red, a green box around each person outside the zone, and a red `BREACH` box around anyone inside it.
228
+
229
+ ### DLStreamer Sample
230
+
231
+ The pipeline below runs the FP16 YOLO26 detector on the sample video via `gvadetect`, tracks each person with `gvatrack`, and uses DLStreamer's `gvaanalytics` element to test membership in the restricted zone.
232
+ The zone is the same `RESTRICTED_ZONE` polygon used by the OpenVINO sample, passed to `gvaanalytics` as a JSON zone, so no polygon math is required in the code.
233
+ `gvaanalytics` attaches `GstAnalyticsZoneMtd` to every tracked person whose center falls inside the polygon.
234
+ The pipeline ends in an `appsink`; for each frame a callback reads the analytics metadata, shades the restricted zone, draws a green box around people outside it and a red `BREACH` box around anyone inside, and writes the annotated result to `output_dlstreamer.mp4`.
235
+ A perimeter-breach event is printed the first time each tracked person enters the zone.
236
+
237
+ > **Notes on running this sample:**
238
+ >
239
+ > - Use the FP16 IR (`yolo26n_openvino_model/yolo26n.xml`).
240
+ > On DLStreamer 2026.1, `gvadetect` cannot auto-derive a YOLO post-processor
241
+ > from the INT8 model produced by the bundled script.
242
+ > To use the INT8 model, supply a matching `model-proc` JSON.
243
+ > - Class names are read automatically from the model's embedded
244
+ > `metadata.yaml` by DLStreamer 2026.0+ -- no external `labels-file` is
245
+ > required.
246
+ > - Export `PYTHONPATH` so the DLStreamer Python module is importable:
247
+ >
248
+ > ```bash
249
+ > source /opt/intel/openvino_2026/setupvars.sh
250
+ > source /opt/intel/dlstreamer/scripts/setup_dls_env.sh
251
+ > export PYTHONPATH=/opt/intel/dlstreamer/python:\
252
+ > /opt/intel/dlstreamer/gstreamer/lib/python3/dist-packages:${PYTHONPATH:-}
253
+ > ```
254
+
255
+ ```python
256
+ import json
257
+ import sys
258
+ import gi
259
+
260
+ gi.require_version("Gst", "1.0")
261
+ gi.require_version("GstApp", "1.0")
262
+ gi.require_version("GstAnalytics", "1.0")
263
+ gi.require_version("DLStreamerMeta", "1.0")
264
+ from gi.repository import Gst, GLib, GstApp, GstAnalytics, DLStreamerMeta
265
+
266
+ Gst.init([])
267
+
268
+ # Register DLStreamerMeta types so GstAnalytics iteration can handle them.
269
+ _ov = sys.modules["gi.overrides.GstAnalytics"]
270
+ _ov.__mtd_types__[DLStreamerMeta.ZoneMtd.get_mtd_type()] = DLStreamerMeta.relation_meta_get_zone_mtd
271
+
272
+ # Import OpenCV after Gst.init to avoid a GStreamer re-initialization conflict.
273
+ import cv2
274
+ import numpy as np
275
+
276
+ MODEL = "yolo26n_openvino_model/yolo26n.xml"
277
+ VIDEO = "worker-zone-detection.mp4"
278
+ DEVICE = "GPU" # change to "CPU" or "NPU" as needed
279
+
280
+ # Restricted zone traced from the yellow-and-black safety tape in the sample
281
+ # video (1920x1080), matching the OpenVINO sample. Edit these points to retrace
282
+ # the tape for a different camera.
283
+ RESTRICTED_ZONE = np.array([[0, 0], [1577, 0], [434, 1079], [0, 1080]], dtype=np.int32)
284
+ ZONE_JSON = json.dumps([{
285
+ "id": "restricted_zone",
286
+ "type": "polygon",
287
+ "points": [{"x": int(x), "y": int(y)} for x, y in RESTRICTED_ZONE],
288
+ }])
289
+
290
+ pipeline = Gst.parse_launch(
291
+ f"filesrc location={VIDEO} ! decodebin3 ! videoconvert ! "
292
+ f"gvadetect model={MODEL} device={DEVICE} threshold=0.4 ! queue ! "
293
+ f"gvatrack tracking-type=short-term-imageless ! queue ! "
294
+ f"gvaanalytics name=analytics ! queue ! "
295
+ f"videoconvert ! video/x-raw,format=BGR ! "
296
+ f"appsink name=sink emit-signals=true max-buffers=4 drop=false sync=false"
297
+ )
298
+ pipeline.get_by_name("analytics").set_property("zones", ZONE_JSON)
299
+
300
+ writer = {"w": None}
301
+ # Track IDs that have already triggered a breach event, so each intruder is
302
+ # reported only once.
303
+ flagged = set()
304
+
305
+
306
+ def on_sample(appsink):
307
+ sample = appsink.emit("pull-sample")
308
+ if sample is None:
309
+ return Gst.FlowReturn.OK
310
+ buf = sample.get_buffer()
311
+ caps = sample.get_caps().get_structure(0)
312
+ w = caps.get_value("width")
313
+ h = caps.get_value("height")
314
+ ok_fr, fr_n, fr_d = caps.get_fraction("framerate")
315
+ fps = (fr_n / fr_d) if (ok_fr and fr_d) else 30.0
316
+
317
+ ok, minfo = buf.map(Gst.MapFlags.READ)
318
+ if not ok:
319
+ return Gst.FlowReturn.OK
320
+ frame = np.ndarray((h, w, 3), buffer=minfo.data, dtype=np.uint8).copy()
321
+ buf.unmap(minfo)
322
+
323
+ now = buf.pts / Gst.SECOND if buf.pts != Gst.CLOCK_TIME_NONE else 0.0
324
+
325
+ # Shade the restricted zone and outline the tape boundary.
326
+ overlay = frame.copy()
327
+ cv2.fillPoly(overlay, [RESTRICTED_ZONE], (0, 0, 255))
328
+ cv2.addWeighted(overlay, 0.25, frame, 0.75, 0, frame)
329
+ cv2.polylines(frame, [RESTRICTED_ZONE], True, (0, 255, 255), 2)
330
+
331
+ breaches = 0
332
+ rmeta = GstAnalytics.buffer_get_analytics_relation_meta(buf)
333
+ if rmeta:
334
+ for od in rmeta.iter_on_type(GstAnalytics.ODMtd):
335
+ if GLib.quark_to_string(od.get_obj_type()) != "person":
336
+ continue
337
+ _, x, y, bw, bh, _ = od.get_location()
338
+
339
+ # Find the tracking ID via the direct relation.
340
+ track_id = None
341
+ for trk in od.iter_direct_related(GstAnalytics.RelTypes.RELATE_TO, GstAnalytics.TrackingMtd):
342
+ success, tid, *_ = trk.get_info()
343
+ if success:
344
+ track_id = tid
345
+ break
346
+
347
+ # gvaanalytics attaches a ZoneMtd relation when the person is in the zone.
348
+ in_zone = any(
349
+ True for _ in od.iter_direct_related(GstAnalytics.RelTypes.RELATE_TO, DLStreamerMeta.ZoneMtd)
350
+ )
351
+ color = (0, 0, 255) if in_zone else (0, 255, 0)
352
+ cv2.rectangle(frame, (int(x), int(y)), (int(x + bw), int(y + bh)), color, 3)
353
+ label = f"id {track_id}" if track_id is not None else "person"
354
+ if in_zone:
355
+ breaches += 1
356
+ cv2.putText(frame, f"BREACH {label}", (int(x), max(int(y) - 8, 14)),
357
+ cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
358
+ if track_id is not None and track_id not in flagged:
359
+ flagged.add(track_id)
360
+ print(f"PERIMETER BREACH id={track_id} t={now:.1f}s "
361
+ f"entered restricted zone at ({int(x + bw / 2)},{int(y + bh)})", flush=True)
362
+ else:
363
+ cv2.putText(frame, label, (int(x), max(int(y) - 8, 14)),
364
+ cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
365
+
366
+ cv2.putText(frame, f"Breaches: {breaches}", (10, 40),
367
+ cv2.FONT_HERSHEY_SIMPLEX, 1.1, (0, 0, 255), 3)
368
+ if writer["w"] is None:
369
+ writer["w"] = cv2.VideoWriter(
370
+ "output_dlstreamer.mp4", cv2.VideoWriter_fourcc(*"mp4v"), fps, (w, h)
371
+ )
372
+ writer["w"].write(frame)
373
+ return Gst.FlowReturn.OK
374
+
375
+
376
+ pipeline.get_by_name("sink").connect("new-sample", on_sample)
377
+ pipeline.set_state(Gst.State.PLAYING)
378
+ pipeline.get_bus().timed_pop_filtered(Gst.CLOCK_TIME_NONE, Gst.MessageType.EOS | Gst.MessageType.ERROR)
379
+ pipeline.set_state(Gst.State.NULL)
380
+ if writer["w"] is not None:
381
+ writer["w"].release()
382
+ ```
383
+
384
+ Expected output:
385
+
386
+ ```text
387
+ PERIMETER BREACH id=2 t=3.8s entered restricted zone at (799,1067)
388
+ PERIMETER BREACH id=8 t=13.7s entered restricted zone at (789,1069)
389
+ ...
390
+ ```
391
+
392
+ The annotated video is saved to `output_dlstreamer.mp4`.
393
+ It shows the restricted zone shaded in red with the tape boundary outlined, a green box around each person outside the zone, and a red `BREACH` box around anyone inside it -- matching the OpenVINO output.
394
+
395
+ #### Expected Output
396
+
397
+ ![DLStreamer expected output](expected_output_dlstreamer.gif)
398
+
399
+ **Device targets:**
400
+
401
+ - `DEVICE = "GPU"` -- default in the sample code.
402
+ - `DEVICE = "CPU"` -- change `DEVICE = "GPU"` to `DEVICE = "CPU"`.
403
+ - `DEVICE = "NPU"` -- change `DEVICE = "GPU"` to `DEVICE = "NPU"` for the Intel NPU.
404
+
405
+ ---
406
+
407
+ ## License
408
+
409
+ Licensed under the MIT License. See [LICENSE](LICENSE) for details.
410
+
411
+ ## References
412
+
413
+ - [YOLO26 Documentation](https://docs.ultralytics.com/models/yolo26/)
414
+ - [OpenVINO YOLO26 Notebook](https://github.com/openvinotoolkit/openvino_notebooks/blob/latest/notebooks/yolov26-optimization/yolov26-object-detection.ipynb)
415
+ - [OpenVINO Documentation](https://docs.openvino.ai/)
416
+ - [NNCF Post-Training Quantization](https://docs.openvino.ai/latest/nncf_ptq_introduction.html)
417
+ - [Intel DLStreamer](https://docs.openedgeplatform.intel.com/2026.0/edge-ai-libraries/dlstreamer/index.html)
expected_output_dlstreamer.gif ADDED

Git LFS Details

  • SHA256: 9c65eddcf28abf4457343a6c244fd4fa38022cf7410d10a701bd1979ba100c9f
  • Pointer size: 132 Bytes
  • Size of remote file: 3.32 MB
expected_output_openvino.gif ADDED

Git LFS Details

  • SHA256: 56a00dac01936e2c9e8d8a6e1347f58cbd37226b69267ef523e801b678116227
  • Pointer size: 132 Bytes
  • Size of remote file: 3.13 MB
export_and_quantize.sh ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # SPDX-License-Identifier: MIT
3
+ # Copyright (C) Intel Corporation
4
+ #
5
+ # Export a YOLO26 person detector for perimeter breach detection to OpenVINO IR.
6
+ # Usage: ./export_and_quantize.sh [MODEL_VARIANT] [PRECISION]
7
+ # Example: ./export_and_quantize.sh yolo26n FP16
8
+ #
9
+ # Supported precisions:
10
+ # FP32 -- Full-precision floating-point weights
11
+ # FP16 -- Half-precision floating-point weights (default)
12
+ # INT8 -- Quantized 8-bit integer weights (requires NNCF)
13
+ #
14
+ # Precision / device compatibility:
15
+ # | Precision | CPU | GPU | NPU |
16
+ # |-----------|-----|-----|-----|
17
+ # | FP32 | Yes | Yes | No |
18
+ # | FP16 | Yes | Yes | Yes |
19
+ # | INT8 | Yes | Yes | Yes |
20
+
21
+ set -euo pipefail
22
+
23
+ MODEL_NAME="${1:-yolo26n}"
24
+ PRECISION="${2:-FP16}"
25
+ PRECISION="$(echo "${PRECISION}" | tr '[:lower:]' '[:upper:]')"
26
+
27
+ if [[ "${PRECISION}" != "FP32" && "${PRECISION}" != "FP16" && "${PRECISION}" != "INT8" ]]; then
28
+ echo "ERROR: unsupported precision '${PRECISION}'. Choose FP32, FP16, or INT8." >&2
29
+ exit 1
30
+ fi
31
+
32
+ echo "--- Installing dependencies ---"
33
+ if [[ "${PRECISION}" == "INT8" ]]; then
34
+ pip install -qU openvino nncf ultralytics opencv-python
35
+ else
36
+ pip install -qU openvino ultralytics opencv-python
37
+ fi
38
+
39
+ # Ask for approval before downloading models and sample files
40
+ echo ""
41
+ echo "This script will download:"
42
+ echo " - Model weights and/or sample files"
43
+ echo ""
44
+ read -p "Continue with downloads? (yes/no): " APPROVAL
45
+ if [[ "${APPROVAL}" != "yes" ]]; then
46
+ echo "Download cancelled by user."
47
+ exit 0
48
+ fi
49
+
50
+ echo ""
51
+ echo "--- Downloading sample test video ---"
52
+ if [[ ! -f worker-zone-detection.mp4 ]]; then
53
+ wget -O worker-zone-detection.mp4 \
54
+ https://github.com/intel-iot-devkit/sample-videos/raw/master/worker-zone-detection.mp4
55
+ echo "Downloaded: worker-zone-detection.mp4"
56
+ else
57
+ echo "Already present: worker-zone-detection.mp4"
58
+ fi
59
+
60
+ if [[ "${PRECISION}" == "FP32" ]]; then
61
+ HALF_FLAG="False"
62
+ EXPORT_LABEL="FP32"
63
+ else
64
+ HALF_FLAG="True"
65
+ EXPORT_LABEL="FP16"
66
+ fi
67
+
68
+ echo "--- Exporting ${MODEL_NAME} to OpenVINO IR (${EXPORT_LABEL}) ---"
69
+ python3 -c "
70
+ from ultralytics import YOLO
71
+
72
+ model = YOLO('${MODEL_NAME}.pt')
73
+ model.export(format='openvino', half=${HALF_FLAG}, dynamic=False, imgsz=640)
74
+ print('Export complete: ${MODEL_NAME}_openvino_model/')
75
+ "
76
+
77
+ if [[ "${PRECISION}" == "INT8" ]]; then
78
+ echo "--- Quantizing to INT8 with NNCF ---"
79
+ python3 -c "
80
+ import nncf
81
+ import openvino as ov
82
+ import numpy as np
83
+ import cv2
84
+
85
+ core = ov.Core()
86
+ model = core.read_model('${MODEL_NAME}_openvino_model/${MODEL_NAME}.xml')
87
+
88
+ # Extract frames from the sample video for calibration.
89
+ cap = cv2.VideoCapture('worker-zone-detection.mp4')
90
+ frames = []
91
+ while len(frames) < 300:
92
+ ret, frame = cap.read()
93
+ if not ret:
94
+ cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
95
+ continue
96
+ img = cv2.resize(frame, (640, 640))
97
+ img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
98
+ img = img.transpose(2, 0, 1)[np.newaxis, ...]
99
+ frames.append(img)
100
+ cap.release()
101
+
102
+ def transform_fn(data_item):
103
+ return frames[data_item % len(frames)]
104
+
105
+ calibration_dataset = nncf.Dataset(list(range(300)), transform_fn)
106
+
107
+ quantized = nncf.quantize(
108
+ model,
109
+ calibration_dataset,
110
+ preset=nncf.QuantizationPreset.MIXED,
111
+ subset_size=300,
112
+ )
113
+
114
+ ov.save_model(quantized, '${MODEL_NAME}_perimeter_int8.xml')
115
+ print('Quantization complete: ${MODEL_NAME}_perimeter_int8.xml')
116
+ "
117
+ fi
118
+ echo "--- Done ---"