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:

  1. Upload image to server
  2. Run inference
  3. 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.