Showing posts with label Video Processing. Show all posts
Showing posts with label Video Processing. Show all posts

Sunday, November 8, 2026

How to Blur People in Video with Python, YOLO and OpenCV

Can Python blur people in a video automatically? This demonstration uses a pretrained YOLO detector and OpenCV to find person bounding boxes in each frame and blur the detected rectangles. It creates a new video locally.

This is a visual-effect demonstration, not a guarantee of anonymity. A detector can miss a person, a rectangle can leave identifying detail outside it, and the blur may be too weak. Check all output carefully and use an appropriate, independently validated privacy process before sharing real footage.

Watch the tutorial

See the full demonstration and the missed-detection example in the YouTube tutorial, or watch the short demonstration.

Before and after

Both images below are from frame 120 of the same synthetic input. The first is the source frame; the second is an actual frame from the processed video, not a simulated illustration. The rectangular area comes from one detected person and includes padding around the box.

Original synthetic video frame showing a person exercising
Frame 120 of the permitted synthetic input, before detection and blurring.
Same frame from the output video, with the detected person blurred
Frame 120 of the processed MP4 at confidence threshold 0.25; the detected person rectangle and some surrounding background have been blurred.

How the example works

  1. Read the input. A video is a sequence of images called frames. OpenCV reads its frames, dimensions and frames per second (FPS).
  2. Detect people. YOLO predicts boxes in each frame. A bounding box is a rectangle around a predicted object; the script requests class 0, which is person for this checkpoint. Confidence is a model score, not a guarantee that a person or all of their pixels are detected.
  3. Blur the selected region. The code expands each box by 8% in each direction, clips it to the frame, and applies a 71 × 71 Gaussian blur to the nonempty rectangle. This is a region of interest, or ROI. The rest of the frame remains outside that operation.
  4. Write the result. OpenCV writes the processed frames to a new MP4 at the input video's reported FPS. The sample does not track identities across frames, and it does not copy an audio track to the output.

Bounding boxes cover rectangles, not exact person outlines: some background can be blurred as well. A person-class detector is not automatically a face or licence-plate detector. Different videos, versions and settings can produce different detections.

What you need

The actual run used Windows CPU with Python 3.13.9, Ultralytics 8.4.24, OpenCV 4.13.0, NumPy 2.3.5 and PyTorch 2.10.0+cpu. Use a permitted video and the official YOLOv8n checkpoint. The demonstration input is a synthetic exercise clip; course students can find squat_demo.mp4 in the 04_zasoby folder of the lesson 52 practice ZIP and copy it to the project folder as input.mp4. A different input can produce different results.

On Windows Command Prompt, create a virtual environment and install the packages:

py -3.13 -m venv .venv
.venv\Scripts\python.exe -m pip install ultralytics==8.4.24

Package installation and acquiring weights require an internet connection unless already cached. This command pins the direct package, not every dependency; a fresh online installation has not been independently verified. The demonstrated prediction was repeated on Windows CPU with the available environment and local files.

Place the permitted source video as input.mp4, the model as yolov8n.pt, and the code below as blur_people.py in the project directory. The script checks that the input and weights exist; it doesn't silently choose an alternative model or video.

Complete Python example

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright (c) 2026 ARTSCI - Author of the AI/ML/CV course materials.
"""Detect people and blur their detected rectangles in a video on CPU.

This example demonstrates a visual effect. Missed detections or weak blur may
leave identifying details visible: do not treat output as anonymized footage.
"""
import argparse
import json
import math
import os
import time
from pathlib import Path

import cv2


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--video', type=Path, required=True)
    parser.add_argument('--weights', type=Path, required=True)
    parser.add_argument('--output', type=Path, default=Path('blur_output'))
    parser.add_argument('--confidence', type=float, default=0.25)
    args = parser.parse_args()
    if not (0 <= args.confidence <= 1):
        raise ValueError('confidence must be between 0 and 1')
    for name, path in (('Video', args.video), ('Weights', args.weights)):
        if not path.is_file():
            raise FileNotFoundError(f'{name} not found: {path}')
    args.output.mkdir(parents=True, exist_ok=True)
    config = args.output / 'config'
    config.mkdir(exist_ok=True)
    os.environ.setdefault('YOLO_CONFIG_DIR', str(config.resolve()))
    os.environ.setdefault('YOLO_OFFLINE', 'true')
    os.environ.setdefault('YOLO_AUTOINSTALL', 'false')
    from ultralytics import YOLO
    import torch
    torch.set_num_threads(4)
    model = YOLO(str(args.weights))

    cap = cv2.VideoCapture(str(args.video))
    if not cap.isOpened():
        raise RuntimeError(f'Cannot open video: {args.video}')
    fps = cap.get(cv2.CAP_PROP_FPS)
    width, height = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)), int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
    if not math.isfinite(fps) or fps <= 0 or width <= 0 or height <= 0:
        raise ValueError('Video FPS and dimensions must be valid')
    out = args.output / 'blurred.mp4'
    writer = cv2.VideoWriter(str(out), cv2.VideoWriter_fourcc(*'mp4v'), fps, (width, height))
    if not writer.isOpened():
        raise RuntimeError(f'Cannot create video: {out}')
    frame_no = 0
    person_boxes = 0
    frames_with_detections = 0
    records = []
    begin = time.perf_counter()
    try:
        while True:
            ok, frame = cap.read()
            if not ok:
                break
            result = model.predict(frame, classes=[0], conf=args.confidence, imgsz=640,
                                   device='cpu', verbose=False)[0]
            boxes = []
            for box in result.boxes:
                x1, y1, x2, y2 = map(float, box.xyxy[0].tolist())
                # Expand by 8 percent to reduce unblurred edges, and clamp to the image.
                pad_x, pad_y = (x2 - x1) * 0.08, (y2 - y1) * 0.08
                left = max(0, math.floor(x1 - pad_x))
                top = max(0, math.floor(y1 - pad_y))
                right = min(width, math.ceil(x2 + pad_x))
                bottom = min(height, math.ceil(y2 + pad_y))
                if right <= left or bottom <= top:
                    continue
                boxes.append({'xyxy': [left, top, right, bottom],
                              'confidence': round(float(box.conf.item()), 6)})
                region = frame[top:bottom, left:right]
                frame[top:bottom, left:right] = cv2.GaussianBlur(region, (71, 71), 0)
            if boxes:
                frames_with_detections += 1
                person_boxes += len(boxes)
            records.append({'frame': frame_no, 'boxes': boxes})
            writer.write(frame)
            frame_no += 1
    finally:
        cap.release()
        writer.release()
    seconds = time.perf_counter() - begin
    if frame_no == 0:
        raise ValueError('Input contained no decodable frames')
    summary = {'frames': frame_no, 'frames_with_person_detections': frames_with_detections,
               'frames_without_person_detections': frame_no - frames_with_detections,
               'person_boxes': person_boxes, 'fps_source': fps, 'width': width, 'height': height,
               'confidence': args.confidence, 'imgsz': 640, 'box_padding_fraction': 0.08,
               'gaussian_kernel': [71, 71], 'device': 'cpu', 'processing_seconds': round(seconds, 3),
               'processing_frames_per_second': round(frame_no / seconds, 3),
               'output': str(out), 'warning': 'Blur is not a privacy or anonymization guarantee.'}
    (args.output / 'result.json').write_text(json.dumps(summary, indent=2), encoding='utf-8')
    (args.output / 'detections.json').write_text(json.dumps(records, indent=2), encoding='utf-8')
    print(json.dumps(summary, indent=2))


if __name__ == '__main__':
    main()

Run the script from the project directory:

.venv\Scripts\python.exe blur_people.py --video input.mp4 --weights yolov8n.pt --output blur_output

The script writes blur_output/blurred.mp4, a machine-readable blur_output/result.json summary, and blur_output/detections.json with per-frame box coordinates and scores. The detection JSON reports predictions: its presence does not establish that the output is anonymous or correct.

Read the result

For this specific test, the input has 241 frames at 24 FPS, with a frame size of 1280 × 720. With a confidence threshold of 0.25, image size 640, and this particular checkpoint, the program returned one person box in each of the 241 frames. This is a count of model outputs in this sample. It is not recall, pixel coverage, a guarantee of complete concealment, or evidence for other recordings.

The example clamps box coordinates before slicing to avoid indexing outside the frame. cv2.GaussianBlur smooths pixels inside that rectangle, while VideoWriter saves the resulting frames. See the OpenCV guide to image smoothing and the Ultralytics object-blurring guide for further background; the current online documentation can describe a later version than the one tested here.

What happens when a person is not detected?

In a separate run we deliberately raised --confidence to 0.99. The detector returned zero person boxes in all 241 frames, so no person rectangle was blurred. In the 0.25 run, the frame-120 person box had a model score of approximately 0.938850, below the deliberately selected 0.99 threshold. This controlled threshold experiment is deliberately different from the 0.25 demonstration. It illustrates a missed detection, not an estimate of the model's error rate in real use. To reproduce this test, use the same input and model:

.venv\Scripts\python.exe blur_people.py --video input.mp4 --weights yolov8n.pt --output high_threshold_output --confidence 0.99
Frame from the high confidence experiment with no person rectangle detected
Frame 120 of the output at the deliberately raised confidence threshold 0.99: no person rectangle was returned and the source remains visible.

If no box is returned for a frame, this script does not infer a hidden location from earlier frames: that frame is written without that ROI being blurred. Even if a box is returned, it may be misplaced or insufficient. Appearance, clothing, posture, faces outside the box, reflections or background context can still identify someone. Strong visual obscuration does not by itself establish legal anonymization.

Try it yourself

  1. Start with footage you have permission to use, without real personal data where possible.
  2. Run the script with the tested settings and inspect the original alongside the output, including difficult frames, the start and end, small subjects and partial occlusions.
  3. Open detections.json and compare its boxes to what is actually visible. Detection count alone is not a ground-truth assessment.
  4. Try a different confidence threshold using --confidence and compare the same frames. A lower threshold may add detections as well as false positives; it does not guarantee that all people are hidden.

See also my guide to removing an image background with YOLO segmentation, and explore more practical applications in my Computer Vision: OpenCV, Machine Learning and YOLO course.

Code and component licenses

The complete example script on this page is © 2026 ARTSCI - Author of the AI/ML/CV course materials, licensed under GNU AGPL-3.0 only, without warranty. Preserve applicable notices and provide corresponding source when sharing covered modifications. This code notice does not automatically license the sample video, course content or other media.

Ultralytics software and model weights retain their own terms; review the Ultralytics assets license and licensing information for your intended use. Other dependencies have their own licenses.