How it works: the method, in public
Which engine ran on your photo, how the diagnosis reached its verdict, how sure it is, and — for the learned pass this site is built to offer — the licence chain, the corpus and the host, written down before anything is downloaded.
Pixels stay in this tab; only the file you save leaves it.
Which engine ran
Every result names its engine, and the label, the work done and the export receipt all read the same value, so they cannot disagree. Today two engines are available:
| Classical pass | Available. Deconvolution against a kernel of the measured shape and length, then an artifact guard that clamps each pixel to the local range of the original. Kernels under 33 taps run the iterative spatial path (eight rounds by default, each correction held inside a bounded band); kernels of 33 taps or more run the frequency-domain path, a regularised (Wiener) deconvolution computed with an FFT in tiles anchored to the full frame, whose cost does not grow with the kernel. The receipt names the path. |
|---|---|
| Learned pass | Not available. No model is published for this build, so nothing offers it and no result is ever labelled as learned. |
| Sharpen | Available, for photos that are soft rather than smeared: an unsharp mask (radius 0.3–5 px, amount 0–300%, threshold 0–20 levels, brightness only by default) with the artifact guard as its halo limiter. Offered when the measurement finds little or nothing to undo; it warns when the photo actually smeared in one direction. |
| De-block | Not available in this build. A block grid gets the verdict “can't be saved” and no pass. |
How the diagnosis reaches a verdict
The photo is measured on a working copy no longer than 1,024 pixels before anything runs. Gradient energy is read in twelve directions; the axis that lost the most is the axis of motion. How far the smear ran is where the autocorrelation of differences along that axis first turns negative, and the same is read across it to tell movement from missed focus. A two-axis grid test looks for block periods of 4, 8 and 16 pixels.
| Movement | The extent along the smear is clearly longer than across it, or gradient energy has collapsed onto one axis. |
|---|---|
| Missed focus | The spread is about equal in every direction. |
| Block grid | A regular grid of block edges was found. Detail inside a block was averaged away; no pass is offered. |
| Nothing to undo | The smear is two pixels or less on the working copy. |
| Beyond reach | The smear is longer than the classical pass handles at this size, or longer than the measurement can read (24 px on the working copy). The original is left untouched. |
Each verdict carries a confidence band — clear, likely or uncertain — derived from how directional the loss was and how decisively the along- and across-smear extents differ. At uncertain, Auto starts from a shorter, more conservative kernel and the page invites you to probe or adjust it. The bands are thresholds set by hand on synthetic smears; they have not yet been calibrated against a labelled corpus, so no precision or recall figure is published and no percentage is ever printed beside a verdict.
The synthetic harness behind the two paths
Both classical paths are checked against a drawn scene whose sharp state is known (scripts/harness-deblur.mjs). Mean absolute error per channel against the sharp scene, 640 × 448, 8 rounds, measured 20 October 2026:
| Motion 15 taps | blurred 4.58 · spatial 4.19 · FFT 4.50 — the spatial path runs |
|---|---|
| Motion 30°, 21 taps | blurred 7.76 · spatial 6.67 · FFT 6.38 — the spatial path runs |
| Motion 31 taps | blurred 6.60 · spatial 5.65 · FFT 5.98 — the spatial path runs |
| Motion 60°, 41 taps | blurred 13.06 · spatial 10.16 · FFT 8.75 — the FFT path runs, about 2× faster |
| Motion 61 taps | blurred 9.77 · spatial 7.26 · FFT 8.43 — the FFT path runs, about 3× faster |
| Defocus 33 taps | blurred 9.83 · spatial 8.21 · FFT 7.47 — the FFT path runs |
The same harness asserts that a banded render, a cropped render with its documented margin and a whole-frame render are byte-identical on every path, and that strength 0 returns the original exactly. The FFT path is not always closer to the sharp scene than the spatial one — on a long straight smear it trails — and it is chosen for long kernels because it stays fast where the spatial path slows down.
The blur check’s figures
| Edge width | The median 10–90% rise distance across the strongest edges, taken separately for four edge orientations; the widest orientation is the reading, because camera shake only widens the edges that cross the movement. In pixels of the 1,024-pixel working copy, with the full-size equivalent beside it. Up to 2.5 px reads as sharp, up to 4 px as soft, above that as blurred. |
|---|---|
| Laplacian variance | The variance of the Laplacian divided by the variance of the image itself, so a low-contrast photo is not marked blurry for being grey. |
| High-frequency ratio | Energy left after removing a 5-pixel box blur, against the energy of the image about its mean. |
| What they are not | None of these is a quality score or a probability that any portal will accept a photo. The thresholds above are provisional, set by hand, and will change once they are calibrated. |
What this build actually has
No published export. The loader, the byte-length check, the Cache Storage bucket, the session builder and the control that deletes the whole thing are all written and typed against a release constant, and that constant is null. Every surface that would offer a download says so instead of drawing a control that cannot complete.
What runs today is the classical pass (or the Sharpen engine) with no download behind it: the smear is measured off your frame, a kernel of that shape is built, and the frame is walked back through it — eight rounds by default — under a bounded correction. It is weaker than a trained pass and it is honest about the difference. It also downloads nothing.
The learned model it is built for, and the licence chain
| Network | NAFNet, in the single-image motion-deblurring form. Fully convolutional, which is what makes a frame of any size feasible in one pass rather than a fixed square. |
|---|---|
| Export | The ONNX conversion published as opencv/deblurring_nafnet, licensed MIT. |
| Upstream | The original implementation is offered under MIT and Apache-2.0 together. |
| Training pairs | The GoPro set, under CC BY 4.0 — sharp and motion-blurred frames from the same hand-held sequences. |
| Attribution | The GoPro corpus and both upstream licences are reproduced on this page because that is where the obligation sits, not in a file nobody opens. |
| Precision | fp32 as exported. An int8 or fp16 conversion would halve the transfer and is the single largest improvement available, but only if the quality can be measured to hold. |
The chain matters practically rather than aesthetically: a corpus or a model licensed against commercial use would make the whole site unshippable however well it scores.
What was rejected, and on what grounds
- MPRNet is out on licence alone. Its terms are a custom academic public licence that forbids commercial use outright, which is an absolute bar here rather than a trade-off to weigh against quality.
- Restormer is MIT and clean, but it is a transformer with no ready ONNX export, and a transformer on single-threaded WebAssembly is near-certainly far slower than a convolutional stack of the same capability.
- SwinIR is Apache-2.0 and clean, and fails on the same two grounds as Restormer.
- All three are worth revisiting the day a maintained quantised export appears, and not before.
What a fetch would look like
| Host | Our model file server. Never a public model hub, because a request to one tells that hub which of this site's visitors is deblurring something. |
|---|---|
| Runtime | The ONNX WebAssembly build, just under 14 MB, served from this origin rather than from anywhere else. Single-threaded: threads need cross-origin isolation, which this static site does not switch on. |
| Trigger | A control naming the exact byte length. Nothing is requested before it is pressed, and Escape stops a transfer in flight and leaves nothing behind. |
| Storage | One Cache Storage bucket whose name carries the release, so a new set of weights is a new bucket rather than an invalidated path. Written only after a complete body of the right length has been read. |
| Removal | One control under the rail. It clears the bucket, drops the session and its memory arena, and reports what it freed. |
| Private windows | The Cache API is refused outright in some profiles. The pass still runs; it pays for the fetch again next session, and the rail says so rather than failing quietly. |
Figures that are estimates, marked as estimates
Two numbers on this domain have been measured and the rest have not. The runtime’s length is the length of the file in this repository. The seconds quoted in the rail are timed on your own processor, running the identical loop the pass runs, which is why they are given as a band rather than as a figure.
Everything about the learned pass is an estimate until a model is published: a size of roughly 90 megabytes, the ten to twenty seconds for a 1024-pixel pass, and the assumption that the published graph has dynamic spatial axes at all. If it turns out to be frozen at 512 by 512 then every frame becomes a tiled job, the overhead roughly doubles, and the ceiling has to be rethought before anything else is built on top of it.
Elsewhere on this domain
- Unblur an imageDiagnose the blur, then reconstruct it, with the kernel on a dial.
- Check if a photo is blurryMeasure blur before you upload: edge width, a heat map and a form checklist.
- What a model download costsWhat is fetched, from where, what is kept and how to clear it.
- PrivacyWhat this page reads, what it sends, and what it keeps.



Model