Instructions to use Banaxi-Tech/face-model with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- ultralytics
How to use Banaxi-Tech/face-model with ultralytics:
from huggingface_hub import hf_hub_download from ultralytics import YOLO # pick the weights file from this repo's "Files and versions" tab weights = hf_hub_download("Banaxi-Tech/face-model", "<weights>.pt") model = YOLO(weights) source = 'http://images.cocodataset.org/val2017/000000039769.jpg' model.predict(source=source, save=True) - Notebooks
- Google Colab
- Kaggle
File size: 12,769 Bytes
d176ecd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 | #!/usr/bin/env python3
"""Detect, track and pixelate (or black out) faces in a video. Audio is preserved via ffmpeg.
Detection runs every N frames; between detections ByteTrack's Kalman filter carries the boxes forward.
Tuned for recall (privacy): low confidence threshold, box padding, raw detections are blurred immediately
(without waiting for track confirmation) and recently lost tracks are held for a few frames.
--keep: leave ONE person's face visible and blur everybody else. Give one or more reference photos of that
person. Fail-safe by design: a face stays blurred unless its track was confirmed as the target by several
consecutive face-recognition matches; a clear non-match, a track box jump, or going too long without a
successful re-check puts the blur back.
Examples:
python blur_video.py in.mp4 out.mp4 --model export/face_yolo11n_fp16.onnx -n 1
python blur_video.py in.mp4 out.mp4 --mode black
python blur_video.py in.mp4 out.mp4 -n 1 --keep me1.jpg me2.jpg
"""
import argparse
import queue
import shutil
import subprocess
import sys
import threading
import time
from types import SimpleNamespace
import cv2
import numpy as np
from ultralytics import YOLO
from ultralytics.trackers.byte_tracker import BYTETracker
def pad_box(x1, y1, x2, y2, pad, w, h):
bw, bh = x2 - x1, y2 - y1
x1, x2 = x1 - bw * pad, x2 + bw * pad
y1, y2 = y1 - bh * pad, y2 + bh * pad
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)))
def iou(a, b):
ix = max(0.0, min(a[2], b[2]) - max(a[0], b[0]))
iy = max(0.0, min(a[3], b[3]) - max(a[1], b[1]))
inter = ix * iy
union = (a[2] - a[0]) * (a[3] - a[1]) + (b[2] - b[0]) * (b[3] - b[1]) - inter
return inter / union if union > 0 else 0.0
def pixelate(frame, box, blocks):
"""Downscale the region so its shorter side has `blocks` cells, then nearest-neighbour upscale."""
x1, y1, x2, y2 = box
if x2 - x1 < 2 or y2 - y1 < 2:
return
roi = frame[y1:y2, x1:x2]
h, w = roi.shape[:2]
cell = max(1.0, min(w, h) / blocks)
small = cv2.resize(roi, (max(1, round(w / cell)), max(1, round(h / cell))), interpolation=cv2.INTER_AREA)
frame[y1:y2, x1:x2] = cv2.resize(small, (w, h), interpolation=cv2.INTER_NEAREST)
def blackout(frame, box):
x1, y1, x2, y2 = box
frame[y1:y2, x1:x2] = 0
def open_ffmpeg_writer(src, dst, w, h, fps, crf):
cmd = [
"ffmpeg", "-y", "-loglevel", "error",
"-f", "rawvideo", "-pix_fmt", "bgr24", "-s", f"{w}x{h}", "-r", f"{fps}", "-i", "-",
"-i", src,
"-map", "0:v:0", "-map", "1:a?",
"-c:v", "libx264", "-preset", "fast", "-crf", str(crf), "-pix_fmt", "yuv420p",
"-c:a", "copy", "-shortest", "-movflags", "+faststart", dst,
]
return subprocess.Popen(cmd, stdin=subprocess.PIPE)
class KeepTracker:
"""Decides which ByteTrack ids are the person to keep visible. Everything not confirmed stays blurred."""
def __init__(self, fid, refs, thr, min_size, hits_needed, recheck, expire, budget):
self.fid, self.refs, self.thr, self.min_size = fid, refs, thr, min_size
self.hits_needed, self.recheck, self.expire, self.budget = hits_needed, recheck, expire, budget
self.state = {} # tid -> dict(hits, kept, last_ok, last_try, box)
self.seen, self.ever_kept = set(), set()
def is_kept(self, tid, idx):
s = self.state.get(tid)
if s is None or not s["kept"]:
return False
if idx - s["last_ok"] > self.expire: # could not re-confirm for too long -> blur again
s["kept"], s["hits"] = False, 0
return False
return True
def update(self, frame, idx, tracked):
"""tracked: list of (xyxy, tid) from a detection frame (clean, un-blurred frame)."""
todo = []
for box, tid in tracked:
self.seen.add(tid)
s = self.state.setdefault(tid, dict(hits=0, kept=False, last_ok=-10**9, last_try=-10**9, box=box))
if s["kept"] and iou(s["box"], box) < 0.3: # box jumped: possible ID switch -> distrust
s["kept"], s["hits"] = False, 0
s["box"] = box
if not self.is_kept(tid, idx):
todo.append((0, box, tid)) # undecided first
elif idx - s["last_try"] >= self.recheck:
todo.append((1, box, tid))
todo.sort(key=lambda t: (t[0], self.state[t[2]]["last_try"]))
for _, box, tid in todo[:self.budget]:
s = self.state[tid]
s["last_try"] = idx
e = self.fid.embed_box(frame, box, min_size=self.min_size)
if e is None:
continue # inconclusive: keep old state (a kept track lapses via `expire`)
if self.fid.similarity(e, self.refs) >= self.thr:
s["hits"] += 1
s["last_ok"] = idx
if s["hits"] >= self.hits_needed:
s["kept"] = True
self.ever_kept.add(tid)
else: # clear non-match: blur immediately
s["hits"], s["kept"] = 0, False
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("input")
ap.add_argument("output")
ap.add_argument("--model", default="runs/face_yolo11n/weights/best.pt", help=".pt or .onnx face detector")
ap.add_argument("-n", "--every", type=int, default=3, help="run detector every N frames (default 3)")
ap.add_argument("--conf", type=float, default=0.25, help="detection confidence threshold (default 0.25)")
ap.add_argument("--pad", type=float, default=0.15, help="pad each box by this fraction per side (default 0.15)")
ap.add_argument("--mode", choices=["pixelate", "black"], default="pixelate",
help="pixelate faces (default) or fill the padded box with solid black")
ap.add_argument("--blocks", type=int, default=8,
help="pixel blocks across a face's shorter side; LOWER = stronger pixelation (default 8)")
ap.add_argument("--hold", type=int, default=8, help="keep blurring a lost track's prediction this many frames")
ap.add_argument("--imgsz", type=int, default=640)
ap.add_argument("--device", default=None, help="e.g. 0 or cpu (default: auto)")
ap.add_argument("--crf", type=int, default=18, help="x264 quality (lower = better)")
k = ap.add_argument_group("keep one person visible (face recognition)")
k.add_argument("--keep", nargs="+", metavar="REF.jpg",
help="reference photo(s) of the person to leave unblurred; everyone else is blurred")
k.add_argument("--keep-threshold", type=float, default=0.45,
help="cosine similarity needed to count as the same person; HIGHER = stricter (default 0.45)")
k.add_argument("--keep-hits", type=int, default=2, help="consecutive matches before a track is unblurred")
k.add_argument("--keep-min-size", type=int, default=40,
help="faces smaller than this many px are never recognised, so they stay blurred (default 40)")
k.add_argument("--keep-recheck", type=int, default=8, help="re-verify a kept track every this many frames")
k.add_argument("--keep-expire", type=int, default=24,
help="blur a kept track again if it could not be re-confirmed for this many frames")
k.add_argument("--keep-budget", type=int, default=4, help="max recognitions per frame (speed cap)")
a = ap.parse_args()
if shutil.which("ffmpeg") is None:
sys.exit("ffmpeg not found on PATH")
if a.every < 1 or a.blocks < 1:
sys.exit("--every and --blocks must be >= 1")
keeper = None
if a.keep:
from face_id import FaceID
fid = FaceID("cpu" if a.device == "cpu" else "0")
refs = fid.reference_embeddings(a.keep)
print(f"keeping the person in {len(refs)} reference photo(s); threshold {a.keep_threshold}")
keeper = KeepTracker(fid, refs, a.keep_threshold, a.keep_min_size, a.keep_hits,
a.keep_recheck, a.keep_expire, a.keep_budget)
cap = cv2.VideoCapture(a.input)
if not cap.isOpened():
sys.exit(f"cannot open {a.input}")
fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) or 0
ok, frame = cap.read()
if not ok:
sys.exit("no frames in input")
H, W = frame.shape[:2]
model = YOLO(a.model, task="detect")
tracker = BYTETracker(SimpleNamespace(
track_high_thresh=a.conf, track_low_thresh=0.1, new_track_thresh=a.conf,
track_buffer=max(30, 4 * a.every), match_thresh=0.8, fuse_score=True))
writer = open_ffmpeg_writer(a.input, a.output, W, H, fps, a.crf)
# Decode and encode run in their own threads so they overlap with detection (frame order is preserved:
# one reader, one writer, FIFO queues). cv2/ffmpeg pipe I/O release the GIL.
rq, wq, write_failed = queue.Queue(maxsize=48), queue.Queue(maxsize=48), []
first_frame = frame
def reader():
rq.put(first_frame)
while True:
okr, f = cap.read()
if not okr:
rq.put(None)
return
rq.put(f)
def writer_loop():
try:
while True:
f = wq.get()
if f is None:
return
writer.stdin.write(f.tobytes())
except (BrokenPipeError, OSError):
write_failed.append(True)
while wq.get() is not None: # drain so the main thread never blocks on a full queue
pass
threading.Thread(target=reader, daemon=True).start()
wthread = threading.Thread(target=writer_loop)
wthread.start()
t0 = time.perf_counter()
idx = n_det = 0
while True:
frame = rq.get()
if frame is None:
break
items = [] # (xyxy, track_id or None)
if idx % a.every == 0:
r = model.predict(frame, conf=a.conf, imgsz=a.imgsz, device=a.device, verbose=False)[0]
det = r.boxes.cpu().numpy()
n_det += 1
tracked = tracker.update(det, frame)
items += [(b[:4], int(b[4])) for b in tracked] # confirmed tracks
items += [(b, None) for b in det.xyxy] # raw detections: don't wait for track confirmation
items += [(t.xyxy, t.track_id) for t in tracker.tracked_stracks if not t.is_activated]
items += [(t.xyxy, t.track_id) for t in tracker.lost_stracks if tracker.frame_id - t.end_frame <= a.hold]
if keeper: # recognise on the clean frame, before anything is drawn
keeper.update(frame, idx, [(b[:4], int(b[4])) for b in tracked])
else:
# no detector this frame: advance Kalman state of every live track and use the prediction
tracker.frame_id += 1
pool = tracker.tracked_stracks + tracker.lost_stracks
if pool:
tracker.multi_predict(pool)
items += [(t.xyxy, t.track_id) for t in tracker.tracked_stracks]
items += [(t.xyxy, t.track_id) for t in tracker.lost_stracks if tracker.frame_id - t.end_frame <= a.hold]
if keeper:
kept_boxes = [np.asarray(b, dtype=float)[:4] for b, tid in items
if tid is not None and keeper.is_kept(tid, idx)]
items = [(b, tid) for b, tid in items
if not (tid is not None and keeper.is_kept(tid, idx))
and not (tid is None and any(iou(np.asarray(b, dtype=float)[:4], kb) >= 0.5 for kb in kept_boxes))]
for b, _ in items:
box = pad_box(*np.asarray(b, dtype=float)[:4], a.pad, W, H)
if a.mode == "black":
blackout(frame, box)
else:
pixelate(frame, box, a.blocks)
wq.put(frame)
idx += 1
if idx % 100 == 0:
el = time.perf_counter() - t0
print(f"\r{idx}/{total or '?'} frames {idx / el:.1f} FPS", end="", flush=True)
wq.put(None)
wthread.join()
cap.release()
try:
writer.stdin.close()
except (BrokenPipeError, OSError):
pass
rc = writer.wait()
if write_failed:
rc = rc or 1
el = time.perf_counter() - t0
print(f"\rdone: {idx} frames in {el:.1f}s = {idx / el:.1f} FPS processing "
f"(detector ran on {n_det} frames, every {a.every}) -> {a.output}")
if keeper:
print(f"keep: {len(keeper.ever_kept)} of {len(keeper.seen)} tracks were recognised as the target and left visible")
if rc != 0:
sys.exit(f"ffmpeg exited with code {rc}")
if __name__ == "__main__":
main()
|