A two-layer YOLO pipeline for detecting and obscuring faces and number plates in 360° equirectangular street-view imagery.
Layer 1 detects people and vehicles in full-resolution images. Testing mode (optional) builds two sampled sets from the just-processed Layer 1 images — the busiest for people, the busiest for vehicles — and runs Layer 2, Blur, and Visualize on each independently, for a fast quality check on both detection types before committing to a full run. Layer 2 crops each detection and runs two custom models — one for faces, one for number plates — back-projecting results into the original image coordinate space. Blur obscures every face / plate (gaussian / box / pixelate / solid, with configurable opacity, shape, and padding) and preserves the original EXIF metadata in the output, with orientation corrected so viewers don't double-rotate it. Visualize draws all three detection layers onto a compressed preview image for quick inspection.
run.py runs the stages in that order — Layer 1 → Testing mode → Layer
2 → Blur → Visualize — toggling each via run.py's run_* config
keys; Testing mode defaults off since it's a QA step, not part of a
normal production run.
- Fill in
config.yaml(paths, models, toggles). - Run the full pipeline:
python run.py
Every script also accepts -c/--config to use a config file other
than config.yaml:
python run.py --config other_config.yaml
Or run any stage individually:
python detect_layer1.py
python test_mode.py # optional — needs Layer 1 done first, see below
python detect_layer2.py
python blur.py
python visualize.py
Each stage skips images it has already processed, so runs are safely
resumable. Set mode: "reprocess" in config.yaml to force everything
to regenerate instead. Every progress line is prefixed with
[project_dir_name] STAGE (e.g. [TNG_Yolo_Run] LAYER 1) so parallel
or interleaved runs are easy to tell apart, and if a run picks up
partially-completed output it prints a one-line
Resuming previous run — X/Y already completed, Z remaining. notice
before starting.
# Paths — output folders are created automatically inside project_dir
src_images: "path/to/source/images"
project_dir: "path/to/output/root"
# Models
# layer1_model must be a pure-conv architecture (yolov8*). Attention-based
# models (yolo11/yolo26/...) OOM at layer1_imgsz=6400 — self-attention memory
# scales quadratically with spatial resolution, and this pipeline's tiny-
# object tuning depends on that large imgsz.
layer1_model: "yolov8s.pt"
layer2_model_faces: "path/to/faces_best.pt"
layer2_model_plates: "path/to/plates_best.pt"
# Layer 1 — detection on full images
layer1_conf: 0.5
layer1_iou: 0.3
layer1_imgsz: 6400
layer1_classes: [0, 2, 3, 5, 7] # person, car, motorcycle, bus, truck
# Layer 2 — separate params per model
layer2_faces_conf: 0.2
layer2_faces_iou: 0.1
layer2_faces_imgsz: 640
layer2_plates_conf: 0.2
layer2_plates_iou: 0.1
layer2_plates_imgsz: 640
# Blur behaviour
blur_method: "gaussian" # gaussian | box | pixelate | solid
blur_kernel: 41
pixelate_size: 12
blur_color: [0, 0, 0] # BGR, used when blur_method is "solid"
blur_opacity: 1.0 # 0.0 = untouched, 1.0 = fully obscured
blur_shape: "rect" # rect | ellipse
blur_expand: 0.0 # +10% per side
# Device: "cpu" | "0" | [0,1,2,3] for multi-GPU
device: "0"
# Testing mode — Layer 2 + Blur + viz on two busiest-subset samples
test_mode_count: 500 # total; split evenly into a person-focused set and
# a vehicle-focused set (car/motorcycle/bus/truck)
test_viz_quality: 50 # JPEG quality for test_mode/{person,vehicles}/viz_output
# (blurred_output uses blur_* settings and JPEG
# quality same as a full run)
# resume: skip images/labels that already have output
# reprocess: ignore existing output and regenerate everything
mode: "resume"
# Toggle stages (used by run.py)
run_layer1: true
run_test_mode: false # opt-in — see "Testing mode" at the top and the
# test_mode.py row in Scripts below
run_layer2: true
run_blur: true
run_viz: trueEither
layer2_model_facesorlayer2_model_platescan be left blank if you only have one model — the other is simply skipped.
project_dirwill containlayer1_labels/,layer2_labels/,blurred_output/, andviz_output/, each created automatically. Blur behaviour (method, opacity, shape, padding) is configured via theblur_*keys inconfig.yaml.
| Script | What it does |
|---|---|
run.py |
Runs all enabled stages in order. Toggle each stage in config.yaml. |
detect_layer1.py |
Runs the general YOLO model on src_images. Writes YOLO .txt label files to layer1_labels. |
detect_layer2.py |
Crops Layer 1 detections, runs the face model (class 0) and plate model (class 1), back-projects results, writes to layer2_labels. |
blur.py |
Blurs all annotated regions from layer2_labels. Saves to blurred_output (JPEG 99%). Preserves EXIF (with corrected orientation). |
visualize.py |
Draws detection boxes by layer and type. Saves to viz_output at 30% JPEG quality. |
test_mode.py |
Splits test_mode_count evenly into a person-focused and a vehicle-focused sample of the busiest Layer 1 images, runs Layer 2 + Blur + Visualize on each independently, writes to project_dir/test_mode/person/ and project_dir/test_mode/vehicles/ (viz at test_viz_quality, blur at the usual blur_* settings). Requires Layer 1 to be complete — stops and reports otherwise. |
| Colour | Source | Detects |
|---|---|---|
| Orange | Layer 1 | Persons, cars, motorcycles, buses, trucks |
| Magenta | Layer 2 — face model | Faces |
| Yellow | Layer 2 — plate model | Number plates |
A legend is drawn in the top-left corner of each preview image.
All .txt files use standard YOLO format — one detection per line:
<class> <cx> <cy> <width> <height>
All values are normalised to [0, 1] relative to image dimensions.
Layer 2 uses class 0 for faces and class 1 for number plates.
ultralytics # pulls in torch/torchvision, used directly for
# GPU VRAM queries in detect_layer1.py
opencv-python
numpy # used directly by blur.py's obscuring methods
pyyaml
psutil # for adaptive batch sizing in detect_layer1.py
piexif # optional — needed for EXIF preservation in blur.py
Install with:
pip install ultralytics opencv-python numpy pyyaml psutil piexif
.
├── run.py # pipeline runner — runs all enabled stages
├── config.yaml # all paths, parameters, and stage toggles
├── detect_layer1.py # stage 1 — general detection
├── test_mode.py # optional — person/vehicle subset check (needs Layer 1)
├── detect_layer2.py # stage 2 — face (cls 0) + plate (cls 1) detection
├── blur.py # stage 3 — privacy blur + EXIF preservation
├── visualize.py # stage 4 — annotated preview images
├── paths.py # shared output-folder layout under project_dir
├── progress.py # shared progress-line tag + resume-status helpers
├── README.md
├── CHANGELOG.md # dated log of notable changes
├── yolov8n.pt # example base model
└── .old/ # archived previous versions