FacePrivacy technical note FP-TN-01 · version 1.1

Shield Blur

An outline-preserving, non-invertible face blur that runs entirely in the browser

Algorithm
Shield blur v4.2
Document
FP-TN-01 v1.1
Published
28 October 2026
Implementation
faceprivacy.ai/blur (Canvas 2D, Web Crypto)

Abstract

A Gaussian blur is a linear, shift-invariant filter: with the kernel known or estimated it can be partly inverted, and recognition models can be trained to see through it. Shield blur replaces the region under the brush with a coarse, randomly dithered approximation of it. The region is reduced to at most eight blocks across its shorter side on a grid with a random phase, each block is pulled 45% toward the region’s mean colour, perturbed by noise and quantised to steps of 10, and the result is upsampled, smoothed and grained. The head outline and colours survive; facial detail does not. Because the reduction is many-to-one and the residual values are randomised, no inverse exists, and the best an attacker can do is synthesise a plausible face that is not the original. The randomness is a CTR_DRBG-style stream keyed from three independent sources: pointer timing, 32 bytes of server randomness from Cloudflare and the browser’s CSPRNG. This note specifies the method exactly, analyses what it does and does not protect, and lists its parameters and history.

1The problem with ordinary blur

A blurred image is commonly modelled as

$$ y = k * x + n $$

where x is the original, k the blur kernel, * convolution and n noise. In the frequency domain the blur is a pointwise product, so a Wiener filter recovers an estimate

$$ \hat{X}(\omega) = \frac{\overline{K(\omega)}\,Y(\omega)}{|K(\omega)|^2 + 1/\mathrm{SNR}(\omega)} $$

which restores every frequency the kernel attenuated but did not remove. A Gaussian never reaches zero, and 8-bit rounding is the only real loss, so a strong-looking blur keeps more of the face than the eye suggests. Learned methods go further: McPherson, Shokri and Shmatikov trained networks that identify people in pixelated images and in faces blurred with YouTube’s tool [1], and Depix recovers pixelated text by rendering candidates through the same deterministic pipeline and matching them [2].

Shield blur therefore has three goals: keep the coarse appearance a viewer expects from a blur (outline, hairline, colour), remove the information that identifies the face, and leave no deterministic pipeline that a candidate search can replay exactly.

2Notation

3Method

While the user drags, the tool shows an ordinary Gaussian blur as a preview and records the mask. When the stroke ends, the region is replaced in one pass. The source for that pass is the image as it was before the stroke (the top of the undo stack), not the preview.

3.1 Block size and random phase

$$ c = \max\!\left(12,\; \frac{b}{6},\; \frac{\min(w,h)}{8}\right) $$
(1)

The third term makes the painted region at most eight blocks across its shorter side, whatever brush was used. The grid has nx = ⌈w/c⌉ + 1 by ny = ⌈h/c⌉ + 1 blocks and starts at a random phase

$$ \varphi = \left(\frac{S_0}{256}\,c,\; \frac{S_1}{256}\,c\right) \in [0,c)^2 $$
(2)

so block Bij covers the square of side c whose corner is (x0 − φx + ic, y0 − φy + jc).

3.2 Reduction

Each block is reduced to one colour by the browser’s high-quality downscale (drawImage with imageSmoothingQuality = 'high'), which approximates the block mean

$$ \mu_{ij} = \frac{1}{|B_{ij}|} \sum_{p \in B_{ij}} I(p) $$
(3)

3.3 Contrast pull and dithered quantisation

First every block is pulled a fraction λ = 0.45 of the way toward the mean colour m of the blocks whose centre lies inside the mask. This keeps the outline but stops the darkest features (eyes, brows, nostrils) from surviving as dark blobs, and it removes a further share of the block-to-block variation:

$$ \tilde{\mu}_{ij} = \mu_{ij} + \lambda\,(m - \mu_{ij}), \qquad m = \frac{1}{|\mathcal{K}|}\sum_{(i,j)\in\mathcal{K}} \mu_{ij} $$
(4a)

Then each block gets a shared brightness offset and a small per-channel offset, and every channel is quantised to a step of Δ = 10:

$$ \nu^{\text{lum}}_{ij} = (S_{8+3k} \bmod 13) - 6, \qquad \nu^{(ch)}_{ij} = \nu^{\text{lum}}_{ij} + \begin{cases} 0 & ch = R \\ (S_{8+3k+ch} \bmod 3) - 1 & ch \in \{G,B\} \end{cases} $$
(4)
$$ q^{(ch)}_{ij} = \operatorname{clamp}_{[0,255]}\!\left(\Delta \cdot \operatorname{round}\!\left(\frac{\tilde{\mu}^{(ch)}_{ij} + \nu^{(ch)}_{ij}}{\Delta}\right)\right) $$
(5)

where k = j nx + i. Keeping the noise mostly common to the three channels shifts brightness, not hue; earlier per-channel noise visibly tinted blocks green or magenta.

3.4 Reconstruction

The block image is upsampled bilinearly back onto the phase-shifted grid and smoothed with a Gaussian, then grain is added per pixel:

$$ J = G_\sigma * \mathcal{U}(q), \qquad \sigma = \max(3,\; 0.6\,s) $$
(6)
$$ J'(p) = \operatorname{clamp}_{[0,255]}\big(J(p) + g_p\big), \qquad g_p = (S_{8+3n_xn_y+p} \bmod 9) - 4 $$
(7)

The same gp is added to all three channels. Finally J′ is written into the image through M softened by a 1 px Gaussian; pixels outside the mask are untouched.

Three copies of a portrait: the original, the face with a standard Gaussian blur, and the face with Shield blur v4.2, which keeps the shape and colour of the face but no features.
Figure 1. Original, standard Gaussian blur and Shield blur v4.2 on the same region, default brush (80 px). The Shield result keeps the outline and skin tone; it holds roughly eight values across the face.

4Randomness

Every random choice in a stroke (grid phase, block noise and grain) is read from one byte stream S of length

$$ |S| = 8 + 3\,n_x n_y + w h $$

Bytes 2–7 are reserved. The stream is produced as follows:

  1. Collect three inputs. rb: 32 bytes from crypto.getRandomValues. h: for every pointer event in the stroke, (performance.now(), x, y) as three little-endian float64 values. rc: 32 bytes fetched in advance from GET https://api.faceprivacy.ai/public/random, which draws them from crypto.getRandomValues on Cloudflare Workers; Cloudflare seeds its servers’ randomness with entropy that includes LavaRand, a camera pointed at a wall of lava lamps [3]. rc is used once and refetched, and is omitted if the request failed.
  2. Derive a key from their concatenation with a version label:
    $$ K = \mathrm{SHA\text{-}256}\big(\texttt{"faceprivacy-shield-v4.2"} \,\|\, r_b \,\|\, h \,\|\, r_c\big) $$
  3. Expand with AES-256 in counter mode over zeros, from a fresh random 128-bit counter block t0 (64-bit counter):
    $$ S = \mathrm{AES\text{-}256\text{-}CTR}_K(t_0,\; 0^{|S|}) $$

This is the construction NIST specifies as CTR_DRBG [4], used here informally (no certification, no reseed schedule; each stroke gets a fresh key). The output is never stored. Hashing the three sources together means any one of them that is weak or missing does not weaken the other two. The tool shows which sources fed the last stroke.

5Why the result cannot be inverted

5.1 The reduction is many-to-one

Per channel, the block mean (3) is a linear map from ℝc² to ℝ with rank 1, so its null space has dimension c² − 1. Every image that differs from the original by any null-space vector, in every block, produces the same block means. The contrast pull (4a) is a contraction toward m, so quantisation (5) then merges all means within a cell of width Δ/(1−λ), about 18 grey levels of the original, rather than Δ. What leaves the reduction is bounded by

$$ \log_2 27^{3} \approx 14.3 \text{ bits per block} $$
(8)

(27 levels per channel), against 24c² bits that went in. For the minimum block of 12 px that is 3,456 bits reduced to at most 14.3; for a 400 px-wide painted face (c = 50) it is 60,000 bits to 14.3. Deconvolution can at best undo Gσ and 𝒰 in (6), which returns q: the blocks themselves, which are already visible.

5.2 Randomisation removes exact replay

Candidate-matching attacks such as Depix render guesses through the same pipeline and look for an exact or near-exact match. Here the grid phase φ, the per-block noise ν and the grain are unknown to the attacker, change on every stroke and are never saved, so a candidate rendered with any guessed parameters does not reproduce the published pixels. The noise also decides which side of a quantisation boundary a block mean falls on, so q alone does not pin the true mean to one cell.

The phase has only 256² values and could be searched. The protection rests on 5.1, the loss of information; the randomness raises the cost of matching and removes determinism, it is not a secret key in the cryptographic sense.

5.3 What a learned model can still do

A generative model can map eight-by-eight-ish blocks to a sharp, plausible face. That face is drawn from the model’s prior, constrained only by the 14-bit-per-block summary, and is not the person’s face. Identity information at this resolution is low but not zero: a model may still estimate coarse attributes such as skin tone or hair colour, and may help verify a guess against a known photo (§7).

6Export

Editing happens on the working image (≤ 1600 px). The exported PNG has the upload’s original resolution: the original pixels everywhere, and inside the union of all blurred strokes the working-image result upsampled with high-quality smoothing. No original pixel inside the mask reaches the export. Files are named blurred-shield-v4.2-<timestamp>.png so the method travels with the file. Nothing is uploaded; the only network request is the 32-byte fetch in §4, which carries nothing about the image.

7Limitations

8Parameters

SymbolValueMeaning
cmax(12, b/6, min(w,h)/8)Block size, px
φS₀, S₁ → [0, c)²Grid phase
λ0.45Pull toward the region’s mean colour
Δ10Quantisation step per channel
ν[−6, 6] + [−1, 1] on G, BBlock noise
σmax(3, 0.6 s)Smoothing, px (12 at the default s = 20)
g[−4, 4]Grain, all channels
p⌈s⌉ + 4Region padding, px
Mdiscs r = b/2 + 3, 1 px soft edgeStroke mask

9Reference outline

onStrokeEnd(region R, mask M, source I_before):
  c   = max(12, b/6, min(w, h)/8)
  nx  = ceil(w/c) + 1;  ny = ceil(h/c) + 1
  S   = AES256CTR(SHA256(label || rb || hand || rc), t0, zeros(8 + 3*nx*ny + w*h))
  phi = (S[0]/256*c, S[1]/256*c)
  mu  = downscale(I_before, R shifted by -phi, nx*c x ny*c -> nx x ny)
  m   = mean of mu over blocks whose centre is inside M
  mu  = mu + 0.45 * (m - mu)
  for each block k:
    lum = S[8+3k] mod 13 - 6
    for ch in R,G,B:
      nu = lum + (ch == R ? 0 : S[8+3k+ch] mod 3 - 1)
      q[k][ch] = clamp(10 * round((mu[k][ch] + nu) / 10))
  J  = gaussian(upscale_bilinear(q) placed at -phi, sigma = max(3, 0.6*s))
  J += grain from S[8+3*nx*ny ...], (S mod 9) - 4, same for R,G,B
  write J into the image through soften(M, 1px)

10Version history

VersionChange
v1Square grid of colour blocks shuffled with the browser CSPRNG, then blurred and grained.
v2Shuffled patches laid out on a golden-angle (Vogel) spiral with a random rotation.
v3Randomness keyed from pointer timing, Cloudflare server randomness and the browser CSPRNG via SHA-256 and AES-CTR.
v4Dropped the shuffle, which destroyed the outline and looked blotchy. Coarse blocks with a random phase, quantised with noise, smoothed and grained. Sampled from the pre-stroke image.
v4.1Block size also bounded by the painted area (eight blocks across its shorter side). Block noise mostly shared across channels so blocks keep their hue.
v4.2Blocks pulled 45% toward the region’s mean colour before quantisation, so the eyes and brows no longer survive as dark blobs.

This note (FP-TN-01 v1.1) describes v4.2. Version 1.0 of the note described v4.1. A later algorithm version will be documented in a new version of the note.

11Use it in your app

The reference implementation is one ES module with no dependencies, the same code the tool runs. It is published at a fixed, versioned address; a versioned file never changes, and a new algorithm version gets a new file.

import { shieldBlur } from 'https://faceprivacy.ai/lib/shield-blur-4.2.1.js';

// canvas: an HTMLCanvasElement or OffscreenCanvas holding the photo.
// The box is where the face is, for example from your face detector,
// with some margin around it.
await shieldBlur(canvas, { x: 120, y: 80, width: 220, height: 260 });
OptionDefaultMeaning
x, y, width, heightrequiredThe area to blur, in canvas pixels
shape'ellipse''ellipse' or 'rect' inside that box
strength20s in §3.4, 5 to 50
brush0b in (1), if the area was painted with a brush
entropynoneExtra random bytes to mix into the key (§4), for example pointer timings

12License and credit

Shield blur is free to use. The code is released under the Shield Blur License 1.0, which is the MIT license with one added condition: any product, website or service that uses it, or a work derived from it, credits “Shield blur by FacePrivacy.ai”, linked to faceprivacy.ai, where its users can see it (next to the result, on an about or credits screen, or in its documentation). Full text: faceprivacy.ai/lib/LICENSE.txt.

This note is licensed under Creative Commons Attribution 4.0; credit “FacePrivacy.ai”. Copyright © 2026 UnFind LLC (FacePrivacy.ai).

Please cite as: FacePrivacy.ai, “Shield Blur: an outline-preserving, non-invertible face blur,” technical note FP-TN-01 v1.1, 2026. https://faceprivacy.ai/research/shield-blur/

13References

  1. R. McPherson, R. Shokri, V. Shmatikov. Defeating Image Obfuscation with Deep Learning. arXiv:1609.00408, 2016. arxiv.org/abs/1609.00408
  2. Depix: recovers passwords from pixelized screenshots. github.com/spipm/Depix
  3. Cloudflare. Randomness 101: LavaRand in Production. blog.cloudflare.com/randomness-101-lavarand-in-production
  4. NIST SP 800-90A Rev. 1. Recommendation for Random Number Generation Using Deterministic Random Bit Generators. csrc.nist.gov/pubs/sp/800/90/a/r1/final
  5. H. Vogel. A better way to construct the sunflower head. Mathematical Biosciences 44, 1979 (v2 only). doi.org/10.1016/0025-5564(79)90080-4