Almost every video project we are asked to review now has a compliance thread running through it. A retail analytics client needs to keep six weeks of footage for a dispute process. A traffic survey firm wants to publish clips in a tender document. A robotics team wants to ship a dataset of factory-floor recordings to an offshore annotation vendor. In all three cases the pixels are fine — the faces and the number plates in them are not.
Anonymisation gets treated as a checkbox late in the project ("we'll blur it before we send it"), and it is where the schedule quietly dies. A blur that misses one frame in fifty is not anonymisation. A blur applied to a re-encoded proxy while the original stays on disk is not anonymisation either. This tutorial walks the pipeline we actually build for clients: detect faces and plates reliably across a whole video, smooth the detections over time so nothing flickers back into view, apply an irreversible redaction, and produce an audit artefact that a data protection officer will accept.
Everything here runs on OpenCV 5 with ONNX models through the DNN module, so it deploys the same way on a workstation, a Jetson, or a container in your own VPC — which matters, because raw footage usually cannot leave the client's network at all.
1. Get the threat model right before you write code
Redaction requirements differ enormously, and the difference decides your architecture:
- Publication redaction — a clip going into a report, a demo reel, or a public dataset. Irreversible, applied to the delivered file, and a single missed frame is a real breach.
- Retention redaction — you keep operational footage but strip identifiers so the retention period can be longer. The original is deleted; the redacted copy is the record of truth.
- Training-data redaction — footage shipped to annotators or into a model pipeline. Here you often want the body and the vehicle intact and only the identifying regions destroyed.
- Live redaction — anonymised at the edge before anything is written to disk. Hardest, but the strongest position: identifiable data never persists.
Write down which of these you are doing, in a sentence, and confirm it with whoever owns the risk. Under GDPR, blurred-but-recoverable footage is still personal data; truly anonymised data falls outside the regulation entirely. That distinction is the whole point of the exercise, and it is a legal call, not an engineering one — engineering just has to make the irreversible version actually irreversible.
2. Detection: two small models, not one big one
Faces and plates are different problems and deserve different detectors.
For faces, OpenCV ships cv::FaceDetectorYN (the YuNet model) and it remains the right default: a few hundred kilobytes, runs comfortably at 1080p on CPU, and handles the small, off-angle, partially occluded faces that a generic person detector misses. Load it once and reuse it:
import cv2, numpy as np
face = cv2.FaceDetectorYN.create(
"face_detection_yunet_2023mar.onnx", "", (320, 320),
score_threshold=0.5, nms_threshold=0.3, top_k=5000)
face.setInputSize((W, H)) # must match the frame, or coords are wrong
_, faces = face.detect(frame) # Nx15: x,y,w,h, 5 landmarks, score
Two mistakes cost people days here. setInputSize must be called whenever the frame size changes, or every box lands in the wrong place. And the default score threshold is tuned for demos, not compliance — for redaction you deliberately run it low (0.3–0.4) and accept false positives. Over-blurring a doorframe is a cosmetic complaint; missing a face is a notifiable incident.
For plates, use a small detector fine-tuned on plates (a YOLO-family nano model exported to ONNX is the usual choice) and run it through the OpenCV 5 DNN engine:
net = cv2.dnn.readNet("plate_yolo_n.onnx")
net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV)
net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)
blob = cv2.dnn.blobFromImage(frame, 1/255.0, (640, 640), swapRB=True, crop=False)
net.setInput(blob)
dets = net.forward()
If you are also running detection on Jetson hardware, the TensorRT path in Real-time object detection on Jetson Orin Nano applies unchanged — redaction is just another consumer of the same detector output. And note the OpenCV 5 caveat we covered in migrating from 4.x to 5.0: readNetFromDarknet is gone, so legacy .weights plate models must be converted to ONNX first.
3. Temporal smoothing is what makes it safe
A per-frame detector will drop a face for three frames when someone turns their head. Played back at 25 fps that is an eighth of a second of a fully identifiable person — enough to defeat the entire exercise.
The fix is to treat redaction as a tracking problem, not a detection problem. Associate detections into tracks (IoU matching plus a Kalman predictor is sufficient; the association logic in our multi-object tracking tutorial transfers directly), and then apply two rules that are specific to anonymisation:
- Coast through gaps. When a track has no detection in a frame, keep redacting its predicted box for up to N frames (we typically use 0.5–1.0 s worth). You would never do this for counting — for redaction it is mandatory.
- Redact retroactively. Run the whole clip in two passes. Pass one collects tracks; pass two burns in the redactions, including a lead-in of a few frames before the first confident detection. The frames where a face is just entering the scene are exactly the ones a single-pass detector misses.
Then dilate every box by 15–20% before redacting. Detector boxes hug the face; ears, hairline and jaw carry identifying information, and plate boxes routinely clip a character at the edge.
4. Redact irreversibly
Gaussian blur is the default choice and the wrong one. A fixed-kernel blur is a known, invertible linear operation; deconvolution attacks on blurred plates and faces are a well-documented party trick, and pixelation with a small block size is worse — it is trivially brute-forced for a finite alphabet like a number plate.
Use destructive redaction:
def redact(frame, box, mode="fill"):
x, y, w, h = box
roi = frame[y:y+h, x:x+w]
if roi.size == 0:
return
if mode == "fill":
roi[:] = (0, 0, 0) # strongest, ugliest
else: # "noise" — keeps footage watchable
k = max(3, (min(w, h) // 4) | 1)
blurred = cv2.GaussianBlur(roi, (k, k), 0)
noise = np.random.default_rng().integers(0, 256, roi.shape, dtype=np.uint8)
roi[:] = cv2.addWeighted(blurred, 0.35, noise, 0.65, 0)
Heavy blur plus strong additive noise destroys the information the deconvolution needs while leaving the scene legible enough for operational review. For publication or dataset delivery, prefer the solid fill: the aesthetic objection is much cheaper than the argument about whether 65% noise is sufficient.
Then close the two holes everyone leaves open. First, re-encode the whole file — never write redacted frames into a container alongside the original stream, and never rely on an overlay layer that a player can toggle. Second, strip metadata: GPS coordinates, device serials, and thumbnails embedded by phones and body cameras survive frame redaction untouched and are personal data in their own right.
5. Produce the audit artefact
The deliverable is not just the video. Alongside it, emit a small JSON manifest per file: source hash, output hash, model names and versions, thresholds, coast frames, dilation factor, total tracks redacted, redacted-pixel-seconds per class, and the count of frames where a track was coasted rather than detected. It takes an afternoon to build and it is the single thing that converts "we blurred it" into a defensible process — it is also what an auditor asks for first.
Pair it with a held-out validation set: 200–500 manually labelled frames sampled from the actual client footage, not from WIDER FACE. Report recall at the operating threshold, and treat recall — not mAP, not precision — as the acceptance metric. We usually contract to a stated recall on a client-labelled set rather than to a vague "faces will be blurred", because it makes the residual risk explicit on both sides.
6. Where it typically goes wrong
- Interlaced or rotation-flagged source. Body cams and dashcams write rotation metadata; OpenCV decodes the raw orientation, so you detect on a sideways frame and find nothing. Normalise orientation on ingest.
- Reflections and screens. Faces in mirrors, shop windows and monitors are identifiable and are missed by every off-the-shelf detector. If they matter, you need a second pass or a manual review stage.
- Small, distant faces. Below roughly 20 px YuNet recall falls off. Tile the frame and run detection on overlapping crops for wide-angle or high-mounted cameras.
- The unredacted intermediates. Temp files, ffmpeg scratch dirs, decode caches, and the frame you attached to a bug report. Keep the whole pipeline inside one controlled directory and wipe it on exit.
- Live redaction that buffers. You cannot retroactively redact a stream you have already written. Hold a short frame buffer (equal to your coast window) in memory before writing, and accept the added latency.
Where this fits
Anonymisation sits underneath almost everything else on this blog: it is what lets an inspection dataset leave the building, a retail analytics deployment pass a DPIA, and a traffic study get published. It is also, in our experience, the requirement that appears in week nine of a twelve-week project.
If you are scoping a video pipeline with a privacy dimension — GDPR retention, an EU AI Act assessment, a dataset going to an external annotation vendor, or edge redaction before storage — get in touch. Our OpenCV consultants have built these pipelines for retail, transport and industrial clients, and can usually tell you within a short review whether your current approach would survive an audit or needs rebuilding before it ships.