chore: project cleanup, track missing files, and update README

This commit is contained in:
fiatcode 2026-02-28 10:17:11 +07:00
parent 4c626a6c89
commit 9667431406
8 changed files with 135 additions and 3 deletions

13
.gitignore vendored
View file

@ -11,3 +11,16 @@ wheels/
# Downloaded ML models # Downloaded ML models
models/ models/
# Video files (avoid committing test videos)
*.mp4
*.avi
*.mkv
*.mov
# Editor swap files
.*.swp
*.swp
# AI Coding context
.opencode/

1
.python-version Normal file
View file

@ -0,0 +1 @@
3.12

53
README.md Normal file
View file

@ -0,0 +1,53 @@
# PyFaceBlur
An interactive command-line tool that automatically detects, clusters, and blurs faces in videos. It guides you through a simple step-by-step process to extract frames, group people by facial identity, select who you want to blur, and re-encode the video.
## Features
- **Interactive CLI:** Built with `rich` and `questionary` for a clean, prompt-based UX including file path auto-completion.
- **Accurate Face Recognition:** Uses [UniFace](https://github.com/yakhyo/uniface) (RetinaFace detection + ArcFace 512-dim neural embeddings via ONNX Runtime) to accurately re-identify the same person across a video.
- **DBSCAN Clustering:** Automatically groups identical faces into "clusters" using Cosine similarity.
- **Hardware-Accelerated Encoding:** Automatically detects and leverages GPU encoders like `av1_vaapi`, `hevc_vaapi`, `h264_vaapi`, `h264_nvenc`, and more via FFmpeg.
- **Visual Face Selection:** Extracts one high-quality thumbnail per detected person and opens your system's file explorer so you can easily check boxes for who to blur.
- **Multiple Blur Styles:** Choose from Gaussian, Pixelate, Blackout, Elliptical, or Median blur methods.
- **Smooth Interpolation:** Bounding boxes are linearly interpolated between sampled keyframes and held static when faces exit/enter, ensuring smooth blurring without split-second exposures.
## Requirements
- Python 3.11+
- [uv](https://docs.astral.sh/uv/) for fast dependency management
- `ffmpeg` installed and available in your system `$PATH` (for frame extraction and re-encoding)
## Setup
```bash
# Clone the repository and navigate to the project directory
cd faceblur-poc
# Sync dependencies using uv
uv sync
```
## Usage
Run the interactive wizard:
```bash
uv run pyfaceblur
```
### The Pipeline
1. **Input:** You provide the path to your video and the frame sampling interval (e.g., sample every 30th frame).
2. **Processing:** The app uses FFmpeg to extract frames, runs RetinaFace to find all faces, and generates ArcFace embeddings.
3. **Clustering:** DBSCAN groups the embeddings to identify unique individuals.
4. **Selection:** The app saves a thumbnail of each person to a temporary folder, opens it, and asks you to select which people to blur using interactive checkboxes.
5. **Encoding:** The app finds the best available video encoder on your system, applies the chosen blur method to the selected faces, interpolates their movement, and generates a new `*_blurred.mp4` video.
## Advanced / POC CLI
The original proof-of-concept command-line interface is also still available for purely extracting and debugging the clustering outputs into an output folder.
```bash
uv run faceblur-poc detect --video input.mp4 --output ./output --interval 30 --confidence 0.7
```

4
main.py Normal file
View file

@ -0,0 +1,4 @@
from faceblur.cli import main
if __name__ == "__main__":
main()

3
src/faceblur/__init__.py Normal file
View file

@ -0,0 +1,3 @@
"""Face detection and clustering POC."""
__version__ = "0.1.0"

View file

@ -6,11 +6,9 @@ from pathlib import Path
from typing import Callable, Dict, List, Optional, Set, Tuple from typing import Callable, Dict, List, Optional, Set, Tuple
import cv2 import cv2
import numpy as np
from .blur import BlurMethod, apply_blur, get_bboxes_for_frame from .blur import BlurMethod, apply_blur, get_bboxes_for_frame
from .cluster import Cluster from .cluster import Cluster
from .detect import FaceData
ENCODER_PRIORITY = [ ENCODER_PRIORITY = [

View file

@ -5,7 +5,6 @@ from pathlib import Path
from typing import List, Dict, Tuple from typing import List, Dict, Tuple
import cv2 import cv2
import numpy as np
from .video import Frame from .video import Frame
from .detect import FaceData from .detect import FaceData

61
src/faceblur/video.py Normal file
View file

@ -0,0 +1,61 @@
"""Video frame extraction module."""
import subprocess
from dataclasses import dataclass
from pathlib import Path
from typing import List
@dataclass
class Frame:
"""Represents an extracted video frame."""
path: Path
index: int
def extract_frames(video_path: str, output_dir: str, interval: int = 30) -> List[Frame]:
"""Extract frames from video at specified interval.
Args:
video_path: Path to input video file
output_dir: Directory to save extracted frames
interval: Extract every Nth frame
Returns:
List of Frame objects
"""
video_path = Path(video_path)
output_dir = Path(output_dir)
if not video_path.exists():
raise FileNotFoundError(f"Video file not found: {video_path}")
output_dir.mkdir(parents=True, exist_ok=True)
pattern = str(output_dir / "frame_%04d.jpg")
cmd = [
"ffmpeg",
"-i",
str(video_path),
"-vf",
f"select='not(mod(n\\,{interval}))'",
"-vsync",
"vfr",
"-q:v",
"2",
"-y",
pattern,
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(f"ffmpeg failed: {result.stderr}")
frames = []
for frame_path in sorted(output_dir.glob("frame_*.jpg")):
index = int(frame_path.stem.split("_")[1])
frames.append(Frame(path=frame_path, index=index))
return frames