PyNvVideoCodec 2.2.1__cp314-cp314-win_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. PyNvVideoCodec/PyNvVideoCodec_121.cp314-win_arm64.pyd +0 -0
  2. PyNvVideoCodec/PyNvVideoCodec_130.cp314-win_arm64.pyd +0 -0
  3. PyNvVideoCodec/VersionCheck.cp314-win_arm64.pyd +0 -0
  4. PyNvVideoCodec/__init__.py +251 -0
  5. PyNvVideoCodec/avformat-62.dll +0 -0
  6. PyNvVideoCodec/avutil-60.dll +0 -0
  7. PyNvVideoCodec/decoders/SimpleDecoder.py +273 -0
  8. PyNvVideoCodec/decoders/ThreadedDecoder.py +129 -0
  9. PyNvVideoCodec/decoders/__init__.py +22 -0
  10. PyNvVideoCodec/transcoder/Transcoder.py +55 -0
  11. PyNvVideoCodec/transcoder/__init__.py +22 -0
  12. benchmarks/cached_decoder_benchmark.py +813 -0
  13. benchmarks/frame_sampling_benchmark.py +765 -0
  14. benchmarks/requirements.txt +52 -0
  15. benchmarks/segmented_transcode_benchmark.py +1546 -0
  16. benchmarks/setup_pycuda_win_arm64.bat +531 -0
  17. pynvvideocodec-2.2.1.data/data/external/ffmpeg/readme.txt +39 -0
  18. pynvvideocodec-2.2.1.data/data/external/ffmpeg/src/ffmpeg-8.1.2.tar.xz +0 -0
  19. pynvvideocodec-2.2.1.dist-info/METADATA +97 -0
  20. pynvvideocodec-2.2.1.dist-info/RECORD +63 -0
  21. pynvvideocodec-2.2.1.dist-info/WHEEL +5 -0
  22. pynvvideocodec-2.2.1.dist-info/licenses/LICENSE.txt +22 -0
  23. pynvvideocodec-2.2.1.dist-info/licenses/NOTICES.txt +167 -0
  24. pynvvideocodec-2.2.1.dist-info/top_level.txt +3 -0
  25. samples/advanced/decode.py +173 -0
  26. samples/advanced/decode_from_memory_buffer.py +204 -0
  27. samples/advanced/decode_perf.py +299 -0
  28. samples/advanced/decode_reconfigure.py +184 -0
  29. samples/advanced/decode_sei_msg.py +193 -0
  30. samples/advanced/decode_with_cuda_control.py +214 -0
  31. samples/advanced/decode_with_low_latency.py +183 -0
  32. samples/advanced/encode_config.json +12 -0
  33. samples/advanced/encode_config_lowlatency.json +13 -0
  34. samples/advanced/encode_perf.py +338 -0
  35. samples/advanced/encode_reconfigure.py +211 -0
  36. samples/advanced/encode_sei_msg.py +188 -0
  37. samples/advanced/simple_decode_from_memory.py +151 -0
  38. samples/advanced/simple_decode_stats.py +179 -0
  39. samples/advanced/transcode.py +288 -0
  40. samples/basic/create_video_segments.py +187 -0
  41. samples/basic/encode.py +574 -0
  42. samples/basic/segments.txt +3 -0
  43. samples/basic/simple_decode_sampling.py +182 -0
  44. samples/basic/transcode_config.json +6 -0
  45. samples/jupyter/ReconfigureDecoder.png +0 -0
  46. samples/jupyter/SimpleDecoderAPIs.png +0 -0
  47. samples/jupyter/ThreadedDecoder.png +0 -0
  48. samples/jupyter/coco_classes.pickle +0 -0
  49. samples/jupyter/face_protect_tutorial.ipynb +881 -0
  50. samples/jupyter/object_detection_tutorial.ipynb +781 -0
  51. samples/jupyter/simple_decode_tutorial.ipynb +674 -0
  52. samples/requirements.txt +84 -0
  53. samples/setup_pycuda_win_arm64.bat +531 -0
  54. samples/utils/Utils.py +405 -0
  55. samples/utils/__init__.py +36 -0
  56. samples/utils/decode_parser.py +642 -0
  57. samples/utils/decode_stats_writer.py +177 -0
  58. samples/utils/encode_parallel_utils.py +174 -0
  59. samples/utils/encode_parser.py +379 -0
  60. samples/utils/frame_utils.py +89 -0
  61. samples/utils/sei_message_parser.py +223 -0
  62. samples/utils/sei_parser.py +223 -0
  63. samples/utils/transcode_parser.py +218 -0
@@ -0,0 +1,881 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "<!--\n",
8
+ "This copyright notice applies to this file only\n",
9
+ "\n",
10
+ "SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n",
11
+ "SPDX-License-Identifier: MIT\n",
12
+ "\n",
13
+ "Permission is hereby granted, free of charge, to any person obtaining a\n",
14
+ "copy of this software and associated documentation files (the \"Software\"),\n",
15
+ "to deal in the Software without restriction, including without limitation\n",
16
+ "the rights to use, copy, modify, merge, publish, distribute, sublicense,\n",
17
+ "and/or sell copies of the Software, and to permit persons to whom the\n",
18
+ "Software is furnished to do so, subject to the following conditions:\n",
19
+ "\n",
20
+ "The above copyright notice and this permission notice shall be included in\n",
21
+ "all copies or substantial portions of the Software.\n",
22
+ "\n",
23
+ "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n",
24
+ "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n",
25
+ "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL\n",
26
+ "THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n",
27
+ "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n",
28
+ "FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n",
29
+ "DEALINGS IN THE SOFTWARE.\n",
30
+ "-->"
31
+ ]
32
+ },
33
+ {
34
+ "cell_type": "markdown",
35
+ "metadata": {},
36
+ "source": [
37
+ "# Face De-identification (Blur) using PyNvVideoCodec + RetinaFace\n",
38
+ "\n",
39
+ "This tutorial demonstrates a face de-identification pipeline that detects faces\n",
40
+ "with **RetinaFace-ResNet34** and blurs them in real time. Decoded frames are\n",
41
+ "displayed inline with Matplotlib.\n",
42
+ "\n",
43
+ "The detector backend is platform-specific:\n",
44
+ "\n",
45
+ "- Windows ARM64 uses ONNX Runtime with the ONNX model directly.\n",
46
+ "- Other platforms keep the original TensorRT FP16 engine flow.\n",
47
+ "\n",
48
+ "## Pipeline Overview\n",
49
+ "\n",
50
+ "1. **Decode** - `ThreadedDecoder` decodes the input MP4 into RGB frames on the GPU\n",
51
+ "2. **Detect** - RetinaFace detects faces with the platform-selected backend\n",
52
+ "3. **Blur** - Gaussian blur is applied to every detected face region\n",
53
+ "4. **Display** - The blurred frame is shown inline (no output file written)\n",
54
+ "\n",
55
+ "### Prerequisites\n",
56
+ "\n",
57
+ "| Dependency | Purpose |\n",
58
+ "|---|---|\n",
59
+ "| PyNvVideoCodec 2.x | GPU video decode via `ThreadedDecoder` |\n",
60
+ "| TensorRT >= 8.6 | RetinaFace FP16 inference on non-Windows ARM64 platforms |\n",
61
+ "| ONNX Runtime | RetinaFace ONNX inference on Windows ARM64 |\n",
62
+ "| PyCUDA | CUDA memory management |\n",
63
+ "| PyTorch >= 2.0 | DLPack to tensor conversion for decoded frames |\n",
64
+ "| torchvision >= 0.15 | GPU Gaussian blur (`F.gaussian_blur`) |\n",
65
+ "| OpenCV | Letterbox resize and RGB/BGR conversion on non-Windows ARM64 platforms |\n",
66
+ "| Pillow | Letterbox resize on Windows ARM64 |\n",
67
+ "| NumPy, Matplotlib | Array ops and inline display |\n",
68
+ "\n",
69
+ "> Windows ARM64 uses ONNX Runtime and Pillow because compatible TensorRT and OpenCV pip wheels are not available for that environment. Other platforms keep TensorRT and OpenCV to preserve the original accelerated sample flow."
70
+ ]
71
+ },
72
+ {
73
+ "cell_type": "markdown",
74
+ "metadata": {},
75
+ "source": [
76
+ "## 1. Install Dependencies"
77
+ ]
78
+ },
79
+ {
80
+ "cell_type": "code",
81
+ "execution_count": null,
82
+ "metadata": {},
83
+ "outputs": [],
84
+ "source": [
85
+ "import platform, subprocess, sys\n",
86
+ "\n",
87
+ "def _is_windows_arm64():\n",
88
+ " return sys.platform == \"win32\" and platform.machine().upper() == \"ARM64\"\n",
89
+ "\n",
90
+ "NVTORCH_WINDOWS_ARM64_INDEX = \"https://pypi.nvidia.com/nvtorch_oot_nightly\"\n",
91
+ "TORCH_WINDOWS_ARM64_PACKAGE = \"torch==2.15.0.dev20260820+cu134\"\n",
92
+ "TORCHVISION_WINDOWS_ARM64_PACKAGE = \"torchvision==0.30.0.dev20260820+cu134\"\n",
93
+ "\n",
94
+ "def _pip_install(*packages):\n",
95
+ " subprocess.check_call([sys.executable, \"-m\", \"pip\", \"install\", *packages])\n",
96
+ "\n",
97
+ "def _install_windows_arm64_torch(*packages):\n",
98
+ " if not ((3, 11) <= sys.version_info[:2] < (3, 15)):\n",
99
+ " raise RuntimeError(\"Windows ARM64 Torch wheels are available for CPython 3.11 through 3.14. Re-run this notebook with a supported kernel.\")\n",
100
+ " _pip_install(\"--extra-index-url\", NVTORCH_WINDOWS_ARM64_INDEX, *packages)\n",
101
+ "\n",
102
+ "# Lightweight CPU-side deps\n",
103
+ "if _is_windows_arm64():\n",
104
+ " _pip_install(\"matplotlib\", \"numpy\", \"onnxruntime\", \"pillow\", \"requests\")\n",
105
+ "else:\n",
106
+ " _pip_install(\"matplotlib\", \"numpy\", \"opencv-python\", \"requests\")\n",
107
+ "\n",
108
+ "if _is_windows_arm64():\n",
109
+ " print(\"Skipping PyCUDA install on Windows ARM64; use the native Windows ARM64 CUDA wheel.\")\n",
110
+ "else:\n",
111
+ " _pip_install(\"pycuda\")\n",
112
+ "\n",
113
+ "# PyTorch + torchvision (used for DLPack frame conversion and GPU Gaussian blur).\n",
114
+ "# Pick the wheel that matches your CUDA runtime:\n",
115
+ "#\n",
116
+ "# CUDA 12.1 : %pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121\n",
117
+ "# CUDA 12.6 : %pip install torch torchvision --index-url https://download.pytorch.org/whl/cu126\n",
118
+ "#\n",
119
+ "# On Jetson / Tegra (Thor): use the torch/torchvision shipped with JetPack.\n",
120
+ "# On Windows ARM64: use NVIDIA's ARM CUDA wheels from nvtorch_oot_nightly.\n",
121
+ "if _is_windows_arm64():\n",
122
+ " _install_windows_arm64_torch(TORCH_WINDOWS_ARM64_PACKAGE, TORCHVISION_WINDOWS_ARM64_PACKAGE)\n",
123
+ "else:\n",
124
+ " _pip_install(\"torch\", \"torchvision\")\n",
125
+ "\n",
126
+ "# Face detector backend\n",
127
+ "if _is_windows_arm64():\n",
128
+ " print(\"Using ONNX Runtime for face detection on Windows ARM64; skipping TensorRT pip install.\")\n",
129
+ "else:\n",
130
+ " import torch\n",
131
+ "\n",
132
+ " cuda_version = (torch.version.cuda or \"\")\n",
133
+ " if cuda_version.startswith(\"12\"):\n",
134
+ " tensorrt_pkg = \"tensorrt-cu12>=10.16.1,<10.17.0\"\n",
135
+ " elif cuda_version.startswith(\"13\"):\n",
136
+ " tensorrt_pkg = \"tensorrt-cu13>=10.16.1,<10.17.0\"\n",
137
+ " else:\n",
138
+ " raise RuntimeError(\n",
139
+ " f\"Unsupported / unknown torch CUDA version: {cuda_version!r}. \"\n",
140
+ " \"Install a CUDA 12.x or 13.x build of torch, then re-run this cell.\"\n",
141
+ " )\n",
142
+ "\n",
143
+ " print(f\"Installing {tensorrt_pkg} (torch CUDA {cuda_version})\")\n",
144
+ " _pip_install(tensorrt_pkg)"
145
+ ]
146
+ },
147
+ {
148
+ "cell_type": "markdown",
149
+ "metadata": {},
150
+ "source": [
151
+ "## 2. Imports and CUDA Initialization"
152
+ ]
153
+ },
154
+ {
155
+ "cell_type": "code",
156
+ "execution_count": null,
157
+ "metadata": {},
158
+ "outputs": [],
159
+ "source": [
160
+ "from PyNvVideoCodec import ThreadedDecoder, OutputColorType\n",
161
+ "import pycuda.driver as cuda\n",
162
+ "from pycuda.autoinit import context\n",
163
+ "import numpy as np\n",
164
+ "from pathlib import Path\n",
165
+ "\n",
166
+ "if _is_windows_arm64():\n",
167
+ " import onnxruntime as ort\n",
168
+ " from PIL import Image\n",
169
+ "else:\n",
170
+ " import cv2\n",
171
+ " import tensorrt as trt\n",
172
+ "\n",
173
+ "import torch\n",
174
+ "import torchvision.transforms.functional as TF\n",
175
+ "\n",
176
+ "import matplotlib.pyplot as plt\n",
177
+ "from IPython.display import clear_output\n",
178
+ "%matplotlib inline\n",
179
+ "\n",
180
+ "_BLUR_DEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
181
+ "\n",
182
+ "if _is_windows_arm64():\n",
183
+ " print(f\"ONNX Runtime: {ort.__version__}\")\n",
184
+ "else:\n",
185
+ " print(f\"TensorRT : {trt.__version__}\")\n",
186
+ "print(f\"PyCUDA : device {cuda.Device(0).name()}\")\n",
187
+ "print(f\"Torch : {torch.__version__} (blur device: {_BLUR_DEVICE})\")"
188
+ ]
189
+ },
190
+ {
191
+ "cell_type": "markdown",
192
+ "metadata": {},
193
+ "source": [
194
+ "## 3. Download ONNX Model & Prepare Face Detector\n",
195
+ "\n",
196
+ "This cell downloads the **RetinaFace-ResNet34** ONNX model (~84 MB) from\n",
197
+ "[yakhyo/retinaface-pytorch](https://github.com/yakhyo/retinaface-pytorch)\n",
198
+ "(MIT License). A SHA-256 check is performed as a warning only (the upstream\n",
199
+ "model may be updated).\n",
200
+ "\n",
201
+ "- Windows ARM64 loads this ONNX model directly with ONNX Runtime.\n",
202
+ "- Other platforms build or reuse a TensorRT FP16 engine from the same ONNX model.\n",
203
+ "- If the ONNX file already exists and passes the hash check, the download is skipped.\n",
204
+ "- If the TensorRT engine already exists it is reused on non-Windows ARM64 platforms (set `FORCE_REBUILD = True` to override)."
205
+ ]
206
+ },
207
+ {
208
+ "cell_type": "code",
209
+ "execution_count": null,
210
+ "metadata": {},
211
+ "outputs": [],
212
+ "source": [
213
+ "import hashlib\n",
214
+ "import tempfile\n",
215
+ "import requests\n",
216
+ "from pathlib import Path\n",
217
+ "\n",
218
+ "# ----- Paths and constants -----\n",
219
+ "MODELS_DIR = Path(\"models\")\n",
220
+ "ONNX_URL = (\"https://github.com/yakhyo/retinaface-pytorch/releases/\"\n",
221
+ " \"download/v0.0.1/retinaface_r34.onnx\")\n",
222
+ "ONNX_SHA256 = \"df36df61476e043603925e70df0e1a90b0ef2200e5e1c069810b5dbff4645dc6\"\n",
223
+ "ONNX_PATH = MODELS_DIR / \"retinaface_r34.onnx\"\n",
224
+ "ENGINE_PATH = Path(\"retinaface_r34.engine\")\n",
225
+ "INPUT_SHAPE = (1, 3, 960, 960)\n",
226
+ "\n",
227
+ "FORCE_REBUILD = False\n",
228
+ "\n",
229
+ "\n",
230
+ "def _sha256(path):\n",
231
+ " h = hashlib.sha256()\n",
232
+ " with open(path, \"rb\") as f:\n",
233
+ " for chunk in iter(lambda: f.read(1 << 20), b\"\"):\n",
234
+ " h.update(chunk)\n",
235
+ " return h.hexdigest()\n",
236
+ "\n",
237
+ "\n",
238
+ "def download_onnx(url, dest, expected_sha256):\n",
239
+ " \"\"\"Download ONNX with SHA-256 verification (atomic write via requests).\"\"\"\n",
240
+ " if dest.is_file():\n",
241
+ " size_mb = dest.stat().st_size / (1024 * 1024)\n",
242
+ " if expected_sha256:\n",
243
+ " actual = _sha256(dest)\n",
244
+ " if actual == expected_sha256:\n",
245
+ " print(f\"ONNX already exists and verified: {dest} ({size_mb:.1f} MB)\")\n",
246
+ " else:\n",
247
+ " print(f\"ONNX exists ({size_mb:.1f} MB) - SHA-256 differs from expected (OK if upstream updated)\")\n",
248
+ " else:\n",
249
+ " print(f\"ONNX already exists: {dest} ({size_mb:.1f} MB)\")\n",
250
+ " return\n",
251
+ "\n",
252
+ " dest.parent.mkdir(parents=True, exist_ok=True)\n",
253
+ " print(f\"Downloading RetinaFace-ResNet34 ONNX from:\\n {url}\")\n",
254
+ "\n",
255
+ " headers = {\n",
256
+ " \"User-Agent\": (\n",
257
+ " \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) \"\n",
258
+ " \"AppleWebKit/537.36 (KHTML, like Gecko) \"\n",
259
+ " \"Chrome/124.0.0.0 Safari/537.36\"\n",
260
+ " ),\n",
261
+ " \"Accept\": \"*/*\",\n",
262
+ " }\n",
263
+ "\n",
264
+ " tmp_fd, tmp_path = tempfile.mkstemp(dir=dest.parent, suffix=\".onnx.part\")\n",
265
+ " tmp = Path(tmp_path)\n",
266
+ "\n",
267
+ " try:\n",
268
+ " with requests.get(url, headers=headers, stream=True, timeout=120) as resp:\n",
269
+ " resp.raise_for_status()\n",
270
+ " total = int(resp.headers.get(\"Content-Length\", 0))\n",
271
+ " downloaded = 0\n",
272
+ " with open(tmp_fd, \"wb\") as f:\n",
273
+ " for chunk in resp.iter_content(chunk_size=1 << 20):\n",
274
+ " if not chunk:\n",
275
+ " continue\n",
276
+ " f.write(chunk)\n",
277
+ " downloaded += len(chunk)\n",
278
+ " if total > 0:\n",
279
+ " pct = downloaded / total * 100\n",
280
+ " print(f\"\\r {downloaded / (1024*1024):.1f}/\"\n",
281
+ " f\"{total / (1024*1024):.1f} MB ({pct:.0f}%)\",\n",
282
+ " end=\"\", flush=True)\n",
283
+ " print()\n",
284
+ "\n",
285
+ " if expected_sha256:\n",
286
+ " actual = _sha256(tmp)\n",
287
+ " if actual != expected_sha256:\n",
288
+ " print(f\"WARNING: SHA-256 mismatch (upstream model may have been updated)\\n\"\n",
289
+ " f\" expected: {expected_sha256}\\n\"\n",
290
+ " f\" got: {actual}\")\n",
291
+ " tmp.replace(dest)\n",
292
+ " except BaseException:\n",
293
+ " tmp.unlink(missing_ok=True)\n",
294
+ " raise\n",
295
+ "\n",
296
+ " print(f\"Saved: {dest} ({dest.stat().st_size / (1024*1024):.1f} MB)\")\n",
297
+ "\n",
298
+ "\n",
299
+ "def build_trt_engine(onnx_path, engine_path):\n",
300
+ " \"\"\"Parse ONNX and build a TensorRT FP16 engine.\"\"\"\n",
301
+ " logger = trt.Logger(trt.Logger.INFO)\n",
302
+ " builder = trt.Builder(logger)\n",
303
+ " network = builder.create_network(\n",
304
+ " 1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))\n",
305
+ " parser = trt.OnnxParser(network, logger)\n",
306
+ "\n",
307
+ " with open(onnx_path, \"rb\") as f:\n",
308
+ " if not parser.parse(f.read()):\n",
309
+ " errors = [str(parser.get_error(i)) for i in range(parser.num_errors)]\n",
310
+ " raise RuntimeError(\"ONNX parse failed:\\n \" + \"\\n \".join(errors))\n",
311
+ "\n",
312
+ " print(f\"Model: {onnx_path.name}\")\n",
313
+ " for i in range(network.num_inputs):\n",
314
+ " t = network.get_input(i)\n",
315
+ " print(f\" Input [{i}] {t.name}: shape={t.shape} dtype={t.dtype}\")\n",
316
+ " for i in range(network.num_outputs):\n",
317
+ " t = network.get_output(i)\n",
318
+ " print(f\" Output [{i}] {t.name}: shape={t.shape} dtype={t.dtype}\")\n",
319
+ "\n",
320
+ " config = builder.create_builder_config()\n",
321
+ " config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 << 30)\n",
322
+ "\n",
323
+ " if builder.platform_has_fast_fp16:\n",
324
+ " config.set_flag(trt.BuilderFlag.FP16)\n",
325
+ " print(\"FP16 precision enabled\")\n",
326
+ "\n",
327
+ " inp = network.get_input(0)\n",
328
+ " if any(d == -1 for d in inp.shape):\n",
329
+ " print(f\"Dynamic input - setting profile to {INPUT_SHAPE}\")\n",
330
+ " profile = builder.create_optimization_profile()\n",
331
+ " profile.set_shape(inp.name, INPUT_SHAPE, INPUT_SHAPE, INPUT_SHAPE)\n",
332
+ " config.add_optimization_profile(profile)\n",
333
+ "\n",
334
+ " print(\"Building TensorRT engine (this may take a minute) ...\")\n",
335
+ " serialised = builder.build_serialized_network(network, config)\n",
336
+ " if serialised is None:\n",
337
+ " raise RuntimeError(\"TensorRT engine build failed\")\n",
338
+ "\n",
339
+ " engine_path.parent.mkdir(parents=True, exist_ok=True)\n",
340
+ " with open(engine_path, \"wb\") as f:\n",
341
+ " f.write(serialised)\n",
342
+ "\n",
343
+ " print(f\"Engine saved: {engine_path} \"\n",
344
+ " f\"({engine_path.stat().st_size / (1024*1024):.1f} MB)\")\n",
345
+ "\n",
346
+ "\n",
347
+ "# ----- Run: download + backend preparation -----\n",
348
+ "download_onnx(ONNX_URL, ONNX_PATH, ONNX_SHA256)\n",
349
+ "\n",
350
+ "if _is_windows_arm64():\n",
351
+ " print(\"Using ONNX Runtime on Windows ARM64; TensorRT engine build is skipped.\")\n",
352
+ "elif ENGINE_PATH.is_file() and not FORCE_REBUILD:\n",
353
+ " size_mb = ENGINE_PATH.stat().st_size / (1024 * 1024)\n",
354
+ " print(f\"Engine already exists: {ENGINE_PATH} ({size_mb:.1f} MB) \"\n",
355
+ " f\"- set FORCE_REBUILD=True to rebuild\")\n",
356
+ "else:\n",
357
+ " build_trt_engine(ONNX_PATH, ENGINE_PATH)\n",
358
+ "\n",
359
+ "print(\"\\nDone!\")"
360
+ ]
361
+ },
362
+ {
363
+ "cell_type": "markdown",
364
+ "metadata": {},
365
+ "source": [
366
+ "## 4. Configuration\n",
367
+ "\n",
368
+ "Set paths and tuning knobs here. The engine was built in the previous step."
369
+ ]
370
+ },
371
+ {
372
+ "cell_type": "code",
373
+ "execution_count": null,
374
+ "metadata": {},
375
+ "outputs": [],
376
+ "source": [
377
+ "VIDEO_URL = \"https://videos.pexels.com/video-files/19527870/19527870-hd_1920_1080_24fps.mp4\"\n",
378
+ "INPUT_VIDEO = \"test_faces.mp4\"\n",
379
+ "\n",
380
+ "if not Path(INPUT_VIDEO).is_file():\n",
381
+ " import requests\n",
382
+ " print(f\"Downloading sample video from:\\n {VIDEO_URL}\")\n",
383
+ " with requests.get(VIDEO_URL, stream=True, timeout=120) as r:\n",
384
+ " r.raise_for_status()\n",
385
+ " with open(INPUT_VIDEO, \"wb\") as f:\n",
386
+ " for chunk in r.iter_content(chunk_size=8192):\n",
387
+ " if chunk:\n",
388
+ " f.write(chunk)\n",
389
+ " print(f\"Saved: {INPUT_VIDEO} \"\n",
390
+ " f\"({Path(INPUT_VIDEO).stat().st_size / (1024*1024):.1f} MB)\")\n",
391
+ "else:\n",
392
+ " print(f\"Using existing video: {INPUT_VIDEO}\")\n",
393
+ "\n",
394
+ "RETINAFACE_INPUT_SIZE = 960\n",
395
+ "\n",
396
+ "CONF_THRESH = 0.4\n",
397
+ "NMS_IOU = 0.4\n",
398
+ "BBOX_SCALE = 1.0\n",
399
+ "PRE_NMS_TOPK = 5000\n",
400
+ "\n",
401
+ "BLUR_KSIZE = 151\n",
402
+ "BLUR_SIGMA = 45\n",
403
+ "\n",
404
+ "DISPLAY_EVERY_N = 5 # show every Nth frame (1 = show all)\n",
405
+ "MAX_FRAMES = 200 # stop after N frames (0 = all)\n",
406
+ "\n",
407
+ "assert Path(INPUT_VIDEO).is_file(), f\"Input video not found: {INPUT_VIDEO}\"\n",
408
+ "assert ONNX_PATH.is_file(), f\"ONNX model not found: {ONNX_PATH} - run step 3 above\"\n",
409
+ "if not _is_windows_arm64():\n",
410
+ " assert ENGINE_PATH.is_file(), f\"Engine not found: {ENGINE_PATH} - run step 3 above\""
411
+ ]
412
+ },
413
+ {
414
+ "cell_type": "markdown",
415
+ "metadata": {},
416
+ "source": [
417
+ "## 5. RetinaFace Utilities\n",
418
+ "\n",
419
+ "Self-contained: letterbox resize, prior-box generation, bbox decode, NMS,\n",
420
+ "and the full post-processing pipeline. No imports from the main project."
421
+ ]
422
+ },
423
+ {
424
+ "cell_type": "code",
425
+ "execution_count": null,
426
+ "metadata": {},
427
+ "outputs": [],
428
+ "source": [
429
+ "# ----- RetinaFace constants -----\n",
430
+ "MIN_SIZES = [[16, 32], [64, 128], [256, 512]]\n",
431
+ "STRIDES = [8, 16, 32]\n",
432
+ "VARIANCE = (0.1, 0.2)\n",
433
+ "BGR_MEAN = np.array([104.0, 117.0, 123.0], dtype=np.float32)\n",
434
+ "\n",
435
+ "\n",
436
+ "def letterbox(image, target_size, pad_val=0):\n",
437
+ " \"\"\"Resize preserving aspect ratio with symmetric zero-padding.\"\"\"\n",
438
+ " h_src, w_src = image.shape[:2]\n",
439
+ " scale = min(target_size / w_src, target_size / h_src)\n",
440
+ " new_w, new_h = int(w_src * scale), int(h_src * scale)\n",
441
+ " if _is_windows_arm64():\n",
442
+ " resized = np.asarray(Image.fromarray(image).resize((new_w, new_h), Image.Resampling.BILINEAR))\n",
443
+ " else:\n",
444
+ " resized = cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_LINEAR)\n",
445
+ " dx = (target_size - new_w) // 2\n",
446
+ " dy = (target_size - new_h) // 2\n",
447
+ " canvas = np.full((target_size, target_size, image.shape[2]), pad_val, dtype=np.uint8)\n",
448
+ " canvas[dy:dy + new_h, dx:dx + new_w, :] = resized\n",
449
+ " return canvas, scale, dx, dy\n",
450
+ "\n",
451
+ "\n",
452
+ "def generate_prior_boxes(input_size):\n",
453
+ " \"\"\"Generate RetinaFace anchors in normalised [cx, cy, w, h].\"\"\"\n",
454
+ " anchors = []\n",
455
+ " for k, step in enumerate(STRIDES):\n",
456
+ " fmap = int(np.ceil(input_size / step))\n",
457
+ " for i in range(fmap):\n",
458
+ " for j in range(fmap):\n",
459
+ " for min_size in MIN_SIZES[k]:\n",
460
+ " s = min_size / input_size\n",
461
+ " cx = (j + 0.5) * step / input_size\n",
462
+ " cy = (i + 0.5) * step / input_size\n",
463
+ " anchors.append([cx, cy, s, s])\n",
464
+ " return np.array(anchors, dtype=np.float32)\n",
465
+ "\n",
466
+ "\n",
467
+ "def decode_boxes(loc, priors):\n",
468
+ " \"\"\"Decode regression deltas into [x1, y1, x2, y2].\"\"\"\n",
469
+ " cxcy = priors[:, :2] + loc[:, :2] * VARIANCE[0] * priors[:, 2:]\n",
470
+ " wh = priors[:, 2:] * np.exp(np.clip(loc[:, 2:] * VARIANCE[1], -50, 50))\n",
471
+ " boxes = np.empty_like(loc)\n",
472
+ " boxes[:, :2] = cxcy - wh / 2\n",
473
+ " boxes[:, 2:] = cxcy + wh / 2\n",
474
+ " return boxes\n",
475
+ "\n",
476
+ "\n",
477
+ "def _compute_iou(box, boxes):\n",
478
+ " x1 = np.maximum(box[0], boxes[:, 0])\n",
479
+ " y1 = np.maximum(box[1], boxes[:, 1])\n",
480
+ " x2 = np.minimum(box[2], boxes[:, 2])\n",
481
+ " y2 = np.minimum(box[3], boxes[:, 3])\n",
482
+ " inter = np.maximum(0, x2 - x1) * np.maximum(0, y2 - y1)\n",
483
+ " area_box = (box[2] - box[0]) * (box[3] - box[1])\n",
484
+ " area_boxes = (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1])\n",
485
+ " return inter / np.maximum(area_box + area_boxes - inter, 1e-6)\n",
486
+ "\n",
487
+ "\n",
488
+ "def nms(boxes, scores, iou_threshold):\n",
489
+ " \"\"\"Greedy NMS with center-distance merge.\"\"\"\n",
490
+ " if len(boxes) == 0:\n",
491
+ " return np.array([], dtype=int)\n",
492
+ " order = scores.argsort()[::-1]\n",
493
+ " keep = []\n",
494
+ " while len(order) > 0:\n",
495
+ " idx = order[0]\n",
496
+ " keep.append(idx)\n",
497
+ " if len(order) == 1:\n",
498
+ " break\n",
499
+ " ious = _compute_iou(boxes[idx], boxes[order[1:]])\n",
500
+ " order = order[np.where(ious <= iou_threshold)[0] + 1]\n",
501
+ "\n",
502
+ " keep_arr = np.array(keep, dtype=int)\n",
503
+ " kept = boxes[keep_arr]\n",
504
+ " cx = (kept[:, 0] + kept[:, 2]) / 2\n",
505
+ " cy = (kept[:, 1] + kept[:, 3]) / 2\n",
506
+ " w = kept[:, 2] - kept[:, 0]\n",
507
+ " h = kept[:, 3] - kept[:, 1]\n",
508
+ " final, suppressed = [], np.zeros(len(keep_arr), dtype=bool)\n",
509
+ " for i in range(len(keep_arr)):\n",
510
+ " if suppressed[i]:\n",
511
+ " continue\n",
512
+ " final.append(keep_arr[i])\n",
513
+ " for j in range(i + 1, len(keep_arr)):\n",
514
+ " if not suppressed[j]:\n",
515
+ " if abs(cx[j] - cx[i]) < max(w[i], w[j]) and abs(cy[j] - cy[i]) < max(h[i], h[j]):\n",
516
+ " suppressed[j] = True\n",
517
+ " return np.array(final, dtype=int)\n",
518
+ "\n",
519
+ "\n",
520
+ "def postprocess_detections(loc, conf, priors, input_size, scale, dx, dy,\n",
521
+ " conf_thresh, nms_iou, bbox_scale):\n",
522
+ " \"\"\"Decode + threshold + NMS + reverse-letterbox.\"\"\"\n",
523
+ " boxes = decode_boxes(loc, priors) * input_size\n",
524
+ " scores = conf[:, 1]\n",
525
+ "\n",
526
+ " mask = scores >= conf_thresh\n",
527
+ " boxes, scores = boxes[mask], scores[mask]\n",
528
+ " if len(boxes) == 0:\n",
529
+ " return np.empty((0, 4), dtype=np.float32)\n",
530
+ "\n",
531
+ " order = scores.argsort()[::-1][:PRE_NMS_TOPK]\n",
532
+ " boxes, scores = boxes[order], scores[order]\n",
533
+ "\n",
534
+ " keep = nms(boxes, scores, iou_threshold=nms_iou)\n",
535
+ " boxes = boxes[keep]\n",
536
+ "\n",
537
+ " # Reverse letterbox\n",
538
+ " out = boxes.copy()\n",
539
+ " out[:, [0, 2]] = (out[:, [0, 2]] - dx) / scale\n",
540
+ " out[:, [1, 3]] = (out[:, [1, 3]] - dy) / scale\n",
541
+ "\n",
542
+ " # Enlarge for blur coverage\n",
543
+ " cx = (out[:, 0] + out[:, 2]) / 2\n",
544
+ " cy = (out[:, 1] + out[:, 3]) / 2\n",
545
+ " w = (out[:, 2] - out[:, 0]) * bbox_scale\n",
546
+ " h = (out[:, 3] - out[:, 1]) * bbox_scale\n",
547
+ " out[:, 0] = cx - w / 2\n",
548
+ " out[:, 1] = cy - h / 2\n",
549
+ " out[:, 2] = cx + w / 2\n",
550
+ " out[:, 3] = cy + h / 2\n",
551
+ " return out\n",
552
+ "\n",
553
+ "\n",
554
+ "print(f\"Prior boxes for {RETINAFACE_INPUT_SIZE}x{RETINAFACE_INPUT_SIZE}: \"\n",
555
+ " f\"{generate_prior_boxes(RETINAFACE_INPUT_SIZE).shape[0]}\")"
556
+ ]
557
+ },
558
+ {
559
+ "cell_type": "markdown",
560
+ "metadata": {},
561
+ "source": [
562
+ "## 6. Face Detector Inference Wrapper"
563
+ ]
564
+ },
565
+ {
566
+ "cell_type": "code",
567
+ "execution_count": null,
568
+ "metadata": {},
569
+ "outputs": [],
570
+ "source": [
571
+ "class ORTInference:\n",
572
+ " \"\"\"Load RetinaFace ONNX model and run inference with ONNX Runtime.\"\"\"\n",
573
+ "\n",
574
+ " def __init__(self, onnx_path):\n",
575
+ " available = ort.get_available_providers()\n",
576
+ " providers = [\"CUDAExecutionProvider\"] if \"CUDAExecutionProvider\" in available else [\"CPUExecutionProvider\"]\n",
577
+ " self.session = ort.InferenceSession(str(onnx_path), providers=providers)\n",
578
+ " self.input_name = self.session.get_inputs()[0].name\n",
579
+ " self.output_names = [out.name for out in self.session.get_outputs()]\n",
580
+ " self.loc_name = next((name for name in self.output_names if \"loc\" in name.lower()), self.output_names[0])\n",
581
+ " self.conf_name = next((name for name in self.output_names if \"conf\" in name.lower()), self.output_names[1])\n",
582
+ "\n",
583
+ " print(f\"ONNX model loaded: {onnx_path}\")\n",
584
+ " print(f\" Provider: {self.session.get_providers()[0]}\")\n",
585
+ " print(f\" Input : {self.input_name} {self.session.get_inputs()[0].shape}\")\n",
586
+ " print(f\" Outputs : {self.output_names}\")\n",
587
+ "\n",
588
+ " def infer(self, preprocessed):\n",
589
+ " raw_outputs = self.session.run(\n",
590
+ " self.output_names,\n",
591
+ " {self.input_name: np.ascontiguousarray(preprocessed, dtype=np.float32)},\n",
592
+ " )\n",
593
+ " outputs = dict(zip(self.output_names, raw_outputs))\n",
594
+ " outputs[\"loc\"] = outputs[self.loc_name]\n",
595
+ " outputs[\"conf\"] = outputs[self.conf_name]\n",
596
+ " return outputs\n",
597
+ "\n",
598
+ "\n",
599
+ "class TRTInference:\n",
600
+ " \"\"\"Load a serialised TensorRT engine and run inference.\"\"\"\n",
601
+ "\n",
602
+ " def __init__(self, engine_path):\n",
603
+ " logger = trt.Logger(trt.Logger.WARNING)\n",
604
+ " with open(engine_path, \"rb\") as f:\n",
605
+ " self.engine = trt.Runtime(logger).deserialize_cuda_engine(f.read())\n",
606
+ " assert self.engine is not None, f\"Failed to load engine: {engine_path}\"\n",
607
+ "\n",
608
+ " self.context = self.engine.create_execution_context()\n",
609
+ " self._stream = cuda.Stream()\n",
610
+ " self._d_bufs = {}\n",
611
+ " self._h_outs = {}\n",
612
+ " self._input_name = \"\"\n",
613
+ " self._input_shape = ()\n",
614
+ "\n",
615
+ " for i in range(self.engine.num_io_tensors):\n",
616
+ " name = self.engine.get_tensor_name(i)\n",
617
+ " shape = tuple(self.engine.get_tensor_shape(name))\n",
618
+ " nbytes = int(np.prod(shape)) * np.dtype(np.float32).itemsize\n",
619
+ " d_mem = cuda.mem_alloc(nbytes)\n",
620
+ " self._d_bufs[name] = d_mem\n",
621
+ " self.context.set_tensor_address(name, int(d_mem))\n",
622
+ "\n",
623
+ " if self.engine.get_tensor_mode(name) == trt.TensorIOMode.OUTPUT:\n",
624
+ " self._h_outs[name] = np.empty(shape, dtype=np.float32)\n",
625
+ " else:\n",
626
+ " self._input_name = name\n",
627
+ " self._input_shape = shape\n",
628
+ "\n",
629
+ " self.output_names = list(self._h_outs.keys())\n",
630
+ " self.loc_name = next((name for name in self.output_names if \"loc\" in name.lower()), self.output_names[0])\n",
631
+ " self.conf_name = next((name for name in self.output_names if \"conf\" in name.lower()), self.output_names[1])\n",
632
+ "\n",
633
+ " print(f\"Engine loaded: {engine_path}\")\n",
634
+ " print(f\" Input : {self._input_name} {self._input_shape}\")\n",
635
+ " print(f\" Outputs: {self.output_names}\")\n",
636
+ "\n",
637
+ " def infer(self, preprocessed):\n",
638
+ " cuda.memcpy_htod(self._d_bufs[self._input_name],\n",
639
+ " np.ascontiguousarray(preprocessed))\n",
640
+ " self.context.execute_async_v3(stream_handle=self._stream.handle)\n",
641
+ " self._stream.synchronize()\n",
642
+ " outputs = {\n",
643
+ " name: (cuda.memcpy_dtoh(buf, self._d_bufs[name]), buf.copy())[1]\n",
644
+ " for name, buf in self._h_outs.items()\n",
645
+ " }\n",
646
+ " outputs[\"loc\"] = outputs[self.loc_name]\n",
647
+ " outputs[\"conf\"] = outputs[self.conf_name]\n",
648
+ " return outputs\n",
649
+ "\n",
650
+ "\n",
651
+ "face_model = ORTInference(ONNX_PATH) if _is_windows_arm64() else TRTInference(ENGINE_PATH)\n",
652
+ "priors = generate_prior_boxes(RETINAFACE_INPUT_SIZE)"
653
+ ]
654
+ },
655
+ {
656
+ "cell_type": "markdown",
657
+ "metadata": {},
658
+ "source": [
659
+ "## 7. Preprocessing and Blur Helpers"
660
+ ]
661
+ },
662
+ {
663
+ "cell_type": "code",
664
+ "execution_count": null,
665
+ "metadata": {},
666
+ "outputs": [],
667
+ "source": [
668
+ "def preprocess_retinaface(bgr_image):\n",
669
+ " \"\"\"BGR mean subtraction + HWC -> NCHW float32.\"\"\"\n",
670
+ " img = bgr_image.astype(np.float32) - BGR_MEAN\n",
671
+ " return np.ascontiguousarray(img.transpose(2, 0, 1)[np.newaxis, ...])\n",
672
+ "\n",
673
+ "\n",
674
+ "def _largest_odd_le(n: int) -> int:\n",
675
+ " \"\"\"Largest odd integer <= n (torchvision requires odd kernel sizes).\"\"\"\n",
676
+ " n = int(n)\n",
677
+ " return n if n % 2 == 1 else n - 1\n",
678
+ "\n",
679
+ "\n",
680
+ "def blur_faces_rgb(rgb_image, bboxes, ksize=BLUR_KSIZE, sigma=BLUR_SIGMA):\n",
681
+ " \"\"\"Gaussian blur on face ROIs using torchvision on CUDA (falls back to CPU).\n",
682
+ "\n",
683
+ " Returns a blurred RGB copy.\n",
684
+ " Each ROI is uploaded to the GPU, blurred, and written back in-place.\n",
685
+ " \"\"\"\n",
686
+ " img = rgb_image.copy()\n",
687
+ " if len(bboxes) == 0:\n",
688
+ " return img\n",
689
+ "\n",
690
+ " h, w = img.shape[:2]\n",
691
+ " k = ksize if ksize % 2 == 1 else ksize + 1\n",
692
+ " sigma_f = float(sigma)\n",
693
+ "\n",
694
+ " for x1, y1, x2, y2 in bboxes:\n",
695
+ " x1i, y1i = max(0, int(x1)), max(0, int(y1))\n",
696
+ " x2i, y2i = min(w, int(x2)), min(h, int(y2))\n",
697
+ " if x2i <= x1i or y2i <= y1i:\n",
698
+ " continue\n",
699
+ "\n",
700
+ " # Kernel must be odd and not larger than the ROI\n",
701
+ " roi_h, roi_w = y2i - y1i, x2i - x1i\n",
702
+ " kh = min(k, _largest_odd_le(roi_h))\n",
703
+ " kw = min(k, _largest_odd_le(roi_w))\n",
704
+ " if kh < 1 or kw < 1:\n",
705
+ " continue\n",
706
+ "\n",
707
+ " roi_np = img[y1i:y2i, x1i:x2i] # HWC uint8\n",
708
+ " roi = (torch.from_numpy(roi_np)\n",
709
+ " .to(_BLUR_DEVICE, non_blocking=True)\n",
710
+ " .permute(2, 0, 1) # HWC -> CHW\n",
711
+ " .unsqueeze(0) # 1xCxHxW\n",
712
+ " .float())\n",
713
+ "\n",
714
+ " blurred = TF.gaussian_blur(roi, kernel_size=[kh, kw],\n",
715
+ " sigma=[sigma_f, sigma_f])\n",
716
+ "\n",
717
+ " out = (blurred.squeeze(0)\n",
718
+ " .clamp_(0, 255)\n",
719
+ " .to(torch.uint8)\n",
720
+ " .permute(1, 2, 0) # CHW -> HWC\n",
721
+ " .cpu()\n",
722
+ " .numpy())\n",
723
+ " img[y1i:y2i, x1i:x2i] = out\n",
724
+ "\n",
725
+ " return img\n",
726
+ "\n",
727
+ "\n",
728
+ "print(\"Helpers ready.\")"
729
+ ]
730
+ },
731
+ {
732
+ "cell_type": "markdown",
733
+ "metadata": {},
734
+ "source": [
735
+ "## 8. Display Helper"
736
+ ]
737
+ },
738
+ {
739
+ "cell_type": "code",
740
+ "execution_count": null,
741
+ "metadata": {},
742
+ "outputs": [],
743
+ "source": [
744
+ "def show_frame(rgb_image, frame_idx, num_faces):\n",
745
+ " \"\"\"Display an RGB frame inline with frame number and face count.\"\"\"\n",
746
+ " plt.figure(figsize=(10, 6))\n",
747
+ " plt.imshow(rgb_image)\n",
748
+ " plt.title(f\"Frame {frame_idx} — {num_faces} face(s) detected\",\n",
749
+ " fontsize=12)\n",
750
+ " plt.axis(\"off\")\n",
751
+ " clear_output(wait=True)\n",
752
+ " plt.show()"
753
+ ]
754
+ },
755
+ {
756
+ "cell_type": "markdown",
757
+ "metadata": {},
758
+ "source": [
759
+ "## 9. Run the Pipeline\n",
760
+ "\n",
761
+ "Decode → detect → blur → display in a single loop.\n",
762
+ "\n",
763
+ "The `ThreadedDecoder` decodes video frames on a background thread so inference\n",
764
+ "never stalls waiting for the next frame. We request **RGB** output so each\n",
765
+ "frame arrives as an `(H, W, 3)` uint8 tensor that we can convert to BGR for\n",
766
+ "RetinaFace and display directly with matplotlib."
767
+ ]
768
+ },
769
+ {
770
+ "cell_type": "code",
771
+ "execution_count": null,
772
+ "metadata": {},
773
+ "outputs": [],
774
+ "source": [
775
+ "color_format = OutputColorType.RGB\n",
776
+ "batch_size = 1\n",
777
+ "\n",
778
+ "decoder = ThreadedDecoder(\n",
779
+ " enc_file_path=INPUT_VIDEO,\n",
780
+ " buffer_size=12,\n",
781
+ " gpu_id=0,\n",
782
+ " use_device_memory=False,\n",
783
+ " output_color_type=color_format,\n",
784
+ ")\n",
785
+ "\n",
786
+ "frame_count = 0\n",
787
+ "total_faces = 0\n",
788
+ "\n",
789
+ "while True:\n",
790
+ " if MAX_FRAMES > 0 and frame_count >= MAX_FRAMES:\n",
791
+ " break\n",
792
+ "\n",
793
+ " frames = decoder.get_batch_frames(batch_size)\n",
794
+ " if len(frames) == 0:\n",
795
+ " break\n",
796
+ "\n",
797
+ " for frame in frames:\n",
798
+ " # Convert dlpack frame to numpy RGB (H, W, 3)\n",
799
+ " rgb = torch.from_dlpack(frame).cpu().numpy()\n",
800
+ " if rgb.ndim == 3 and rgb.shape[0] == 3:\n",
801
+ " rgb = rgb.transpose(1, 2, 0) # CHW to HWC\n",
802
+ " rgb = np.ascontiguousarray(rgb, dtype=np.uint8)\n",
803
+ "\n",
804
+ " # RGB to BGR for RetinaFace\n",
805
+ " if _is_windows_arm64():\n",
806
+ " bgr = rgb[..., ::-1]\n",
807
+ " else:\n",
808
+ " bgr = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)\n",
809
+ "\n",
810
+ " # Letterbox + preprocess\n",
811
+ " lb, scale, dx, dy = letterbox(bgr, RETINAFACE_INPUT_SIZE)\n",
812
+ " inp = preprocess_retinaface(lb)\n",
813
+ "\n",
814
+ " # Face detector inference\n",
815
+ " outputs = face_model.infer(inp)\n",
816
+ "\n",
817
+ " # Post-process to face bboxes in original coords\n",
818
+ " faces = postprocess_detections(\n",
819
+ " outputs[\"loc\"].squeeze(0),\n",
820
+ " outputs[\"conf\"].squeeze(0),\n",
821
+ " priors,\n",
822
+ " RETINAFACE_INPUT_SIZE,\n",
823
+ " scale, dx, dy,\n",
824
+ " conf_thresh=CONF_THRESH,\n",
825
+ " nms_iou=NMS_IOU,\n",
826
+ " bbox_scale=BBOX_SCALE,\n",
827
+ " )\n",
828
+ "\n",
829
+ " total_faces += len(faces)\n",
830
+ "\n",
831
+ " # Blur faces on RGB image\n",
832
+ " blurred = blur_faces_rgb(rgb, faces)\n",
833
+ "\n",
834
+ " # Display periodically\n",
835
+ " if frame_count % DISPLAY_EVERY_N == 0:\n",
836
+ " show_frame(blurred, frame_count, len(faces))\n",
837
+ "\n",
838
+ " frame_count += 1\n",
839
+ "\n",
840
+ "print(f\"\\nDone: {frame_count} frames processed, {total_faces} total faces detected.\")"
841
+ ]
842
+ },
843
+ {
844
+ "cell_type": "markdown",
845
+ "metadata": {},
846
+ "source": [
847
+ "## Summary\n",
848
+ "\n",
849
+ "| Component | Implementation |\n",
850
+ "|---|---|\n",
851
+ "| Video decode | `ThreadedDecoder` (PyNvVideoCodec 2.x) - background-threaded NVDEC |\n",
852
+ "| Face detection | Windows ARM64: RetinaFace ONNX via ONNX Runtime; other platforms: RetinaFace FP16 via TensorRT |\n",
853
+ "| Image helpers | Windows ARM64: Pillow and NumPy channel swap; other platforms: OpenCV |\n",
854
+ "| Post-processing | NumPy: bbox decode, threshold, NMS, reverse letterbox |\n",
855
+ "| Blur | `torchvision.transforms.functional.gaussian_blur` on face ROIs (GPU) |\n",
856
+ "| Display | `matplotlib` + `IPython.display.clear_output` for inline animation |"
857
+ ]
858
+ }
859
+ ],
860
+ "metadata": {
861
+ "kernelspec": {
862
+ "display_name": ".venv",
863
+ "language": "python",
864
+ "name": "python3"
865
+ },
866
+ "language_info": {
867
+ "codemirror_mode": {
868
+ "name": "ipython",
869
+ "version": 3
870
+ },
871
+ "file_extension": ".py",
872
+ "mimetype": "text/x-python",
873
+ "name": "python",
874
+ "nbconvert_exporter": "python",
875
+ "pygments_lexer": "ipython3",
876
+ "version": "3.12.4"
877
+ }
878
+ },
879
+ "nbformat": 4,
880
+ "nbformat_minor": 4
881
+ }