When we built the face blur tool for ToolZip, we made a deliberate choice: no image ever leaves the user's device. Everything runs client-side, including the AI inference.
Here's how we pulled it off.
The Problem with Server-Side Processing
The conventional approach for image AI features is:
- Upload image to server
- Run inference
- Return result
This works fine technically, but it comes with trade-offs:
- Privacy: users must trust you with their photos
- Cost: inference compute isn't free at scale
- Latency: upload + process + download round-trip
- Compliance: GDPR, data residency concerns
For a face blur tool specifically, the privacy angle matters a lot. People blur faces when they don't want the content to be seen — making them upload to a server feels contradictory.
Enter TensorFlow.js
TensorFlow.js lets you run ML models directly in the browser using WebGL for GPU acceleration. The models we care about:
- MediaPipe Face Detection — lightweight, optimized for real-time use
- BlazeFace — ultra-fast face detection (sub-millisecond on modern hardware)
Both are available as npm packages (@mediapipe/face_detection, @tensorflow-models/blazeface) and can run entirely client-side.
Implementation Overview
1. Load the Model
import * as faceDetection from '@tensorflow-models/face-detection';
const model = faceDetection.SupportedModels.MediaPipeFaceDetector;
const detectorConfig = {
runtime: 'mediapipe',
solutionPath: 'https://cdn.jsdelivr.net/npm/@mediapipe/face_detection'
};
const detector = await faceDetection.createDetector(model, detectorConfig);
The first load downloads the model weights (~3MB). After that, it's cached by the browser.
2. Detect Faces
const image = document.getElementById('input-image');
const faces = await detector.detectFaces(image);
// Each face includes:
// { box: { xMin, yMin, width, height }, keypoints: [...] }
3. Apply Blur to Detected Regions
function blurFaceRegions(canvas, faces, blurIntensity = 15) {
const ctx = canvas.getContext('2d');
faces.forEach(face => {
const { xMin, yMin, width, height } = face.box;
// Add padding around detected face
const padding = Math.floor(width * 0.15);
const x = Math.max(0, xMin - padding);
const y = Math.max(0, yMin - padding);
const w = Math.min(canvas.width - x, width + padding * 2);
const h = Math.min(canvas.height - y, height + padding * 2);
// Save current state
ctx.save();
// Clip to face region
ctx.beginPath();
ctx.rect(x, y, w, h);
ctx.clip();
// Apply blur via CSS filter
ctx.filter = `blur(${blurIntensity}px)`;
ctx.drawImage(canvas, 0, 0);
ctx.restore();
});
}
The CSS filter: blur() approach works well for rectangular regions. For a more sophisticated oval blur (following face shape), you'd use a radial gradient mask instead.
Performance Considerations
MediaPipe Face Detector is designed for real-time video (30fps+), so for static images it's practically instant. But there are a few gotchas:
Model initialization time: The first inference takes ~300-500ms while TF.js warms up WebGL. Subsequent calls are much faster.
// Warm up the model with a small dummy tensor
const warmupTensor = tf.zeros([1, 100, 100, 3]);
await detector.estimateFaces(warmupTensor);
warmupTensor.dispose();
Large images: Performance degrades with very large images. We resize inputs to max 1920px before detection, then scale the bounding boxes back up.
const MAX_DETECTION_SIZE = 1920;
function prepareForDetection(img) {
const scale = Math.min(1, MAX_DETECTION_SIZE / Math.max(img.width, img.height));
const canvas = document.createElement('canvas');
canvas.width = img.width * scale;
canvas.height = img.height * scale;
canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height);
return { canvas, scale };
}
What About False Positives and Misses?
No face detector is perfect. Common failure cases:
- Partial faces: faces at the edge of frame, heavily occluded
- Extreme angles: faces turned more than ~45 degrees from frontal
- Small faces: tiny in the frame (less than ~60px)
- Unusual lighting: very dark, backlit, strong shadows
For a blur tool, false negatives (missed faces) are more problematic than false positives (blurring something that isn't a face). So we err on the side of a slightly larger bounding box and offer manual adjustment.
The Privacy Architecture
The full data flow for our tool:
User selects image → FileReader API (local) →
Canvas element (local) → TF.js inference (local GPU) →
Canvas manipulation (local) → Download result (local)
Zero network requests involved in the actual processing. The only external call is the one-time model download, which gets cached.
Browser Support
WebGL (required for TF.js GPU acceleration) is supported by 97%+ of browsers as of 2024. For the ~3% without WebGL, TF.js falls back to CPU via WASM or pure JS — slower but functional.
Try It
If you want to see this in action without setting anything up: ToolZip Image Face Blur — runs entirely in your browser, no account needed.
The complete source approach described here is what we use in production. Give it a try and let me know if you have questions about any part of the implementation.
Top comments (0)