skip to content>_lesfer

Jan 10, 2026 · learning · shipped

FaceBlur

A service that blurs faces in images and videos. The API takes the upload and answers at once, workers do the computer vision on a queue, and the browser polls until the result is ready to download.

Repository
  • Python
  • FastAPI
  • Taskiq
  • RabbitMQ
  • Redis
  • OpenCV
  • FFmpeg
  • SQLite
  • React
  • Vite
  • Tailwind CSS
  • shadcn/ui
  • Prometheus
  • Grafana
  • Docker

Case study

Blurring a face in a photo takes a moment; blurring every face in a minute of video takes long enough to tie up whatever is doing it. FaceBlur is a small, complete answer to that shape of problem: the API never does the heavy work itself. It validates the upload, puts a task on a queue and answers straight away, and the browser polls for the result while a worker does the computer vision. I built it alone in a week in January, as an exercise in that architecture end to end.

Highlights

  • An async FastAPI API that accepts one or more images, or a single video, and returns a task id instead of making the client wait.
  • Taskiq workers fed through RabbitMQ, with Redis holding results, so the client can poll /results/{id} and get a 202 while the work is pending, then the image, or a zip for several.
  • Face detection on the CPU with three OpenCV Haar cascades (two frontal, one profile), run on the plain and contrast-equalized frame and on its mirror image for faces turned the other way, with overlapping detections merged.
  • A video path that detects on a half-size frame every fourth frame, caps output at 20 frames per second, re-encodes to H.264 for browser playback, and puts the original audio back.
  • Guards before anything reaches the queue: file type, size and count limits, a 60-second cap on video, and a rate limit on the upload endpoints.
  • Operations endpoints for health, queue depth and Prometheus metrics, an optional Grafana profile with a dashboard, and a cleanup loop that deletes results once they're collected or once they age out.
  • A React interface that checks limits before uploading, estimates how long a video will take, polls for the result, and plays it back inline; plus a Streamlit demo for the API alone.

The Request Path Stays Light

Every heavy step happens in a worker. The API's job is to refuse what it shouldn't process (a wrong extension, a file over the size limit, a video over a minute) with an error that says what to fix, and to enqueue what it should. Results are written to storage by the worker and served once: collecting a result deletes it, and a periodic sweep removes anything nobody came back for. A /queue endpoint reports how many jobs are waiting and how many workers are consuming, which is what the interface uses to set expectations.

Finding Faces On A CPU

The detector is deliberately classic. Haar cascades ship with OpenCV, need no model download and run on any CPU, so the service deploys anywhere. Their weakness is recall: one cascade misses faces a different one finds. So each frame is searched with three cascades at two sensitivity settings, on the grayscale image and on a histogram-equalized copy that helps in poor light, and the profile cascade runs again on the mirrored frame, since it only knows faces turned one way. The overlapping boxes this produces are merged into one per face. The trade-off stays visible: a face at a steep angle or half hidden can still slip through, and a learned detector is the step up when anonymization has to be dependable.

The blur itself is a pixelation, softened and masked to an ellipse over a box padded by a fifth on each side, so it covers the whole head and reads as a face rather than a rectangle.

Video Without Waiting Forever

Video is where the cost is, so every setting that trades quality for time is a configuration value. Detection runs on a frame scaled to half size, and only every fourth frame; the frames in between reuse the last boxes, which holds up because faces move little in a few frames. High-frame-rate input is thinned to 20 frames per second. OpenCV writes the frames, trying codecs until one opens, and FFmpeg re-encodes the result to H.264 so browsers can play it, muxing the original audio back in when there is any.

Why This Project Matters

FaceBlur is small, but it has every part a compute-heavy feature needs in production: a request path that stays fast, work that runs where it can't block anyone, limits that protect the workers, storage that cleans up after itself, and enough metrics to see the queue. It's the same split I'd reach for whenever a feature takes longer than a request should.