Banaxi-Tech commited on
Commit
d176ecd
·
verified ·
1 Parent(s): 85ca8fb

Upload face detector (YOLO11n/s), ONNX exports, scripts, model card

Browse files
README.md ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: agpl-3.0
3
+ library_name: ultralytics
4
+ pipeline_tag: object-detection
5
+ tags:
6
+ - face-detection
7
+ - yolo11
8
+ - object-detection
9
+ - onnx
10
+ - privacy
11
+ ---
12
+
13
+ # face-model — single-class face detector for blurring faces in video
14
+
15
+ YOLO11 (nano and small) fine-tuned from the COCO-pretrained Ultralytics weights as a one-class `face` detector on WIDER FACE,
16
+ plus a video script that detects, tracks and pixelates (or blacks out) faces. Built for **recall** (privacy), not leaderboard accuracy.
17
+
18
+ ## Important licence notes (read before using commercially)
19
+
20
+ - The code and weights are derived from **Ultralytics YOLO11, which is AGPL-3.0**. Using them in a product or network service means
21
+ complying with AGPL-3.0 or obtaining an Ultralytics enterprise licence.
22
+ - The **training data, WIDER FACE, is listed as non-commercial** (CC BY-NC-ND 4.0 on its Hugging Face card; the original
23
+ WIDER FACE terms are for non-commercial research). Do not assume these weights are cleared for commercial use.
24
+ - `code/face_id.py` (optional `--keep` mode) expects InsightFace `buffalo_l` ONNX models, which are **non-commercial research only**.
25
+ They are *not* included in this repo; download them yourself.
26
+
27
+ ## Files
28
+
29
+ | File | Notes |
30
+ |---|---|
31
+ | `yolo11n/face_yolo11n.pt` | PyTorch weights, 5.4 MB |
32
+ | `yolo11n/face_yolo11n_fp32.onnx`, `_fp16.onnx` | ONNX, fixed 640x640, batch 1 |
33
+ | `yolo11n/face_yolo11n_int8.onnx` | INT8 (ONNX Runtime static QDQ). The Detect-head decode nodes are kept in float; plain INT8 returned zero detections |
34
+ | `yolo11s/face_yolo11s.pt`, `_fp32.onnx`, `_fp16.onnx` | Larger, more accurate |
35
+ | `code/` | `blur_video.py`, `train.py`, `convert_wider_to_yolo.py`, `eval_model.py`, `recall_at_conf.py`, `quantize_int8.py`, `face_id.py` |
36
+
37
+ ## Results (WIDER FACE val, 3,222 images / 39,112 faces, 640 px, all difficulty levels together)
38
+
39
+ | Model | mAP50 | mAP50-95 |
40
+ |---|---|---|
41
+ | YOLO11n `.pt` | 0.684 | 0.362 |
42
+ | YOLO11n ONNX FP32 | 0.683 | 0.362 |
43
+ | YOLO11n ONNX FP16 | 0.682 | 0.361 |
44
+ | YOLO11n ONNX INT8 | 0.667 | 0.348 |
45
+ | YOLO11s `.pt` | 0.741 | 0.401 |
46
+ | YOLO11s ONNX FP16 | 0.739 | 0.399 |
47
+
48
+ **Recall at confidence 0.25 (IoU >= 0.5)**, i.e. the fraction of labelled faces that get a box:
49
+
50
+ | Face height (px, images stored at <=640 px) | n=39,112 | YOLO11n FP16 | YOLO11n INT8 | YOLO11s FP16 |
51
+ |---|---|---|---|---|
52
+ | < 10 | 16,305 | 33.1% | 31.6% | 42.1% |
53
+ | 10-20 | 10,521 | 72.2% | 70.7% | 79.1% |
54
+ | 20-40 | 7,478 | 86.6% | 85.4% | 90.4% |
55
+ | 40-80 | 3,167 | 93.7% | 92.9% | 95.3% |
56
+ | > 80 | 1,641 | 95.6% | 95.0% | 97.3% |
57
+ | **Overall** | | **61.4%** | **60.0%** | **67.9%** |
58
+
59
+ WIDER FACE is dominated by tiny faces (median face height about 12 px). Performance on tiny, far-away faces is the main weakness;
60
+ on faces >= 20 px the nano finds about 90%. **No detector guarantees every face is caught in every frame** — spot-check any
61
+ video where missing a face matters.
62
+
63
+ Not measured: speed on phones or other hardware. Any such numbers you may see elsewhere in this project were estimates.
64
+
65
+ ## Training
66
+
67
+ - Start: COCO `yolo11n.pt` / `yolo11s.pt`, fine-tuned (not from scratch), single class `face`.
68
+ - Data: WIDER FACE train, converted to YOLO format (`code/convert_wider_to_yolo.py`): invalid boxes dropped, small faces kept, images downscaled to <= 640 px long side.
69
+ - 640 px, AMP. Nano: 40 epochs. Small: 40 epochs (resumed from a checkpoint at epoch 34 with the schedule shortened to end at 40).
70
+ - Extra augmentation: random motion blur and JPEG compression to imitate video frames (`train.py`).
71
+
72
+ ## Use
73
+
74
+ ```bash
75
+ pip install ultralytics onnxruntime-gpu lap
76
+ python code/blur_video.py in.mp4 out.mp4 --model yolo11n/face_yolo11n_fp16.onnx -n 1 # pixelate
77
+ python code/blur_video.py in.mp4 out.mp4 --model yolo11n/face_yolo11n_fp16.onnx --mode black # solid black
78
+ ```
79
+
80
+ Defaults: detect every 3 frames (`-n`) with ByteTrack in between, confidence 0.25, 15% box padding, 8 pixel blocks across the
81
+ shorter side of each face. Use `-n 1` for the strictest coverage. Audio is preserved via ffmpeg. With an ONNX model use
82
+ `--device 0` (GPU); `--device cpu` makes Ultralytics try to pip-install `onnxruntime`.
83
+
84
+ `--keep ref.jpg [...]` leaves one person visible and blurs everyone else using face recognition; every other face stays blurred
85
+ unless confirmed over several frames. It needs the InsightFace models mentioned above.
code/blur_video.py ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Detect, track and pixelate (or black out) faces in a video. Audio is preserved via ffmpeg.
3
+
4
+ Detection runs every N frames; between detections ByteTrack's Kalman filter carries the boxes forward.
5
+ Tuned for recall (privacy): low confidence threshold, box padding, raw detections are blurred immediately
6
+ (without waiting for track confirmation) and recently lost tracks are held for a few frames.
7
+
8
+ --keep: leave ONE person's face visible and blur everybody else. Give one or more reference photos of that
9
+ person. Fail-safe by design: a face stays blurred unless its track was confirmed as the target by several
10
+ consecutive face-recognition matches; a clear non-match, a track box jump, or going too long without a
11
+ successful re-check puts the blur back.
12
+
13
+ Examples:
14
+ python blur_video.py in.mp4 out.mp4 --model export/face_yolo11n_fp16.onnx -n 1
15
+ python blur_video.py in.mp4 out.mp4 --mode black
16
+ python blur_video.py in.mp4 out.mp4 -n 1 --keep me1.jpg me2.jpg
17
+ """
18
+ import argparse
19
+ import queue
20
+ import shutil
21
+ import subprocess
22
+ import sys
23
+ import threading
24
+ import time
25
+ from types import SimpleNamespace
26
+
27
+ import cv2
28
+ import numpy as np
29
+ from ultralytics import YOLO
30
+ from ultralytics.trackers.byte_tracker import BYTETracker
31
+
32
+
33
+ def pad_box(x1, y1, x2, y2, pad, w, h):
34
+ bw, bh = x2 - x1, y2 - y1
35
+ x1, x2 = x1 - bw * pad, x2 + bw * pad
36
+ y1, y2 = y1 - bh * pad, y2 + bh * pad
37
+ return max(0, int(np.floor(x1))), max(0, int(np.floor(y1))), min(w, int(np.ceil(x2))), min(h, int(np.ceil(y2)))
38
+
39
+
40
+ def iou(a, b):
41
+ ix = max(0.0, min(a[2], b[2]) - max(a[0], b[0]))
42
+ iy = max(0.0, min(a[3], b[3]) - max(a[1], b[1]))
43
+ inter = ix * iy
44
+ union = (a[2] - a[0]) * (a[3] - a[1]) + (b[2] - b[0]) * (b[3] - b[1]) - inter
45
+ return inter / union if union > 0 else 0.0
46
+
47
+
48
+ def pixelate(frame, box, blocks):
49
+ """Downscale the region so its shorter side has `blocks` cells, then nearest-neighbour upscale."""
50
+ x1, y1, x2, y2 = box
51
+ if x2 - x1 < 2 or y2 - y1 < 2:
52
+ return
53
+ roi = frame[y1:y2, x1:x2]
54
+ h, w = roi.shape[:2]
55
+ cell = max(1.0, min(w, h) / blocks)
56
+ small = cv2.resize(roi, (max(1, round(w / cell)), max(1, round(h / cell))), interpolation=cv2.INTER_AREA)
57
+ frame[y1:y2, x1:x2] = cv2.resize(small, (w, h), interpolation=cv2.INTER_NEAREST)
58
+
59
+
60
+ def blackout(frame, box):
61
+ x1, y1, x2, y2 = box
62
+ frame[y1:y2, x1:x2] = 0
63
+
64
+
65
+ def open_ffmpeg_writer(src, dst, w, h, fps, crf):
66
+ cmd = [
67
+ "ffmpeg", "-y", "-loglevel", "error",
68
+ "-f", "rawvideo", "-pix_fmt", "bgr24", "-s", f"{w}x{h}", "-r", f"{fps}", "-i", "-",
69
+ "-i", src,
70
+ "-map", "0:v:0", "-map", "1:a?",
71
+ "-c:v", "libx264", "-preset", "fast", "-crf", str(crf), "-pix_fmt", "yuv420p",
72
+ "-c:a", "copy", "-shortest", "-movflags", "+faststart", dst,
73
+ ]
74
+ return subprocess.Popen(cmd, stdin=subprocess.PIPE)
75
+
76
+
77
+ class KeepTracker:
78
+ """Decides which ByteTrack ids are the person to keep visible. Everything not confirmed stays blurred."""
79
+
80
+ def __init__(self, fid, refs, thr, min_size, hits_needed, recheck, expire, budget):
81
+ self.fid, self.refs, self.thr, self.min_size = fid, refs, thr, min_size
82
+ self.hits_needed, self.recheck, self.expire, self.budget = hits_needed, recheck, expire, budget
83
+ self.state = {} # tid -> dict(hits, kept, last_ok, last_try, box)
84
+ self.seen, self.ever_kept = set(), set()
85
+
86
+ def is_kept(self, tid, idx):
87
+ s = self.state.get(tid)
88
+ if s is None or not s["kept"]:
89
+ return False
90
+ if idx - s["last_ok"] > self.expire: # could not re-confirm for too long -> blur again
91
+ s["kept"], s["hits"] = False, 0
92
+ return False
93
+ return True
94
+
95
+ def update(self, frame, idx, tracked):
96
+ """tracked: list of (xyxy, tid) from a detection frame (clean, un-blurred frame)."""
97
+ todo = []
98
+ for box, tid in tracked:
99
+ self.seen.add(tid)
100
+ s = self.state.setdefault(tid, dict(hits=0, kept=False, last_ok=-10**9, last_try=-10**9, box=box))
101
+ if s["kept"] and iou(s["box"], box) < 0.3: # box jumped: possible ID switch -> distrust
102
+ s["kept"], s["hits"] = False, 0
103
+ s["box"] = box
104
+ if not self.is_kept(tid, idx):
105
+ todo.append((0, box, tid)) # undecided first
106
+ elif idx - s["last_try"] >= self.recheck:
107
+ todo.append((1, box, tid))
108
+ todo.sort(key=lambda t: (t[0], self.state[t[2]]["last_try"]))
109
+ for _, box, tid in todo[:self.budget]:
110
+ s = self.state[tid]
111
+ s["last_try"] = idx
112
+ e = self.fid.embed_box(frame, box, min_size=self.min_size)
113
+ if e is None:
114
+ continue # inconclusive: keep old state (a kept track lapses via `expire`)
115
+ if self.fid.similarity(e, self.refs) >= self.thr:
116
+ s["hits"] += 1
117
+ s["last_ok"] = idx
118
+ if s["hits"] >= self.hits_needed:
119
+ s["kept"] = True
120
+ self.ever_kept.add(tid)
121
+ else: # clear non-match: blur immediately
122
+ s["hits"], s["kept"] = 0, False
123
+
124
+
125
+ def main():
126
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
127
+ ap.add_argument("input")
128
+ ap.add_argument("output")
129
+ ap.add_argument("--model", default="runs/face_yolo11n/weights/best.pt", help=".pt or .onnx face detector")
130
+ ap.add_argument("-n", "--every", type=int, default=3, help="run detector every N frames (default 3)")
131
+ ap.add_argument("--conf", type=float, default=0.25, help="detection confidence threshold (default 0.25)")
132
+ ap.add_argument("--pad", type=float, default=0.15, help="pad each box by this fraction per side (default 0.15)")
133
+ ap.add_argument("--mode", choices=["pixelate", "black"], default="pixelate",
134
+ help="pixelate faces (default) or fill the padded box with solid black")
135
+ ap.add_argument("--blocks", type=int, default=8,
136
+ help="pixel blocks across a face's shorter side; LOWER = stronger pixelation (default 8)")
137
+ ap.add_argument("--hold", type=int, default=8, help="keep blurring a lost track's prediction this many frames")
138
+ ap.add_argument("--imgsz", type=int, default=640)
139
+ ap.add_argument("--device", default=None, help="e.g. 0 or cpu (default: auto)")
140
+ ap.add_argument("--crf", type=int, default=18, help="x264 quality (lower = better)")
141
+ k = ap.add_argument_group("keep one person visible (face recognition)")
142
+ k.add_argument("--keep", nargs="+", metavar="REF.jpg",
143
+ help="reference photo(s) of the person to leave unblurred; everyone else is blurred")
144
+ k.add_argument("--keep-threshold", type=float, default=0.45,
145
+ help="cosine similarity needed to count as the same person; HIGHER = stricter (default 0.45)")
146
+ k.add_argument("--keep-hits", type=int, default=2, help="consecutive matches before a track is unblurred")
147
+ k.add_argument("--keep-min-size", type=int, default=40,
148
+ help="faces smaller than this many px are never recognised, so they stay blurred (default 40)")
149
+ k.add_argument("--keep-recheck", type=int, default=8, help="re-verify a kept track every this many frames")
150
+ k.add_argument("--keep-expire", type=int, default=24,
151
+ help="blur a kept track again if it could not be re-confirmed for this many frames")
152
+ k.add_argument("--keep-budget", type=int, default=4, help="max recognitions per frame (speed cap)")
153
+ a = ap.parse_args()
154
+
155
+ if shutil.which("ffmpeg") is None:
156
+ sys.exit("ffmpeg not found on PATH")
157
+ if a.every < 1 or a.blocks < 1:
158
+ sys.exit("--every and --blocks must be >= 1")
159
+
160
+ keeper = None
161
+ if a.keep:
162
+ from face_id import FaceID
163
+ fid = FaceID("cpu" if a.device == "cpu" else "0")
164
+ refs = fid.reference_embeddings(a.keep)
165
+ print(f"keeping the person in {len(refs)} reference photo(s); threshold {a.keep_threshold}")
166
+ keeper = KeepTracker(fid, refs, a.keep_threshold, a.keep_min_size, a.keep_hits,
167
+ a.keep_recheck, a.keep_expire, a.keep_budget)
168
+
169
+ cap = cv2.VideoCapture(a.input)
170
+ if not cap.isOpened():
171
+ sys.exit(f"cannot open {a.input}")
172
+ fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
173
+ total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) or 0
174
+ ok, frame = cap.read()
175
+ if not ok:
176
+ sys.exit("no frames in input")
177
+ H, W = frame.shape[:2]
178
+
179
+ model = YOLO(a.model, task="detect")
180
+ tracker = BYTETracker(SimpleNamespace(
181
+ track_high_thresh=a.conf, track_low_thresh=0.1, new_track_thresh=a.conf,
182
+ track_buffer=max(30, 4 * a.every), match_thresh=0.8, fuse_score=True))
183
+ writer = open_ffmpeg_writer(a.input, a.output, W, H, fps, a.crf)
184
+
185
+ # Decode and encode run in their own threads so they overlap with detection (frame order is preserved:
186
+ # one reader, one writer, FIFO queues). cv2/ffmpeg pipe I/O release the GIL.
187
+ rq, wq, write_failed = queue.Queue(maxsize=48), queue.Queue(maxsize=48), []
188
+ first_frame = frame
189
+
190
+ def reader():
191
+ rq.put(first_frame)
192
+ while True:
193
+ okr, f = cap.read()
194
+ if not okr:
195
+ rq.put(None)
196
+ return
197
+ rq.put(f)
198
+
199
+ def writer_loop():
200
+ try:
201
+ while True:
202
+ f = wq.get()
203
+ if f is None:
204
+ return
205
+ writer.stdin.write(f.tobytes())
206
+ except (BrokenPipeError, OSError):
207
+ write_failed.append(True)
208
+ while wq.get() is not None: # drain so the main thread never blocks on a full queue
209
+ pass
210
+
211
+ threading.Thread(target=reader, daemon=True).start()
212
+ wthread = threading.Thread(target=writer_loop)
213
+ wthread.start()
214
+
215
+ t0 = time.perf_counter()
216
+ idx = n_det = 0
217
+ while True:
218
+ frame = rq.get()
219
+ if frame is None:
220
+ break
221
+ items = [] # (xyxy, track_id or None)
222
+ if idx % a.every == 0:
223
+ r = model.predict(frame, conf=a.conf, imgsz=a.imgsz, device=a.device, verbose=False)[0]
224
+ det = r.boxes.cpu().numpy()
225
+ n_det += 1
226
+ tracked = tracker.update(det, frame)
227
+ items += [(b[:4], int(b[4])) for b in tracked] # confirmed tracks
228
+ items += [(b, None) for b in det.xyxy] # raw detections: don't wait for track confirmation
229
+ items += [(t.xyxy, t.track_id) for t in tracker.tracked_stracks if not t.is_activated]
230
+ items += [(t.xyxy, t.track_id) for t in tracker.lost_stracks if tracker.frame_id - t.end_frame <= a.hold]
231
+ if keeper: # recognise on the clean frame, before anything is drawn
232
+ keeper.update(frame, idx, [(b[:4], int(b[4])) for b in tracked])
233
+ else:
234
+ # no detector this frame: advance Kalman state of every live track and use the prediction
235
+ tracker.frame_id += 1
236
+ pool = tracker.tracked_stracks + tracker.lost_stracks
237
+ if pool:
238
+ tracker.multi_predict(pool)
239
+ items += [(t.xyxy, t.track_id) for t in tracker.tracked_stracks]
240
+ items += [(t.xyxy, t.track_id) for t in tracker.lost_stracks if tracker.frame_id - t.end_frame <= a.hold]
241
+
242
+ if keeper:
243
+ kept_boxes = [np.asarray(b, dtype=float)[:4] for b, tid in items
244
+ if tid is not None and keeper.is_kept(tid, idx)]
245
+ items = [(b, tid) for b, tid in items
246
+ if not (tid is not None and keeper.is_kept(tid, idx))
247
+ and not (tid is None and any(iou(np.asarray(b, dtype=float)[:4], kb) >= 0.5 for kb in kept_boxes))]
248
+
249
+ for b, _ in items:
250
+ box = pad_box(*np.asarray(b, dtype=float)[:4], a.pad, W, H)
251
+ if a.mode == "black":
252
+ blackout(frame, box)
253
+ else:
254
+ pixelate(frame, box, a.blocks)
255
+ wq.put(frame)
256
+
257
+ idx += 1
258
+ if idx % 100 == 0:
259
+ el = time.perf_counter() - t0
260
+ print(f"\r{idx}/{total or '?'} frames {idx / el:.1f} FPS", end="", flush=True)
261
+
262
+ wq.put(None)
263
+ wthread.join()
264
+ cap.release()
265
+ try:
266
+ writer.stdin.close()
267
+ except (BrokenPipeError, OSError):
268
+ pass
269
+ rc = writer.wait()
270
+ if write_failed:
271
+ rc = rc or 1
272
+ el = time.perf_counter() - t0
273
+ print(f"\rdone: {idx} frames in {el:.1f}s = {idx / el:.1f} FPS processing "
274
+ f"(detector ran on {n_det} frames, every {a.every}) -> {a.output}")
275
+ if keeper:
276
+ print(f"keep: {len(keeper.ever_kept)} of {len(keeper.seen)} tracks were recognised as the target and left visible")
277
+ if rc != 0:
278
+ sys.exit(f"ffmpeg exited with code {rc}")
279
+
280
+
281
+ if __name__ == "__main__":
282
+ main()
code/convert_wider_to_yolo.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Convert WIDER FACE (HF parquet, CUHK-CSE/wider_face @ refs/convert/parquet) to single-class YOLO format.
3
+
4
+ - Skips boxes flagged `invalid` and degenerate boxes (<1px).
5
+ - Keeps small faces (no min-size filter) since blurring needs high recall.
6
+ - Drops images whose only boxes are invalid (they would teach the model to ignore real faces).
7
+ - Downscales images so the long side is <= --max-side (matches training imgsz; boxes are normalized).
8
+
9
+ Usage:
10
+ python convert_wider_to_yolo.py --raw raw/default --out dataset
11
+ """
12
+ import argparse
13
+ import io
14
+ import os
15
+ from concurrent.futures import ProcessPoolExecutor
16
+ from pathlib import Path
17
+
18
+ import pyarrow.parquet as pq
19
+ from PIL import Image
20
+
21
+ SPLITS = {"train": "train", "validation": "val"}
22
+
23
+
24
+ def convert_row(args):
25
+ name, img_bytes, bboxes, invalid, out_img, out_lbl, max_side = args
26
+ valid = [b for b, inv in zip(bboxes, invalid) if not inv and b[2] >= 1 and b[3] >= 1]
27
+ if not valid:
28
+ return "dropped", 0, 0
29
+ im = Image.open(io.BytesIO(img_bytes)).convert("RGB")
30
+ w, h = im.size
31
+ scale = min(1.0, max_side / max(w, h))
32
+ if scale < 1.0:
33
+ im = im.resize((max(1, round(w * scale)), max(1, round(h * scale))), Image.LANCZOS)
34
+ stem = Path(name).stem
35
+ im.save(out_img / f"{stem}.jpg", quality=95)
36
+ lines = []
37
+ for x, y, bw, bh in valid:
38
+ x1, y1 = max(0.0, x), max(0.0, y)
39
+ x2, y2 = min(float(w), x + bw), min(float(h), y + bh)
40
+ if x2 - x1 < 1 or y2 - y1 < 1:
41
+ continue
42
+ lines.append(f"0 {(x1 + x2) / 2 / w:.6f} {(y1 + y2) / 2 / h:.6f} {(x2 - x1) / w:.6f} {(y2 - y1) / h:.6f}")
43
+ (out_lbl / f"{stem}.txt").write_text("\n".join(lines) + "\n")
44
+ return "ok", len(lines), len(bboxes) - len(valid)
45
+
46
+
47
+ def rows(files, out_img, out_lbl, max_side):
48
+ for f in files:
49
+ pf = pq.ParquetFile(f)
50
+ for batch in pf.iter_batches(batch_size=64):
51
+ for r in batch.to_pylist():
52
+ yield (r["image"]["path"], r["image"]["bytes"], r["faces"]["bbox"],
53
+ r["faces"]["invalid"], out_img, out_lbl, max_side)
54
+
55
+
56
+ def main():
57
+ ap = argparse.ArgumentParser()
58
+ ap.add_argument("--raw", default="raw/default")
59
+ ap.add_argument("--out", default="dataset")
60
+ ap.add_argument("--max-side", type=int, default=640)
61
+ ap.add_argument("--limit", type=int, default=0, help="max images per split (0 = all; for testing)")
62
+ ap.add_argument("--workers", type=int, default=os.cpu_count())
63
+ a = ap.parse_args()
64
+
65
+ out = Path(a.out)
66
+ for src, dst in SPLITS.items():
67
+ files = sorted((Path(a.raw) / src).glob("*.parquet"))
68
+ out_img, out_lbl = out / "images" / dst, out / "labels" / dst
69
+ out_img.mkdir(parents=True, exist_ok=True)
70
+ out_lbl.mkdir(parents=True, exist_ok=True)
71
+ gen = rows(files, out_img, out_lbl, a.max_side)
72
+ if a.limit:
73
+ import itertools
74
+ gen = itertools.islice(gen, a.limit)
75
+ ok = dropped = faces = 0
76
+ with ProcessPoolExecutor(a.workers) as ex:
77
+ for status, n, _ in ex.map(convert_row, gen, chunksize=16):
78
+ if status == "ok":
79
+ ok += 1
80
+ faces += n
81
+ else:
82
+ dropped += 1
83
+ print(f"{dst}: {ok} images, {faces} valid faces, {dropped} images dropped (no valid boxes)")
84
+
85
+ (out / "data.yaml").write_text(
86
+ f"path: {out.resolve()}\ntrain: images/train\nval: images/val\nnames:\n 0: face\n")
87
+ print("wrote", out / "data.yaml")
88
+
89
+
90
+ if __name__ == "__main__":
91
+ main()
code/eval_model.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Evaluate one or more face-detector weights (.pt / .onnx) on the converted WIDER FACE val split.
3
+
4
+ Usage: python eval_model.py export/face_yolo11n.pt export/face_yolo11n_fp32.onnx export/face_yolo11n_fp16.onnx
5
+ """
6
+ import argparse
7
+
8
+ from ultralytics import YOLO
9
+
10
+
11
+ def main():
12
+ ap = argparse.ArgumentParser()
13
+ ap.add_argument("models", nargs="+")
14
+ ap.add_argument("--data", default="dataset/data.yaml")
15
+ ap.add_argument("--imgsz", type=int, default=640)
16
+ ap.add_argument("--workers", type=int, default=4)
17
+ a = ap.parse_args()
18
+ for path in a.models:
19
+ r = YOLO(path, task="detect").val(data=a.data, imgsz=a.imgsz, batch=1, device=0,
20
+ workers=a.workers, plots=False, verbose=False)
21
+ print(f"RES {path}: mAP50={r.box.map50:.4f} mAP50-95={r.box.map:.4f} P={r.box.mp:.3f} R={r.box.mr:.3f} "
22
+ f"| {r.speed['preprocess']:.2f}+{r.speed['inference']:.2f}+{r.speed['postprocess']:.2f} ms/img",
23
+ flush=True)
24
+
25
+
26
+ if __name__ == "__main__":
27
+ main()
code/face_id.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Face identity helper: SCRFD landmarks + ArcFace (w600k_r50) embeddings via plain onnxruntime.
3
+
4
+ No `insightface` package needed (it would pull a CPU onnxruntime over onnxruntime-gpu); only the two ONNX files
5
+ from InsightFace's buffalo_l pack in models/. NOTE: those pretrained models are licensed for non-commercial
6
+ research use only.
7
+
8
+ Used by blur_video.py --keep to leave one person's face visible while blurring everyone else.
9
+ """
10
+ from pathlib import Path
11
+
12
+ import cv2
13
+ import numpy as np
14
+ import onnxruntime as ort
15
+
16
+ MODELS = Path(__file__).resolve().parent / "models"
17
+ # ArcFace 112x112 alignment template (left eye, right eye, nose, left mouth, right mouth)
18
+ ARC_TEMPLATE = np.array([[38.2946, 51.6963], [73.5318, 51.5014], [56.0252, 71.7366],
19
+ [41.5493, 92.3655], [70.7299, 92.2041]], np.float32)
20
+
21
+
22
+ def _session(path, device):
23
+ prov = ["CUDAExecutionProvider", "CPUExecutionProvider"] if device != "cpu" else ["CPUExecutionProvider"]
24
+ so = ort.SessionOptions()
25
+ so.log_severity_level = 3
26
+ return ort.InferenceSession(str(path), sess_options=so, providers=prov)
27
+
28
+
29
+ def _nms(boxes, scores, thr=0.4):
30
+ idx = cv2.dnn.NMSBoxes([[float(b[0]), float(b[1]), float(b[2] - b[0]), float(b[3] - b[1])] for b in boxes],
31
+ [float(s) for s in scores], 0.0, thr)
32
+ return np.array(idx, dtype=int).reshape(-1)
33
+
34
+
35
+ class FaceID:
36
+ def __init__(self, device="cpu"):
37
+ self.det = _session(MODELS / "det_10g.onnx", device)
38
+ self.rec = _session(MODELS / "w600k_r50.onnx", device)
39
+ self.det_in = self.det.get_inputs()[0].name
40
+ self.rec_in = self.rec.get_inputs()[0].name
41
+
42
+ def detect(self, img, size=192, thr=0.4):
43
+ """SCRFD on `img` letterboxed (top-left) into a size x size canvas.
44
+ Returns boxes (N,4) xyxy, scores (N,), landmarks (N,5,2), all in `img` pixel coordinates."""
45
+ h, w = img.shape[:2]
46
+ s = size / max(h, w)
47
+ nw, nh = max(1, int(round(w * s))), max(1, int(round(h * s)))
48
+ canvas = np.zeros((size, size, 3), np.uint8)
49
+ canvas[:nh, :nw] = cv2.resize(img, (nw, nh))
50
+ blob = cv2.dnn.blobFromImage(canvas, 1 / 128.0, (size, size), (127.5, 127.5, 127.5), swapRB=True)
51
+ outs = self.det.run(None, {self.det_in: blob})
52
+ B, S, K = [], [], []
53
+ for i, stride in enumerate((8, 16, 32)):
54
+ sc, bb, kp = outs[i][:, 0], outs[i + 3] * stride, outs[i + 6] * stride
55
+ g = size // stride
56
+ ys, xs = np.mgrid[:g, :g]
57
+ ctr = np.repeat(np.stack([xs, ys], -1).reshape(-1, 2) * stride, 2, axis=0).astype(np.float32)
58
+ m = sc >= thr
59
+ if not m.any():
60
+ continue
61
+ c, bb, kp = ctr[m], bb[m], kp[m]
62
+ B.append(np.stack([c[:, 0] - bb[:, 0], c[:, 1] - bb[:, 1], c[:, 0] + bb[:, 2], c[:, 1] + bb[:, 3]], 1))
63
+ K.append(np.stack([c[:, 0:1] + kp[:, 0::2], c[:, 1:2] + kp[:, 1::2]], -1))
64
+ S.append(sc[m])
65
+ if not B:
66
+ return np.zeros((0, 4)), np.zeros(0), np.zeros((0, 5, 2))
67
+ B, S, K = np.concatenate(B) / s, np.concatenate(S), np.concatenate(K) / s
68
+ keep = _nms(B, S)
69
+ return B[keep], S[keep], K[keep]
70
+
71
+ def embed_aligned(self, img, lmk):
72
+ M, _ = cv2.estimateAffinePartial2D(lmk.astype(np.float32), ARC_TEMPLATE, method=cv2.LMEDS)
73
+ if M is None:
74
+ return None
75
+ face = cv2.warpAffine(img, M, (112, 112), borderValue=0)
76
+ blob = cv2.dnn.blobFromImage(face, 1 / 127.5, (112, 112), (127.5, 127.5, 127.5), swapRB=True)
77
+ e = self.rec.run(None, {self.rec_in: blob})[0][0]
78
+ return e / (np.linalg.norm(e) + 1e-9)
79
+
80
+ def embed_box(self, frame, box, expand=0.5, min_size=40):
81
+ """Embedding of the face inside `box` (xyxy) of a full frame, or None when it is too small / not found.
82
+ Crops the box expanded by `expand`, finds landmarks of the face nearest the box centre, aligns, embeds."""
83
+ H, W = frame.shape[:2]
84
+ x1, y1, x2, y2 = [float(v) for v in box[:4]]
85
+ bw, bh = x2 - x1, y2 - y1
86
+ if min(bw, bh) < min_size:
87
+ return None
88
+ cx1, cy1 = max(0, int(x1 - bw * expand)), max(0, int(y1 - bh * expand))
89
+ cx2, cy2 = min(W, int(x2 + bw * expand)), min(H, int(y2 + bh * expand))
90
+ crop = frame[cy1:cy2, cx1:cx2]
91
+ if crop.size == 0:
92
+ return None
93
+ B, S, K = self.detect(crop)
94
+ if len(B) == 0:
95
+ return None
96
+ tx, ty = (x1 + x2) / 2 - cx1, (y1 + y2) / 2 - cy1
97
+ d = [np.hypot((b[0] + b[2]) / 2 - tx, (b[1] + b[3]) / 2 - ty) / max(bw, 1) for b in B]
98
+ j = int(np.argmin(d))
99
+ if d[j] > 0.35: # the found face is not the one in `box`
100
+ return None
101
+ return self.embed_aligned(crop, K[j])
102
+
103
+ def reference_embeddings(self, paths):
104
+ """One embedding per reference photo (largest face in each). Raises if a photo has no usable face."""
105
+ embs = []
106
+ for p in paths:
107
+ img = cv2.imread(str(p))
108
+ if img is None:
109
+ raise SystemExit(f"cannot read reference image {p}")
110
+ B, S, K = self.detect(img, size=640, thr=0.5)
111
+ if len(B) == 0:
112
+ raise SystemExit(f"no face found in reference image {p}")
113
+ if len(B) > 1:
114
+ print(f"warning: {len(B)} faces in {p}; using the largest")
115
+ j = int(np.argmax((B[:, 2] - B[:, 0]) * (B[:, 3] - B[:, 1])))
116
+ e = self.embed_aligned(img, K[j])
117
+ if e is None:
118
+ raise SystemExit(f"could not align face in {p}")
119
+ embs.append(e)
120
+ return np.stack(embs)
121
+
122
+ @staticmethod
123
+ def similarity(emb, refs):
124
+ """Best cosine similarity of `emb` against the reference embeddings."""
125
+ return float((refs @ emb).max())
code/quantize_int8.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Post-training static INT8 quantization (QDQ, per-channel weights) of the face detector ONNX.
3
+
4
+ Usage: python quantize_int8.py export/face_yolo11n_fp32.onnx export/face_yolo11n_int8.onnx [--calib 300]
5
+ """
6
+ import argparse
7
+ import random
8
+ from pathlib import Path
9
+
10
+ import cv2
11
+ import numpy as np
12
+ import onnx
13
+ from onnxruntime.quantization import CalibrationDataReader, CalibrationMethod, QuantFormat, QuantType, quantize_static
14
+
15
+
16
+ def letterbox(img, size=640):
17
+ h, w = img.shape[:2]
18
+ s = min(size / h, size / w)
19
+ nh, nw = round(h * s), round(w * s)
20
+ img = cv2.resize(img, (nw, nh), interpolation=cv2.INTER_LINEAR)
21
+ canvas = np.full((size, size, 3), 114, np.uint8)
22
+ top, left = (size - nh) // 2, (size - nw) // 2
23
+ canvas[top:top + nh, left:left + nw] = img
24
+ return canvas
25
+
26
+
27
+ class Reader(CalibrationDataReader):
28
+ def __init__(self, files, input_name):
29
+ self.it = iter(files)
30
+ self.name = input_name
31
+
32
+ def get_next(self):
33
+ f = next(self.it, None)
34
+ if f is None:
35
+ return None
36
+ img = letterbox(cv2.imread(str(f)))[:, :, ::-1].transpose(2, 0, 1) # BGR->RGB, CHW
37
+ return {self.name: (img[None].astype(np.float32) / 255.0)}
38
+
39
+
40
+ def main():
41
+ ap = argparse.ArgumentParser()
42
+ ap.add_argument("src")
43
+ ap.add_argument("dst")
44
+ ap.add_argument("--calib", type=int, default=300)
45
+ ap.add_argument("--images", default="dataset/images/train")
46
+ a = ap.parse_args()
47
+
48
+ random.seed(0)
49
+ files = sorted(Path(a.images).glob("*.jpg"))
50
+ random.shuffle(files)
51
+ files = files[:a.calib]
52
+ g = onnx.load(a.src).graph
53
+ inp = g.input[0].name
54
+ # The Detect head's decode (DFL box decoding, class Sigmoid, final Concats) mixes box values up to 640 with
55
+ # 0-1 class scores in one tensor; INT8 would give them one shared scale and crush the scores. Keep it float.
56
+ exclude = [n.name for n in g.node
57
+ if n.name.startswith("/model.23/") and not n.name.startswith(("/model.23/cv2.", "/model.23/cv3."))]
58
+ print(f"keeping {len(exclude)} decode nodes in float")
59
+ quantize_static(a.src, a.dst, Reader(files, inp), quant_format=QuantFormat.QDQ, per_channel=True,
60
+ weight_type=QuantType.QInt8, activation_type=QuantType.QUInt8,
61
+ calibrate_method=CalibrationMethod.MinMax, nodes_to_exclude=exclude)
62
+ # keep ultralytics metadata (class names, stride, imgsz) so the model loads like the original
63
+ src, q = onnx.load(a.src), onnx.load(a.dst)
64
+ del q.metadata_props[:]
65
+ q.metadata_props.extend(src.metadata_props)
66
+ onnx.save(q, a.dst)
67
+ print(f"RES wrote {a.dst}: {Path(a.dst).stat().st_size / 1e6:.1f} MB (from {Path(a.src).stat().st_size / 1e6:.1f} MB)")
68
+
69
+
70
+ if __name__ == "__main__":
71
+ main()
code/recall_at_conf.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Face recall at a fixed confidence threshold on WIDER val (what fraction of GT faces get a box with IoU>=0.5)."""
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+ import numpy as np
7
+ from ultralytics import YOLO
8
+
9
+
10
+ def iou(a, b):
11
+ x1 = np.maximum(a[:, None, 0], b[None, :, 0]); y1 = np.maximum(a[:, None, 1], b[None, :, 1])
12
+ x2 = np.minimum(a[:, None, 2], b[None, :, 2]); y2 = np.minimum(a[:, None, 3], b[None, :, 3])
13
+ inter = np.clip(x2 - x1, 0, None) * np.clip(y2 - y1, 0, None)
14
+ aa = (a[:, 2] - a[:, 0]) * (a[:, 3] - a[:, 1]); ab = (b[:, 2] - b[:, 0]) * (b[:, 3] - b[:, 1])
15
+ return inter / (aa[:, None] + ab[None, :] - inter + 1e-9)
16
+
17
+
18
+ def main():
19
+ ap = argparse.ArgumentParser()
20
+ ap.add_argument("--model", default="export/face_yolo11n_fp16.onnx")
21
+ ap.add_argument("--conf", type=float, default=0.25)
22
+ ap.add_argument("--iou", type=float, default=0.5)
23
+ ap.add_argument("--val", default="dataset/images/val")
24
+ ap.add_argument("--device", default="0", help="0 for GPU, cpu for CPU")
25
+ ap.add_argument("--imgsz", type=int, default=640, help="inference size (.pt models only; ONNX exports are fixed)")
26
+ a = ap.parse_args()
27
+
28
+ model = YOLO(a.model, task="detect")
29
+ files = sorted(Path(a.val).glob("*.jpg"))
30
+ hits, sizes, n_pred, n_fp = [], [], 0, 0
31
+ for i in range(0, len(files), 1): # ONNX export is fixed batch=1
32
+ batch = files[i:i + 1]
33
+ for f, r in zip(batch, model.predict([str(x) for x in batch], conf=a.conf, imgsz=a.imgsz, device=a.device, verbose=False)):
34
+ h, w = r.orig_shape
35
+ gt = np.array([[float(v) for v in l.split()[1:]] for l in
36
+ Path(str(f).replace("images", "labels").replace(".jpg", ".txt")).read_text().split("\n") if l.strip()])
37
+ g = np.stack([(gt[:, 0] - gt[:, 2] / 2) * w, (gt[:, 1] - gt[:, 3] / 2) * h,
38
+ (gt[:, 0] + gt[:, 2] / 2) * w, (gt[:, 1] + gt[:, 3] / 2) * h], 1)
39
+ p = r.boxes.xyxy.cpu().numpy()
40
+ n_pred += len(p)
41
+ if len(p):
42
+ m = iou(g, p)
43
+ hits += list(m.max(1) >= a.iou)
44
+ n_fp += int((m.max(0) < a.iou).sum())
45
+ else:
46
+ hits += [False] * len(g)
47
+ sizes += list(gt[:, 3] * h) # face height in px at the image's stored (<=640) scale
48
+ hits, sizes = np.array(hits), np.array(sizes)
49
+ print(f"RES faces={len(hits)} overall recall@conf{a.conf}/IoU{a.iou} = {hits.mean():.1%} (false-positive boxes: {n_fp}/{n_pred})")
50
+ for lo, hi in [(0, 10), (10, 20), (20, 40), (40, 80), (80, 1e9)]:
51
+ m = (sizes >= lo) & (sizes < hi)
52
+ print(f"RES face height {lo:>3}-{int(min(hi, 999)):>3}px: {hits[m].mean():6.1%} (n={m.sum()})")
53
+
54
+
55
+ if __name__ == "__main__":
56
+ main()
code/train.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Fine-tune YOLO11n (COCO-pretrained) as a single-class face detector on converted WIDER FACE.
3
+
4
+ Adds video-like degradation (motion blur + JPEG compression) on top of ultralytics' default augmentation,
5
+ implemented with plain cv2 so no albumentations / second OpenCV build is needed.
6
+
7
+ Usage:
8
+ python train.py # full run (40 epochs)
9
+ python train.py --fraction 0.02 --epochs 1 --name smoke # quick pipeline check
10
+ """
11
+ import argparse
12
+ import random
13
+ from pathlib import Path
14
+
15
+ import cv2
16
+ import numpy as np
17
+ from ultralytics import YOLO
18
+ from ultralytics.data import augment
19
+
20
+
21
+ def _motion_blur(img, max_k=15):
22
+ k = random.choice(range(3, max_k + 1, 2))
23
+ kernel = np.zeros((k, k), np.float32)
24
+ kernel[k // 2, :] = 1.0
25
+ rot = cv2.getRotationMatrix2D((k / 2 - 0.5, k / 2 - 0.5), random.uniform(0, 180), 1.0)
26
+ kernel = cv2.warpAffine(kernel, rot, (k, k))
27
+ s = kernel.sum()
28
+ return img if s == 0 else cv2.filter2D(img, -1, kernel / s)
29
+
30
+
31
+ def _jpeg(img, qmin=20, qmax=90):
32
+ ok, enc = cv2.imencode(".jpg", img, [cv2.IMWRITE_JPEG_QUALITY, random.randint(qmin, qmax)])
33
+ return cv2.imdecode(enc, cv2.IMREAD_COLOR) if ok else img
34
+
35
+
36
+ def install_video_degradation(p_blur=0.3, p_jpeg=0.3):
37
+ """Wrap ultralytics' Albumentations step (runs after mosaic/affine, train only) with cv2 degradations."""
38
+ orig = augment.Albumentations.__call__
39
+
40
+ def call(self, labels):
41
+ img = labels["img"]
42
+ if random.random() < p_blur:
43
+ img = _motion_blur(img)
44
+ if random.random() < p_jpeg:
45
+ img = _jpeg(img)
46
+ labels["img"] = img
47
+ return orig(self, labels)
48
+
49
+ augment.Albumentations.__call__ = call
50
+
51
+
52
+ def main():
53
+ ap = argparse.ArgumentParser()
54
+ ap.add_argument("--data", default="dataset/data.yaml")
55
+ ap.add_argument("--model", default="yolo11n.pt")
56
+ ap.add_argument("--epochs", type=int, default=40)
57
+ ap.add_argument("--batch", type=int, default=64)
58
+ ap.add_argument("--imgsz", type=int, default=640)
59
+ ap.add_argument("--workers", type=int, default=16)
60
+ ap.add_argument("--fraction", type=float, default=1.0, help="fraction of train set (smoke tests)")
61
+ ap.add_argument("--cache", default="disk", help="ram | disk | False")
62
+ ap.add_argument("--project", default=str(Path(__file__).resolve().parent / "runs"))
63
+ ap.add_argument("--name", default="face_yolo11n")
64
+ ap.add_argument("--no-degrade", action="store_true")
65
+ ap.add_argument("--resume", default=None,
66
+ help="path to a stopped run's last.pt; continues it and finishes at --epochs "
67
+ "(the LR schedule is recomputed for the new total)")
68
+ a = ap.parse_args()
69
+
70
+ if not a.no_degrade:
71
+ install_video_degradation()
72
+
73
+ if a.resume:
74
+ import torch
75
+ ck = torch.load(a.resume, map_location="cpu", weights_only=False)
76
+ ck["train_args"]["epochs"] = a.epochs
77
+ torch.save(ck, a.resume)
78
+ model = YOLO(a.resume)
79
+ model.train(resume=True)
80
+ else:
81
+ model = YOLO(a.model)
82
+ model.train(
83
+ data=a.data, epochs=a.epochs, imgsz=a.imgsz, batch=a.batch, workers=a.workers,
84
+ fraction=a.fraction, cache=False if a.cache == "False" else a.cache, amp=True,
85
+ close_mosaic=5, patience=0, project=a.project, name=a.name, exist_ok=True,
86
+ plots=False,
87
+ )
88
+ m = model.val(data=a.data, imgsz=a.imgsz, batch=a.batch)
89
+ print(f"RESULT mAP50={m.box.map50:.4f} mAP50-95={m.box.map:.4f} P={m.box.mp:.4f} R={m.box.mr:.4f}")
90
+
91
+
92
+ if __name__ == "__main__":
93
+ main()
yolo11n/face_yolo11n.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:98b9fedf995fdc7bf20c6bbccde5d1ca46b3fb908843207be67794395d6d6a28
3
+ size 5447834
yolo11n/face_yolo11n_fp16.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:958d99534290613fb7fb646467671a893b5ebc9941de385ceea039cc0612e827
3
+ size 5355828
yolo11n/face_yolo11n_fp32.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9506b1f2b497cf0d3c39eb756ecacef82965d6d01e8037f3c2a91f6cb1b22297
3
+ size 10604582
yolo11n/face_yolo11n_int8.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:48a6930873aee81ce40464b13de001aff90757e86f119d08cac642e9e4d80778
3
+ size 3258276
yolo11s/face_yolo11s.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:09cca2067809392d575e947b6be2a7b4bc09123b078c60f5f41155afdae833c9
3
+ size 19155034
yolo11s/face_yolo11s_fp16.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a2be67ca882dd4e694a407a59b1c9762508e3756b7077b8f919799b2ed492e72
3
+ size 19017872
yolo11s/face_yolo11s_fp32.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1c4eac37a7a5efbe791225022799ac8980743bf0271204f43f782b327785c1eb
3
+ size 37928280