unpixel

package module
v0.12.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 24, 2026 License: GPL-3.0 Imports: 19 Imported by: 0

README

UnPixel

A faithful pure-Go port of Bishop Fox's unredacter — reconstructs text hidden behind mosaic pixelation or Gaussian blur redaction. Background: Never use pixelation to redact text.

CI Go Reference Go Report Card Go 1.26 License GPL-3.0-or-later

Status: v0.11.0 published, with decode-all-testdata wave (post-v0.11.0, unreleased) — pure-Go mosaic and Gaussian-blur recovery with decoding improvements, nine opt-in pure-Go features, and structural answers to font fidelity.

  • Core: zero-config auto-detection (block size / blur σ / font / language), bilingual blind recovery (FR/EN), ≥85% test coverage, all gates green. Panel 17/17 exact, no regressions.
  • Decode-all wave (6 items): information-theoretic capacity triage (internal/capacity), advance-width pre-filter (−58% pool, 6.8× DecodeLineWhole faster), LM-fused Viterbi (--decoder did + trellis DP), calibrate-from-visible + Nelder-Mead (3× vs coordinate descent), non-blind L0 text deblur (--l0-deblur, opt-in). All pure-Go, opt-in, panel 17/17, coverage ≥85%.
  • Honest scope: these are capability + perf gains on testdata. Do NOT move real/wild/sick recovery — real-world images remain bound by render-alignment (DID boundary emission needs context-aware refinement), absent visible text (for calibration), and information-theoretic limits. Width filter −58% pool, Nelder-Mead 3× carry zero quality loss.
  • Quick wins (Q1–Q5): auto-gamma selector (--gamma=auto|linear|sRGB), adaptive word-pool recall, 10k-word frequency-ranked dictionaries (EN/FR), letter-spacing calibration (--letter-spacing-search), French apostrophe elision. All opt-in, blind zero-config path.
  • Big bets (B1–B4): variable-font fitting (--decoder varfont + coordinate descent), multi-frame fusion for sub-block recovery, font ranker (310× cheaper probe), trained-HMM language-structure upgrades (--thmm-lang, --thmm-jpeg). All pure-Go, opt-in.
  • Decoders: guided/beam default, LM-guided monospace (--decoder mono-hmm), Depix-style reference-matching (--decoder ref-match), grid-window beam for proportional fonts (--decoder window-hmm), learned-emission Viterbi HMM (--decoder trained-hmm), variable-font fitting (--decoder varfont), Document-Image-Decoding trellis (--decoder did).
  • Real captures: input normalization (--normalize), re-mosaic blur error-correction (--remosaic), improved blur-σ estimation, multi-format input (JPEG/GIF/WebP/BMP/TIFF), robust mosaic-vs-blur auto-detect + best-effort surfacing (Result.BelowThreshold).
  • Tracking: mise run journal writes an evolving docs/JOURNAL.md over all corpora; a SICK / check-number parity corpus benchmarks against Hill-2016.
  • Honesty: quick wins and big bets improve SHORT known-font phrase recovery, calibration robustness, and font selection. They do NOT break the real/wild/sick journal walls — those need deeper blockers: context/beam for single-band per-word scoring and variable fonts spanning real internet images. Real-world recovery stays font-fidelity-bounded (supply the exact font via --font). See PROGRESS.md for the roadmap and docs/DELTA.md for the delta vs the original Bishop Fox unredacter.

Table of contents

How it works

Pixelation is a deterministic function of its input, so UnPixel doesn't "un-blur" anything — it runs generate-and-test. It renders a candidate string in the target font, re-pixelates it with the same block grid as the redacted region, scores the image distance against the target, and drives a guided depth-first search over candidate strings: discover the grid offset, then extend the plaintext character by character, pruning branches as soon as the re-pixelated output stops matching. Because pixelation averages each block, only the true text reproduces the target's blocks exactly. When image distance alone is ambiguous, optional plausibility priors break ties: a dictionary of real words, common passwords (including French ones), and recognized secret formats (UUIDs, API tokens, Luhn checksums) all add a small bonus, making natural language and structured secrets rank higher than random noise.

See docs/DESIGN.md for the algorithm and the library choices behind it, and docs/DELTA.md for how UnPixel compares to the original Bishop Fox unredacter and what the blur / zero-config work added.

Features

  • Pure Go, no CGO. Deterministic, statically linked, cross-compilable — no C toolchain.
  • Library-agnostic progress API. Run streams typed Progress events on a buffered channel (best guess, current candidate, score, depth, offsets probed/total, evaluated count, elapsed), so any UI — web/SSE, TUI, desktop — can subscribe via the channel or the OnProgress callback.
  • Pluggable everything. Swap the Renderer, Pixelator, Metric, or search Strategy through Config; the faithful defaults are wired by importing the defaults package. Built-in choices: guided-DFS, beam, or monospace fast-path; mosaic or Gaussian-blur operator; optional Richardson-Lucy deconvolution for exploratory preprocessing; pixelmatch or SSIM distance; optional priors to break equal-image ties: char-bigram language model, dictionary words, and structured-secret formats (UUID, API tokens, Luhn checksums, common French/English passwords).
  • Concurrent by default. Grid-offset discovery and per-offset search fan out across Config.Workers goroutines (default: all CPUs) with a deterministic merge — same output regardless of scheduling. Intra-node parallelism of the DFS tree further accelerates wide-charset recovery. ~4–7× faster offset discovery on a typical laptop.
  • Auto-detects the block size. Leave Config.BlockSize unset and New infers the mosaic grid from the image (InferBlockSize), so callers don't have to measure it.
  • Blur, not just mosaic. Blur is also a deterministic function of its input, so the same attack applies: pixelate.NewGaussianBlur(σ) / WithPixelator reproduce a Gaussian blur. Blur recovery is now zero-config on σ: unpixel.RecoverBlurred(ctx, img, opts...) auto-estimates σ via InferBlurSigma (density-adaptive gradient-percentile, accurate to ~±2% for σ∈{1,2,4,8}), then searches σ adaptively as a dimension of the search (like block size in mosaic), defaulting to beam search with language prior to recover longer words where per-character image signal is weak. The CLI --redaction blur auto-searches σ when --blur-sigma is unset. Result.BlurSigma records the recovered σ. Optional exploratory Richardson-Lucy deconvolution (--deblur) for known-PSF cases. Re-mosaic error correction (--remosaic / WithRemosaic() / WithRemosaicGrid(b) / WithRemosaicLinear()): apply Hill–Zhou–Saul–Shacham (PETS-2016 §4) composite Gaussian-blur → block-average operator to collapse σ-mismatch and JPEG noise; opt-in via CLI or API, auto-selects block grid as max(2, round(σ)) and supports both sRGB and linear-light averaging (GEGL/GIMP targets). Honest note: on self-consistent synthetics the plain path already converges, so the benefit is for real-world σ-mismatch/JPEG; never regresses vs plain.
  • Zero-config font matching. Recovery needs the redaction's typeface — so with no --font, UnPixel sweeps a built-in set of redistributable fonts (Liberation Sans/Serif/Mono ≈ Arial/Times/Courier, Carlito ≈ Calibri, Caladea ≈ Cambria, Adwaita Mono, Noto Sans Mono, Source Code Pro & JetBrains Mono for code) in parallel and keeps the best fit by whole-image score. Or match it yourself with --font/--font-size/--letter-spacing (and sweep your own via repeated --font/--font-dir). Library: the fonts bundle + RecoverMultiFont; render.NewXImageFromFonts for a custom face. For real-world images, supplying the exact font via --font or --font-dir significantly improves recovery, since font fidelity dominates the score.
  • LM-guided monospace mosaic decoder (--decoder mono-hmm): fuses a bigram language model with per-character image signal via left-to-right beam search, avoiding the charset^length exponential barrier. Recovers long monospace text (10–50+ characters) when character-by-character signals are weak. Options: --lang en|fr, --font <bundled-or-path>, --charset. Limitation: font fidelity dominates — exact font via --font is strongly recommended for real captures.
  • Reference-matching mosaic decoder (--decoder ref-match): Depix-style per-glyph reference matching that recovers arbitrary content (passwords, code, random strings) and works on proportional fonts (not just monospace). Renders candidate glyphs, matches columns left-to-right with zero language assumption. Options: --font <bundled-or-path>, --charset. Limitation: works when font is known; bundled sweep is exploratory, exact font required for real reliability.
  • Input normalization for real-world captures (--normalize): morphological background estimation and removal (additive/multiplicative), dark-theme auto-inversion, and optional JPEG deblocking. Extends blur recovery to textured/vignette/dark-background scenarios. Multi-format input decoding: PNG/JPEG/GIF/WebP/BMP/TIFF.
  • Robust mosaic-vs-blur auto-detection (InferBlockSizeRobust): detects pixelated grids even when resampled, anti-aliased, or JPEG-compressed. Routes true pixelizations to mosaic pipeline and ambiguous cases to blur. Best-effort result surfacing (Result.BelowThreshold) returns the best candidate even when it falls below the acceptance threshold, suitable for exploratory analysis.
  • Automatic Top-K pruning for code. When a language model is set and the charset is wide (≥40 runes), the search automatically narrows candidates to the most-likely next characters per language, keeping the search tractable (~10.8× speedup for wide charsets) while maintaining full recall on the default small-charset path.
  • Ranked results, not just one guess. Each Result carries the top-N candidates per grid offset (sorted by score, ties broken deterministically) plus Confidence/Ambiguity and a whole-image BestTotal distance — comparable across runs, so it can rank fonts or styles — letting callers surface alternatives instead of a single best guess.
  • Self-consistent correctness. Fidelity is judged by a redaction round-trip (redact a known plaintext, then recover it). Matching a Chromium-rendered redaction is a documented Phase-2 goal (needs a chromedp renderer).
  • 87% test coverage across rendering, pixelation, metrics, search, CLI, and end-to-end.

Install

As a library:

go get github.com/oioio-space/unpixel

As a command-line tool:

go install github.com/oioio-space/unpixel/cmd/unpixel@latest

From source (development) — uses mise for the toolchain and tasks:

git clone https://github.com/oioio-space/unpixel.git
cd unpixel
mise run setup     # install pinned tools + wire git hooks
mise run test      # verify the build
mise run           # list all tasks

Requires Go 1.26+.

Command-line tool

unpixel redacted.png                       # zero-config: sweeps the built-in fonts; best guess to stdout
cat redacted.png | unpixel -               # read PNG from stdin
unpixel --format json --top 10 redacted.png
unpixel --strategy beam --metric ssim --workers 8 redacted.png

# Match a known typeface yourself (skips the sweep) — e.g. a Consolas code screenshot:
unpixel --font Consolas.ttf --font-size 24 --letter-spacing -0.2 -b 5 redacted.png

# Sweep your own candidate fonts (or a whole directory) instead of the built-ins:
unpixel --font Arial.ttf --font Consolas.ttf --font Courier.ttf -b 5 redacted.png
unpixel --font-dir /usr/share/fonts/truetype -b 5 redacted.png

# Decoders (v0.11.0+):
unpixel --decoder mono-hmm --lang en image.png                              # LM-guided monospace decoder
unpixel --decoder mono-hmm --lang fr --font "JetBrains Mono" long-text.png  # with specific font
unpixel --decoder ref-match --font "Liberation Sans" passwords.png          # reference-matching for arbitrary content
unpixel --decoder window-hmm --lang en image.png                            # window-grid beam decoder (proportional fonts)
unpixel --decoder trained-hmm image.png                                     # learned-emission Viterbi HMM (digit/PIN codes)
unpixel --decoder varfont image.png                                         # variable-font fitting (coordinate descent)
unpixel --decoder varfont --varfont-text "known" image.png                  # varfont with known text (calibration mode)
unpixel --decoder did image.png                                             # Document-Image-Decoding trellis (boundary-free DP)
unpixel --normalize --redaction blur real-blur.jpg                          # normalize input + blur recovery on JPEG

# Deblurring (post-v0.11.0):
unpixel --l0-deblur image.png                                               # non-blind L0 text deblur (Pan CVPR-2014, opt-in)

# Quick wins (v0.11.0):
unpixel --gamma auto image.png                                              # auto-select sRGB or linear-light
unpixel --blind --letter-spacing-search image.png                           # calibrate letter-spacing via search
unpixel --blind --lang en image.png                                         # bilingual blind recovery with 10k-word dictionary

# Big bets & advanced (v0.11.0):
unpixel --decoder varfont --varfont-axes "wght:200:900:500" image.png      # sweep variable-font weight axis
unpixel --thmm-lang en --thmm-jpeg 85 image.png                             # trained-HMM with language structure + JPEG augmentation

# Re-mosaic correction (Hill–Zhou–Saul–Shacham PETS-2016, §4):
unpixel --remosaic --redaction blur blurred.png                             # apply blur→remosaic error correction
unpixel --remosaic-grid 4 --redaction blur image.png                        # pin the remosaic block grid
unpixel --remosaic-linear --redaction blur gimp-output.png                  # use linear-light remosaic (GEGL/GIMP)

Key flags (unpixel --help for the full list):

Flag Default Purpose
--charset a–z + space Candidate characters to try
--charset-preset Named charset when --charset is unset: lower, alnum, ascii/code
--block-size, -b 0 (auto) Pixelation block size; 0 auto-detects from the image
--font embedded (Liberation Sans) TTF/OTF font to render candidates; repeat to sweep and keep the best fit
--font-dir Directory of TTF/OTF fonts to sweep (each tried; best whole-image fit wins)
--font-size 0 (32) Font size in points to match the redaction
--letter-spacing 0 Extra px after each glyph, like CSS letter-spacing (may be negative)
--redaction auto auto, mosaic, or blur (blur auto-detected when there's no mosaic grid)
--blur-sigma 0 (auto) Gaussian blur radius for --redaction blur; 0 estimates it from the image
--blur-exact off Force the exact Gaussian (default uses the ~3× faster box approx at large σ)
--deblur 0 (off) Optional Richardson-Lucy deconvolution iterations (exploratory preprocessing)
--denoise -1 (auto) Median denoise for --blind mode: -1 auto-detects impulse noise, 0 disables, N forces N×N window
--decoder default default (guided DFS/beam), mono-hmm (LM-guided monospace beam), ref-match (reference-matching for known fonts), window-hmm (grid-window beam for proportional fonts), trained-hmm (learned-emission Viterbi HMM for constrained alphabets), varfont (variable-font fitting via coordinate descent), did (Document-Image-Decoding trellis, boundary-free DP)
--gamma srgb auto (choose sRGB or linear-light, keep lower distance), linear (force linear-light), srgb (force sRGB)
--letter-spacing-search off Enable letter-spacing sweep (Bishop-Fox method); records Result.LetterSpacing
--varfont-text Known text for variable-font calibration mode (bypasses blind search)
--varfont-axes Variable-font axis bounds, e.g. wght:200:900:500 (axis:min:max:step)
--varfont-linear off Use linear-light rendering for variable-font fitting
--thmm-lang off Enable language-structure training for trained-HMM (en or fr)
--thmm-jpeg 0 JPEG quality augmentation for trained-HMM emissions (e.g. 85 trains on lossy variants)
--remosaic off Enable Hill–Zhou–Saul–Shacham PETS-2016 §4 composite blur→remosaic error correction (scales σ-mismatch and JPEG noise)
--remosaic-grid 0 (auto) Block grid size for --remosaic; 0 auto-detects as max(2, round(σ))
--remosaic-linear off Use linear-light block averaging for --remosaic (GEGL/GIMP-rendered targets)
--strategy guided guided (full DFS), beam (bounded), or mono (monospace fast-path)
--beam-width 0 (16) Candidates kept per depth level under --strategy beam
--metric pixelmatch pixelmatch (faithful; auto pixelmatch-fast on block-average mosaic for identical results, zero-config) or ssim (structural)
--language off Break ties between equal-image candidates toward plausible text (char-bigram prior)
--secrets off Boost plausibility of structured formats (UUID, API token, Luhn checksums, common passwords) and dictionary words
--workers 0 (all CPUs) Grid offsets searched concurrently; also the sweep's core budget
--top, -n 5 Ranked candidates to report
--normalize off Enable input normalization for blur recovery: morphological background removal + dark-theme inversion
--normalize-bg divide Background removal mode: divide (multiplicative), subtract (additive), or none
--deblock 0 (off) Median deblocking radius for JPEG (0 = off, N = force (2N+1)×(2N+1) kernel)
--format, -f text text or machine-readable json
--timeout 0 (none) Max recovery time
--l0-deblur off Enable non-blind Pan-CVPR-2014 L0 text deblurring (requires known σ from blur-sigma inference)

The best guess prints to stdout (so it pipes cleanly); the ranked table and live progress go to stderr. --format json emits a stable schema (best_guess, confidence, total_score, top, and a ranked fonts array when sweeping).

Tip: lower block sizes carry less information per glyph, so a tighter --threshold (e.g. 0.1) prunes coincidental matches and lets the whole-image score pick the complete answer.

Quick start

One call — give it an image (or a path), get the text back:

package main

import (
	"context"
	"fmt"

	"github.com/oioio-space/unpixel"
	_ "github.com/oioio-space/unpixel/defaults" // wires the default renderer/pixelator/metric/strategy
)

func main() {
	res, err := unpixel.RecoverFile(context.Background(), "redacted.png")
	if err != nil {
		panic(err)
	}
	fmt.Println("recovered:", res.BestGuess)
}

Recover / RecoverReader / RecoverFile take functional options for the common knobs while auto-detecting the rest (block size, …):

res, err := unpixel.Recover(ctx, img,
	unpixel.WithCharset("abcdefghijklmnopqrstuvwxyz0123456789 "),
	unpixel.WithWorkers(8),
)

Unknown typeface? Build a renderer per candidate font (you supply the .ttf) and let RecoverMultiFont recover with each in parallel, ranked best-fit first:

import "github.com/oioio-space/unpixel/defaults"

var rs []unpixel.Renderer
for _, data := range fontFiles { // each is TTF/OTF bytes you read
	r, err := defaults.RendererFromFonts(data, nil)
	if err != nil {
		panic(err)
	}
	rs = append(rs, r)
}
ranked, err := unpixel.RecoverMultiFont(ctx, img, rs, unpixel.WithBlockSize(5))
best := ranked[0] // lowest BestTotal — the font that fit best
fmt.Printf("%q via font #%d\n", best.Result.BestGuess, best.Index)

For streaming progress or full control, drop to the low-level Engine (the helpers wrap exactly this):

Low-level Engine API
eng, err := unpixel.New(img, unpixel.Config{}) // zero Config = faithful defaults
if err != nil {
	panic(err)
}
progress, results := eng.Run(context.Background())
go unpixel.OnProgress(progress, func(p unpixel.Progress) {
	fmt.Printf("\rbest: %-20q (%.3f)", p.BestGuess, p.BestScore)
})
fmt.Println("\nrecovered:", (<-results).BestGuess)
Blind & bilingual recovery (experimental)

UnPixel can recover text without knowing the font, block size, or language in advance. The blind package auto-detects the redaction region, calibrates the block size and font size, sweeps built-in fonts, uses a frequency-weighted bilingual prior (French or English) to score candidates, and automatically denoises salt-pepper/impulse noise by default:

unpixel --blind --lang fr testdata/real/marx.png
unpixel --blind --lang en --block-size 8 image.png
unpixel --blind --lang fr --denoise 0 image.png    # disable auto-denoise
unpixel --blind --denoise 3 image.png              # force 3×3 median window

In Go, use blind.Recover with language selection and optional denoise control:

import "github.com/oioio-space/unpixel/blind"

res, err := blind.Recover(ctx, img,
	blind.WithLanguage(blind.French), // or blind.English (the default)
	blind.WithDenoise(-1),             // auto-detect (default); 0 = off, N = force N×N window
)
if err != nil {
	panic(err)
}
fmt.Println(res.Text)
fmt.Println("Font:", res.Font, "Block:", res.Block, "Distance:", res.Dist)
fmt.Println("Denoise applied:", res.Denoise)  // radius used, or 0 if none

The blind package re-exports English/French (and ParseLanguage for a string flag), so no internal import is needed. Status: experimental. Blind recovery is proven end-to-end on synthetic mosaics rendered in the bundled fonts (sans/serif/mono); it is most reliable there. Real captures in a font outside the bundle, or containing punctuation/apostrophes outside the dictionary, recover only partially. It is also compute-heavy: a large multi-line screenshot with the font sweep can take many minutes — pin --block-size/--font-size and a single language to keep it tractable.

For long monospace text where character-by-character image signals are weak, the mosaictext package offers an opt-in LM-guided beam decoder that breaks the exponential barrier. Instead of generate-and-test per character (charset^length candidates), it decodes left-to-right with a bigram language model fused into the objective:

text, err := mosaictext.DecodeHMM(ctx, img,
	mosaictext.WithLanguage(lang.French),  // or English
	mosaictext.WithCharset("abcdefg…"),    // optional; defaults to ASCII letters + common punctuation
	mosaictext.WithFont("JetBrains Mono"), // pins a bundled mono font; omit to sweep all mono fonts
)
if err != nil {
	panic(err)
}
fmt.Println(text)

On the command line, use --decoder mono-hmm to activate it:

unpixel --decoder mono-hmm --lang fr image.png                            # auto-detect font
unpixel --decoder mono-hmm --lang en --font "Source Code Pro" image.png   # pin the font
unpixel --decoder mono-hmm --lang en --font Consolas.ttf image.png   # supply a custom TTF/OTF

API: mosaictext.DecodeHMM(ctx, img, opts...) with options WithLanguage, WithCharset, WithEmissionTemperature, WithFont (bundled mono by name), WithFontFile/WithFontFileBold (caller-supplied TTF/OTF bytes).

Why it matters: The default generate-and-test scales as charset^(text length), so a 25-character line becomes intractable. The beam decoder is polynomial in length, scanning once left-to-right with a bounded beam (default width 8) and scoring each cell by fused image-MSE + log-probability transitions. This is the principled successor to post-hoc language reranking — the LM is now part of the optimized objective.

Key limitation: Recovery quality is bounded by font fidelity. On synthetic mosaics in bundled fonts (Liberation, JetBrains Mono, Noto Sans Mono, etc.), the decoder recovers full-length sequences. On real captures where the exact font is not bundled, the decode is partial or incorrect. Mitigation: supply the exact font via --font (path to a TTF/OTF). When the font is known, this is expected to recover most real monospace redactions. Note: an exact global-MAP Viterbi variant was attempted and rejected — the per-cell emissions are not independent (block-boundary averaging couples adjacent cells), so the bigram lookahead overwhelms the emission signal even at tuned temperatures; the beam search avoids this by scoring each cell against the already-committed context.

Reference-matching mosaic decoder (arbitrary content + proportional fonts)

For arbitrary text (passwords, source code, random strings) and proportional-width fonts, the mosaictext package offers a Depix-style reference-matching decoder that does not assume language structure. It renders per-glyph references in candidate fonts, pixelates them with the target's block grid, and greedily matches block columns left-to-right:

text, err := mosaictext.DecodeReference(ctx, img,
	mosaictext.WithRefCharset("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"),
	mosaictext.WithRefFont("Liberation Sans"), // pins a bundled font; omit to sweep all bundled fonts
)
if err != nil {
	panic(err)
}
fmt.Println(text)

On the command line, use --decoder ref-match to activate it:

unpixel --decoder ref-match image.png                                  # auto-detect font
unpixel --decoder ref-match --font "Liberation Sans" image.png         # pin the font
unpixel --decoder ref-match --font Arial.ttf image.png                 # supply a custom TTF/OTF
unpixel --decoder ref-match --charset "0-9A-Z" image.png               # narrow the charset

API: mosaictext.DecodeReference(ctx, img, opts...) with options WithRefCharset (default: all printable ASCII), WithRefFont (bundled font by name), WithRefFontFile/WithRefFontFileBold (caller-supplied TTF/OTF bytes), WithRefLinear (tri-state: auto/sRGB/linear-light block averaging).

Why it matters: Unlike the LM-guided decoder (which assumes character bigrams), reference-matching makes no language assumption, so it recovers arbitrary secrets (passwords, codes, random strings) exactly when the font is known. It also works on proportional fonts (not just monospace), expanding beyond code-like content. On self-consistent synthetic fixtures (e.g., "Pa55w0rd!" in Liberation Sans proportional, "X7kQ2mR9" in Liberation Mono), the decoder recovers the text exactly (distance near-zero).

Font contract: When you supply a font via --font (path to TTF/OTF) or WithRefFontFile, that font is used exclusively and the bundled sweep is skipped — this is the primary mitigation for real-world images. When no font is specified, the decoder sweeps all bundled fonts (Liberation, Carlito, Caladea, Source Code Pro, etc.) in both sRGB and linear-light modes, keeping the result with the lowest whole-image block distance.

Key limitation: Like all generate-and-test approaches, recovery is bounded by font fidelity. On real images where the exact font is not bundled (e.g., Notepad/Sublime screenshots), the bundled-sweep decode is incorrect. The exact-font path (--font yourfont.ttf or WithRefFontFile) is the technique's strength and is expected to recover redactions when the font is known.

Grid-window beam decoder (proportional-font mosaic text recovery)

For grid-aligned mosaic text (not monospace-limited), the mosaictext package offers a grid-window HMM beam decoder that slides a window over pixelated grid cells and scores each candidate character by its per-window block MSE. This recovers proportional-font mosaics that the monospace mono-hmm cannot:

text, err := mosaictext.DecodeWindowHMM(ctx, img,
	mosaictext.WithWHMMCharset("0123456789 "),  // candidate alphabet
	mosaictext.WithWHMMFont("Liberation Sans"), // or omit to sweep bundled fonts
)
if err != nil {
	panic(err)
}
fmt.Println(text)

On the command line, use --decoder window-hmm to activate it:

unpixel --decoder window-hmm image.png                          # auto-detect font
unpixel --decoder window-hmm --lang en --font Arial.ttf image.png  # supply a custom TTF/OTF
unpixel --decoder window-hmm --charset "0-9" image.png          # narrow the charset

API: mosaictext.DecodeWindowHMM(ctx, img, opts...) with options WithWHMMCharset (default: "0123456789 "), WithWHMMFont (bundled font by name), WithWHMMFontFile/WithWHMMFontFileBold (caller-supplied TTF/OTF bytes), WithWHMMLinear (tri-state: auto/sRGB/linear-light block averaging), WithWHMMBeamWidth (default: 16), WithWHMMSeed (optional RNG seed for reproducibility).

Why it matters: Proportional fonts have variable-width glyphs (unlike monospace), so the character-grid alignment changes per position. The window-HMM beam variant scores each grid cell window independently by MSE, allowing per-glyph recovery without assuming monospace structure. On synthetic grid-aligned fixtures (e.g., "hello world" rendered proportional then pixelated), the decoder recovers text exactly (distance near-zero).

Design note: This is the grid-window beam variant, not the learned-emission HMM (Hill et al. PETS-2016 full model with k-means + Viterbi); the latter requires blind column-anchored observations (a structural redesign). The beam search variant trades some optimality for robustness to font mismatch.

Key limitation: Like all approaches, recovery is bounded by font fidelity. On real images where the exact font is not bundled, the decode is inaccurate. The exact-font path (--font yourfont.ttf or WithWHMMFontFile) is the strength and is expected to recover proportional-font redactions when the font is known.

Learned-emission Viterbi HMM decoder (constrained alphabets)

For constrained character alphabets (digits, PINs, check numbers), the mosaictext package offers a learned-emission Viterbi HMM decoder that trains on rendered examples to model per-window block observations and character-tuple state transitions. It then decodes the target via a single Viterbi pass over the block grid without assuming character boundaries — true column-anchored blind recovery:

text, err := mosaictext.DecodeTrainedHMM(ctx, img,
	mosaictext.WithTHMMCharset("0123456789"),         // digits: PINs, credit cards, check numbers
	mosaictext.WithTHMMFont("Liberation Mono"),       // or omit to sweep bundled fonts
)
if err != nil {
	panic(err)
}
fmt.Println(text)

On the command line, use --decoder trained-hmm to activate it:

unpixel --decoder trained-hmm image.png                                  # auto-detect font
unpixel --decoder trained-hmm --charset "0-9" --font Arial.ttf image.png # with specific font
unpixel --decoder trained-hmm --charset "0-9A-Z" image.png               # custom charset (auto-font sweep)

API: mosaictext.DecodeTrainedHMM(ctx, img, opts...) with options WithTHMMCharset (default: digits), WithTHMMFont (bundled font by name), WithTHMMFontFile/WithTHMMFontFileBold (caller-supplied TTF/OTF bytes), WithTHMMLinear (tri-state: auto/sRGB/linear-light block averaging), WithTHMMK (KMeans clusters; default 128), WithTHMMWindow (window width in blocks; default auto), WithTHMMCorpus (training corpus size; default 2000), WithTHMMSeed (PRNG seed for reproducibility).

Why it matters: This is the genuine learned-emission HMM from Hill et al. (PETS-2016), with k-means quantized block observations and empirically-trained state transitions. Unlike beam search (which commits partial decisions), Viterbi finds the globally optimal state path, provided the model captures the true distribution.

Honest scope & limitations:

  • Recovers the constrained-alphabet case exactly on self-consistent synthetic fixtures (digits/PINs rendered and re-pixelated at the same grid/offset). Achieves the paper's reference result (~100% on digit codes).
  • Per-window emission accuracy is modest (~55%) — the k-means clustering of block windows loses fine structure — but global path optimization compensates when the true answer is structurally plausible.
  • Brittle to grid/render geometry mismatch: The model is trained on a specific (block size, font size, font face, block phase) tuple. On independent test images with different geometry, even when the same font, accuracy drops sharply (paper's Fig-14 offset-sensitivity; observed <5% on paper-parity fixtures with different image sources).
  • Not a general decoder for real images. Font fidelity and geometry drift dominate recovery success. On real captures, supply the exact font via --font and ensure block-size consistency. For out-of-sample geometry, window-hmm (beam) is currently more robust.
  • Best suited for: digits, PINs, check numbers, or other highly constrained codes on self-consistent redactions. For proportional-font text or weak per-window signal, use window-hmm or the LM-guided mono-hmm instead.
Document-Image-Decoding trellis decoder (boundary-free DP)

For variable-width fonts and arbitrary text (proportional or monospace), the mosaictext package offers a Document-Image-Decoding (DID) trellis decoder that discovers character boundaries via dynamic programming over glyph start-columns. Unlike reference-matching (which assumes known boundaries), DID jointly optimizes boundary positions and character identity:

text, err := mosaictext.DecodeDID(ctx, img,
	mosaictext.WithDIDCharset("abcdefghijklmnopqrstuvwxyz "),
	mosaictext.WithDIDFont("Liberation Sans"),
)
if err != nil {
	panic(err)
}
fmt.Println(text)

On the command line, use --decoder did to activate it:

unpixel --decoder did image.png                                  # auto-detect font
unpixel --decoder did --charset "0-9a-z " --font Arial.ttf image.png  # with specific font
unpixel --decoder did --lang en image.png                        # with language prior

API: mosaictext.DecodeDID(ctx, img, opts...) with options WithDIDCharset (default: lowercase + space), WithDIDFont (bundled font by name), WithDIDFontFile/WithDIDFontFileBold (caller-supplied TTF/OTF bytes), WithDIDLinear (tri-state: auto/sRGB/linear-light block averaging), WithDIDLanguage (optional bigram LM for tie-breaking).

Why it matters: The trellis DP formulation discovers both boundaries and characters in a single pass, allowing exact recovery on clean monospace and proportional short text where reference-matching and beam search cannot — a first for UnPixel. The emission is render-glyph→pixelate→distance (exact forward model), and transitions are character-adjacency constraints.

Honest scope & limitations:

  • Recovers clean short text exactly on self-consistent synthetic fixtures (both monospace and proportional) where the font is known and the text fits cleanly in a grid-aligned band. Achieves 100% on synthetic "hello world" in both Liberation Sans (proportional) and Liberation Mono, with distance near-zero.
  • Per-glyph-isolated emission breaks on real images: The DP scores each glyph independently (render+pixelate+distance), but on real redactions, boundaries interact — pixels at the boundary between two glyphs average together post-pixelation, so isolated emissions mismatch the full-line pixelation. This is why real-world recovery remains incomplete on the SICK corpus: the "sick sentence" with complex characters fails on boundary emission, not on the DP itself.
  • Font fidelity dominates: Like all approaches, when the exact font is not supplied, accuracy drops sharply. The exact-font path (--font yourfont.ttf or WithDIDFontFile) is the strength and is expected to recover clean short-text redactions when the font is known.
  • Best suited for: clean, short, grid-aligned monospace or proportional text (up to ~5–10 characters) where the exact font is known. For longer, noisier, or real-world images, use window-hmm (beam) or mono-hmm (LM-guided).

Public API (root package unpixel and sub-packages blind / mosaictext):

Symbol Purpose
Recover(ctx, image.Image, ...Option) (Result, error) One call: search and return the best result
RecoverReader(ctx, io.Reader, ...Option) / RecoverFile(ctx, path, ...Option) Decode then Recover
RecoverMultiFont(ctx, image.Image, []Renderer, ...Option) ([]FontResult, error) Sweep candidate fonts in parallel; results ranked best-fit first by BestTotal
RecoverBlurred(ctx, image.Image, ...Option) (Result, error) Zero-config Gaussian-blur recovery: auto-estimates σ, searches σ as a dimension (adaptive or bounded sweep), defaults to beam+language-prior for longer words
With* options (WithCharset, WithWorkers, WithRenderer, WithStrategy, WithPriors, …) Tweak the common knobs; WithConfig seeds a full Config
New(redacted image.Image, cfg Config) (*Engine, error) Build an engine; zero Config = faithful defaults
(*Engine).Run(ctx) (<-chan Progress, <-chan Result) Run the search; stream progress, deliver the result
(*Engine).Config() Config Resolved config (e.g. the inferred block size)
OnProgress(ch <-chan Progress, fn func(Progress)) Drain progress events into a callback (any UI)
InferBlockSize(image.Image) int Detect the mosaic block size (exact GCD of grid boundaries)
InferBlockSizeRobust(image.Image) (blockSize int, support float64) Detect mosaic block size robustly (handles resampled/anti-aliased/JPEG'd grids); returns support confidence
InferBlurSigma(image.Image) float64 Estimate Gaussian blur radius σ from image contrast
InferImpulseNoise(image.Image) float64 Detect impulse (salt-pepper) noise; returned value used by blind.Recover to auto-denoise
Renderer, Pixelator, Metric (PixelmatchFast via defaults.PixelmatchFastMetric()), Strategy Pluggable pipeline interfaces; PixelmatchFast skips anti-aliasing detection on block-average mosaic (identical results, ~35% faster), keeps faithful Pixelmatch on blur for cross-engine robustness
Config, Style, Result, FontResult, Eval, Offset, Progress, EventKind Configuration and result/event types
blind.Recover(ctx, image.Image, ...Option) (*Recovery, error) Blind bilingual recovery (FR/EN) without knowing font/block/offset; re-exports English/French/ParseLanguage
blind.With* options (WithLanguage, WithBlock, WithOffset, WithFontSize, WithLinear, WithFonts, WithMetric) Fine-tune blind recovery or override auto-detection
mosaictext.Decode(ctx, image.Image, ...Option) (string, error) Zero-config monospace mosaic decoder (auto grid inference + character recognition)
mosaictext.DecodeHMM(ctx, image.Image, ...Option) (string, error) LM-guided beam decoder for long monospace mosaic text; fuses bigram language model into search objective; polynomial in length, breaks charset^len barrier; font-limited on out-of-bundle typefaces
mosaictext.With* options (WithLanguage, WithCharset, WithEmissionTemperature, WithFont, WithFontFile, WithFontFileBold) Configure DecodeHMM font/language/charset/tuning
mosaictext.DecodeReference(ctx, image.Image, ...RefOption) (string, error) Reference-matching decoder: recovers arbitrary content (passwords, code, random strings) from mosaics via per-glyph reference matching; no language assumption; exact on self-consistent fixtures when font is known; works on proportional fonts
mosaictext.WithRef* options (WithRefCharset, WithRefFont, WithRefFontFile, WithRefFontFileBold, WithRefLinear) Configure DecodeReference font/charset/color-space selection
mosaictext.DecodeWindowHMM(ctx, image.Image, ...WHMMOption) (string, error) Grid-window beam decoder: recovers proportional-font mosaic text via per-cell window MSE scoring; no monospace assumption; exact on grid-aligned fixtures when font is known
mosaictext.WithWHMM* options (WithWHMMCharset, WithWHMMFont, WithWHMMFontFile, WithWHMMFontFileBold, WithWHMMLinear, WithWHMMBeamWidth, WithWHMMSeed) Configure DecodeWindowHMM font/charset/color-space/beam tuning
mosaictext.DecodeTrainedHMM(ctx, image.Image, ...THMMOption) (string, error) Learned-emission Viterbi HMM decoder: trains on rendered corpus, decodes via single column-anchored Viterbi pass; exact on self-consistent constrained alphabets (digits/PINs), brittle to geometry mismatch on real images
mosaictext.WithTHMM* options (WithTHMMCharset, WithTHMMFont, WithTHMMFontFile, WithTHMMFontFileBold, WithTHMMLinear, WithTHMMLanguage, WithTHMMJPEG, WithTHMMK, WithTHMMWindow, WithTHMMCorpus, WithTHMMSeed) Configure DecodeTrainedHMM font/charset/language-structure/JPEG-augmentation/KMeans-K/window-width/corpus-size/color-space
mosaictext.DecodeDID(ctx, image.Image, ...DIDOption) (string, error) Document-Image-Decoding trellis decoder: boundary-free DP over glyph start-columns; exact on clean monospace and proportional short text when font is known; per-glyph-isolated emission limits real-world recovery (boundary interaction)
mosaictext.WithDID* options (WithDIDCharset, WithDIDFont, WithDIDFontFile, WithDIDFontFileBold, WithDIDLinear, WithDIDLanguage) Configure DecodeDID font/charset/language-prior/color-space
mosaictext.DecodeVarFont(ctx, image.Image, ...VarFontOption) (VarFontResult, error) Variable-font fitting decoder: fits variable-font axes (e.g. weight) via coordinate descent to match the redaction; calibration mode (known text) + best-effort blind mode
mosaictext.WithVarFont* options (WithVarFontText, WithVarFontAxes, WithVarFontLinear) Configure DecodeVarFont known text / axis bounds / color-space

Configuration

Pass a Config to unpixel.New. Every zero value falls back to a documented default.

Field Type Default Meaning
Charset string "abcdefghijklmnopqrstuvwxyz " Candidate characters to search
MaxLength int 20 Maximum plaintext length
BlockSize int 0 → auto / 8 Pixelation block size; ≤0 auto-detects via InferBlockSize (or InferBlockSizeRobust for resampled/anti-aliased/JPEG'd mosaics), else falls back to 8
Threshold float64 0.25 Max image-distance score (0–1) to keep a candidate
SpaceThreshold float64 0.5 Looser threshold for extending with a space (whitespace blur)
ThresholdFor func(rune) float64 space→SpaceThreshold, else Threshold Per-character threshold; override for new char classes
TopN int 5 Ranked candidates kept per offset in Result.TopN
Style Style Liberation Sans, 32 px, white bg Font size/weight/padding and LetterSpacing for rendering (the font itself comes from the Renderer)
Renderer Renderer x/image/font (pure Go) Text → raster
Pixelator Pixelator block-average Raster → pixelated
Metric Metric orisano/pixelmatch Image-distance score
Strategy Strategy guided DFS Candidate-space search (defaults.GuidedStrategy() / defaults.BeamStrategy(width))
BeamWidth int 16 Candidates kept per depth level — beam strategy only
CacheSize int 4096 LRU size for prefix-render memoization — beam strategy only (0 disables)
Workers int 0 → all CPUs Grid offsets probed/searched concurrently; 1 forces sequential. Never changes output

Selecting beam search (bounded branching + prefix-render caching) instead of the default DFS:

cfg := unpixel.Config{Strategy: defaults.BeamStrategy(0)} // 0 = use BeamWidth (default 16)

Architecture

Package layout
github.com/oioio-space/unpixel
├── unpixel.go              # Engine, Config, Result, Eval, Offset, Progress; the 4 interfaces
├── defaults/               # wires the default components (breaks the root↔internal import cycle)
├── fonts/                  # bundled redistributable fonts (OFL/Apache) for the zero-config sweep
├── internal/
│   ├── imutil/             # crop / pad / compose; blueMargin & leftEdge detection
│   ├── pixelate/           # block-average pixelator; grid-origin crop; white padding
│   ├── metric/             # pixelmatch (faithful default) + simple RGB metric
│   ├── render/             # pure-Go x/image/font renderer (embedded or custom fonts; letter-spacing)
│   │   └── fonts/          #   embedded Liberation Sans (Regular/Bold) + OFL license
│   └── search/             # offset discovery + marginal cropping + guided DFS + whole-image ranking
└── cmd/unpixel/            # CLI (urfave/cli/v3): recovery, font sweep, text/JSON output

The four interfaces (Renderer, Pixelator, Metric, Strategy) live in the root package so they can be implemented and injected from outside; the concrete implementations stay under internal/.

Rendering fidelity & Phase-2 roadmap

The original unredacter rasterizes via Chromium; UnPixel uses pure-Go golang.org/x/image/font. Byte-identical glyphs across engines aren't possible (different hinting/anti-aliasing), so correctness is judged by self-consistency: redact a known plaintext with UnPixel's own renderer, then recover it. Recovering a Chromium-produced redaction (e.g. the original secret.png) is a Phase-2 goal requiring a chromedp renderer.

Landed: top-N confidence/ambiguity reporting; a beam-search strategy with prefix-render memoization (defaults.BeamStrategy); an SSIM metric (defaults.SSIMMetric); automatic block-size inference (InferBlockSize); and goroutine fan-out over offset discovery and per-offset search (Config.Workers, deterministic merge). Still ahead (behind the interfaces; the faithful default stays put): edge-aware metrics and the chromedp fidelity renderer (deferred — it would require a Chrome binary at runtime/CI). Details in docs/DESIGN.md § Phase-2.

Real-world images: input normalization, known limitations and roadmap

UnPixel recovers synthetic mosaics and Gaussian blurs accurately with the right font and parameters. Real internet images (screenshots, GIMP-rendered redactions, etc.) remain challenging. Zero-config recovery succeeds on images where the font is in the bundled set and the redaction matches our assumptions; it struggles when:

  • Font is out of bundle: supplying --font / --font-dir with the exact typeface dramatically improves recovery (font fidelity dominates the score).
  • Long text with sparse signal: text shorter than ~5 glyphs or very tall/thin glyphs (monospace, code) produce weak per-character image signals; generate-and-test explores an exponential space. A language model prior + beam search (enabled by --language or unpixel --blind) helps.
  • Blur vs mosaic ambiguity: the auto-detector now correctly identifies real pixelated images (InferBlockSizeRobust), but miscalibrated parameters or unusual encodings (JPEG compression, resampling) can still confuse it.
  • Real blur is not Gaussian: internet JPEG blur is often motion or defocus, not clean Gaussian, so the score stays above threshold and recovery returns no candidate.
  • Textured / vignette / dark backgrounds: real captures often have a non-white background or dark-theme rendering that breaks the σ-search's clean-Gaussian-on-flat-white assumption. Input normalization (opt-in --normalize or unpixel.WithNormalize(...)) preprocesses the image via grayscale morphological background estimation, removing additive/multiplicative background and auto-inverting dark themes, so the redaction matches the model assumptions. Validated on synthetic fixtures with textured/vignette/dark/JPEG blocking; on real images it restores the assumption but does not by itself recover the text — font fidelity (P-B / --font) and deblurring (P-C) remain the blocking factors. Design insight: normalization is the lever, not deconvolution — sharpening the input (Wiener/blind L0 deconvolution) fights the generate-and-test loop, so P-C normalizes the input and feeds the existing σ-search unchanged. Wiener/blind L0 are deferred for future investigation.

Multi-format input support: the CLI now decodes JPEG, GIF, WebP, BMP, and TIFF in addition to PNG (via image package registration), so real-world .jpg/.jpeg/.webp captures load cleanly.

Roadmap (PROGRESS.md § P5–P7): phased pure-Go extensions — HMM/Viterbi mosaic decoder (P-A, delivered), Depix-style reference-matching with self-synthesized fonts (P-B), input normalization for blur recovery (P-C, delivered), and foundation work (P-D: robust auto-detect, best-effort surfacing). All opt-in, benchmarked, zero regressions on synthetic corpus.

Contributing

mise run test:watch   # TDD loop
mise run lint         # gofmt + go vet + golangci-lint
mise run test         # tests
mise run cover        # coverage report (floor: 85%)
mise run bench        # hot-path benchmarks (benchstat-proven)
mise run gen          # regenerate test fixtures (mise run gen:check verifies no drift)
mise run ci           # everything CI runs

Commits go through git hooks — artifacts → secrets (gitleaks) → vulns (gosec + govulncheck) → style (golangci-lint) → cgo:check — plus a /simplify review. CI re-runs all of it and adds a CycloneDX SBOM scanned by grype, a full-history secret scan, and the coverage floor.

Two absolute project rules: the project stays pure Go (no CGO), and the hot path is benchmarked with benchstat-proven changes. See CLAUDE.md for tooling and PROGRESS.md for the roadmap.

Credits

License

GPL-3.0-or-later — see LICENSE. UnPixel is a derivative work of Bishop Fox's unredacter (GPL-3.0); the copyleft license is preserved. © the UnPixel authors; original © Bishop Fox.

Documentation

Overview

Package unpixel reconstructs text hidden behind pixelation.

UnPixel attempts to recover the original text from a pixelated (mosaic-blurred) image region. The algorithm works by rendering each candidate string into an off-screen image, re-applying the same block-average pixelation with the same grid origin, and measuring the pixel-level distance against the redacted region. A guided depth-first search explores the candidate space character by character, pruning branches whose cumulative score exceeds the configured threshold.

Usage

Obtain or synthesise an image.Image containing the pixelated region, construct an Engine with New, and call Run. Import the defaults package for its side-effect to wire the standard renderer, pixelator, metric, and search strategy; without it the components must be supplied explicitly in Config.

import _ "github.com/oioio-space/unpixel/defaults" // wire default components

eng, err := unpixel.New(img, unpixel.Config{})
if err != nil { ... }
progCh, resCh := eng.Run(ctx)
unpixel.OnProgress(progCh, func(p unpixel.Progress) {
    fmt.Println(p.BestGuess)
})
for r := range resCh {
    fmt.Println(r.BestGuess, r.BestScore)
}

UnPixel is a faithful Go port of Bishop Fox's unredacter (GPL-3.0).

Example

Example demonstrates the quick-start usage of UnPixel: construct an Engine from a pixelated image, run the search, consume progress events, then collect results. The image here is a small synthetic white rectangle used purely to keep the example self-contained and fast; in practice, supply the actual pixelated screenshot region.

package main

import (
	"context"
	"fmt"
	"image"
	"image/color"

	"github.com/oioio-space/unpixel"
	_ "github.com/oioio-space/unpixel/defaults"
)

func main() {
	// Build a tiny synthetic image to stand in for a real pixelated screenshot.
	img := image.NewRGBA(image.Rect(0, 0, 64, 16))
	for y := range img.Bounds().Dy() {
		for x := range img.Bounds().Dx() {
			img.SetRGBA(x, y, color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff})
		}
	}

	eng, err := unpixel.New(img, unpixel.Config{
		Charset:   "ab ",
		MaxLength: 2,
		BlockSize: unpixel.DefaultBlockSize,
	})
	if err != nil {
		fmt.Println("new:", err)
		return
	}

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	progCh, resCh := eng.Run(ctx)

	// Consume progress events concurrently while collecting results below.
	// OnProgress blocks until the progress channel is closed (EventDone).
	done := make(chan struct{})
	go func() {
		defer close(done)
		unpixel.OnProgress(progCh, func(p unpixel.Progress) {
			if p.Kind == unpixel.EventNewBest {
				fmt.Println("new best:", p.BestGuess)
			}
		})
	}()

	for r := range resCh {
		if r.Err != nil {
			fmt.Println("result error:", r.Err)
			continue
		}
		fmt.Println("result:", r.BestGuess)
	}
	<-done
}

Index

Examples

Constants

View Source
const (
	// CharsetLower is lowercase ASCII letters plus space (the faithful default).
	CharsetLower = DefaultCharset
	// CharsetAlnum adds uppercase letters and digits to CharsetLower.
	CharsetAlnum = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 "
	// CharsetASCII is every printable ASCII character (0x20–0x7E). Use it for
	// source code and other symbol-heavy text.
	CharsetASCII = " !\"#$%&'()*+,-./0123456789:;<=>?@" +
		"ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
)

Candidate-alphabet presets for Config.Charset (or WithCharset). A wider charset recovers more text but enlarges the search space, so prefer the narrowest one that fits the target.

View Source
const (
	// DefaultCharset is the candidate alphabet: lowercase ASCII letters and space.
	DefaultCharset = "abcdefghijklmnopqrstuvwxyz "
	// DefaultMaxLength is the maximum candidate string length the search explores.
	DefaultMaxLength = 20
	// DefaultBlockSize is the pixelation block side length in pixels.
	DefaultBlockSize = 8
	// DefaultThreshold is the per-block score gate for non-space characters;
	// candidates scoring above this value are pruned.
	DefaultThreshold = 0.25
	// DefaultSpaceThreshold is the per-block score gate for the space character,
	// which is visually ambiguous and requires a more permissive threshold.
	DefaultSpaceThreshold = 0.5
	// DefaultTopN is the default maximum number of ranked candidates retained
	// per grid offset and exposed on Result.TopN.
	DefaultTopN = 5
	// DefaultBeamWidth is the number of candidates retained per depth level by
	// BeamStrategy. Larger values improve recall at the cost of more evaluations.
	DefaultBeamWidth = 16
	// DefaultCacheSize is the maximum number of stageImage results held by
	// CachingScorer. Zero disables caching.
	DefaultCacheSize = 4096
)

Default configuration values, matching the original unredacter reference implementation.

View Source
const RobustSupportThreshold = 0.5

RobustSupportThreshold is the minimum autocorrelation support score (in [0, 1]) that InferBlockSizeRobust considers a detectable periodic block structure. A value of 0.5 means at least half of the boundary signal must align with the dominant period; below this the detector reports no grid.

Variables

View Source
var DefaultBlurStrategy func() Strategy

DefaultBlurStrategy is a hook that returns the default Strategy used by RecoverBlurred when the caller has not supplied one via WithStrategy. It is populated by importing the defaults package for its side-effect.

The default is BeamStrategy: beam search bounds the per-level branching factor to DefaultBeamWidth candidates, giving O(length × width) evaluations regardless of charset size — critical for longer words (≥5 chars) behind blur, where per-character image signal is too weak for guided DFS's unbounded expansion to finish in reasonable time. Callers that need full recall (at higher cost) can override with WithStrategy(defaults.GuidedStrategy()).

View Source
var DefaultComponents func(cfg *Config) error

DefaultComponents is a hook that wires the standard Renderer, Pixelator, Metric, and Strategy into cfg for any fields that are still nil. It is populated by importing the defaults package for its side-effect, which avoids an import cycle between the root package and its internal implementations. Applications that supply all component fields explicitly need not set this.

View Source
var ErrNilImage = errors.New("redacted image must not be nil")

ErrNilImage is returned by New when a nil image is passed.

View Source
var ErrNoComponents = errors.New("unpixel: missing component and no DefaultComponents wired; " +
	"import github.com/oioio-space/unpixel/defaults or set the component via options")

ErrNoComponents is returned by the Recover helpers when the Config leaves a component (Renderer, Pixelator, Metric, or Strategy) nil and the defaults package has not been imported to wire them.

Functions

func InferBlockSize added in v0.2.0

func InferBlockSize(img image.Image) int

InferBlockSize estimates the pixelation block size, in pixels, of a mosaic- redacted image by detecting the spacing of its block grid. It scans for the columns and rows where the colour changes — the boundaries between constant- colour blocks — and returns the greatest common divisor of the gaps between them, which equals the block side length even when some adjacent blocks happen to share a colour (their gap is then a multiple of the block size).

It returns 0 when the grid cannot be determined — a uniform image, an image smaller than 2×2, or a non-pixelated image whose gaps reduce to 1. New uses it to fill a non-positive Config.BlockSize, falling back to DefaultBlockSize when inference returns 0.

func InferBlockSizeRobust added in v0.9.0

func InferBlockSizeRobust(img image.Image) (blockSize int, support float64)

InferBlockSizeRobust detects the mosaic block size using autocorrelation of the boundary signal, tolerating noisy or anti-aliased block edges caused by resampling, JPEG compression, or screenshot scaling.

It builds a binary boundary signal (1 at each column/row where the colour changes appreciably, 0 elsewhere), computes its autocorrelation, and finds the dominant period in the range [minRobustPeriod, maxRobustPeriod]. The support score in [0, 1] reflects how consistently boundaries align with that period: 1.0 means every boundary falls exactly on a grid line; values above RobustSupportThreshold indicate a detectable periodic structure.

When the exact-GCD result from InferBlockSize is available and its implied autocorrelation support exceeds the autocorrelation peak, the exact result wins (preserving byte-identical behaviour for clean mosaics). When the image is too small, uniform, or has no detectable period, it returns (0, 0).

func InferBlurSigma added in v0.4.0

func InferBlurSigma(img image.Image) float64

InferBlurSigma estimates the Gaussian-blur standard deviation (in pixels) of a blurred image using an edge-spread formula with a density-adaptive gradient percentile. The approach is purely spatial — no FFT — and is calibrated for both sparse step-edge inputs (large images, small σ) and dense text images (small images, large σ) (Polyblur / Chen & Ma gradient-ratio insight, Hill 2016 redaction paper).

Method

For a Gaussian-blurred step edge, the peak gradient is contrast/(σ·√(2π)). Inverting: σ = contrast/(gPeak·√(2π)). The challenge is robustly estimating gPeak from the gradient distribution:

  • A fixed high percentile (e.g. 99th) over-selects for dense-edge images (text) but under-selects for sparse-edge images (single step in a large field), landing on the gradient shoulder rather than the peak.
  • A fixed very-high percentile (e.g. 99.9th) works for sparse edges but over-selects for dense-edge text images, picking noise outliers.

The solution: measure the edge density (fraction of gradient pairs above 5% of contrast), and adaptively choose the percentile as:

pct = clamp(1 − edgeFrac × 0.05, 0.95, 0.999)

This keeps the selected gradient within the true-peak region of the edge distribution regardless of whether edges cover 1% (large step) or 20% (dense text) of the image area. The constant 0.05 is calibrated to within ±35% accuracy for σ ∈ [1,8] on both step-edge and rendered-text inputs.

It returns 0 when the image is too small or essentially flat (contrast < 8). A returned value near 0 means the image is sharp (probably not blurred); a larger value is the estimated blur radius. The estimate is a starting point for the σ-sweep in RecoverBlurred — accuracy within ±35% is sufficient.

func InferDarkBackground added in v0.3.0

func InferDarkBackground(img image.Image) bool

InferDarkBackground reports whether img looks like dark text/content on a dark background (e.g. a dark-mode screenshot), judged from its border pixels. New uses it to decide whether to invert the image so it matches the dark-on-light rendering pipeline.

func InferFontSize added in v0.4.0

func InferFontSize(img image.Image) float64

InferFontSize estimates the point size of the text in img from its content height, for zero-config calibration: rendered text spans about 0.92×fontSize vertically (ascenders to descenders), so fontSize ≈ contentHeight / 0.92. "Content" is any row that differs from the image's background colour.

It returns 0 when no content stands out. The estimate is a ballpark — a short word without ascenders/descenders, or blur spreading the glyphs, skews it — so it is best used to seed the search (optionally with a small size sweep) rather than as an exact value. Measure it on the redacted region (see LocateRedaction) or on a screenshot's sharp reference text, whichever is cleaner.

func InferImpulseNoise added in v0.7.0

func InferImpulseNoise(img image.Image) float64

InferImpulseNoise estimates the fraction [0, 1] of impulse-corrupted pixels in img. It is a cheap sampled estimate (O(samples), ≤ 50 000 pixels checked) used by blind.Recover to decide whether to apply a median pre-filter automatically before block-size detection and decoding.

Method (RVIN / local-extremum test from the SAPN literature): for each sampled interior pixel p, collect the BT.601 luminance of its 8 neighbours. Pixel p is counted as an impulse if both of the following hold:

  1. |lum(p) – median8| > impulseNoiseThreshold (large deviation from context).
  2. lum(p) is the strict minimum OR strict maximum among the 9 values in the 3×3 window (isolated spike, not part of a ramp).

Condition 2 rejects smooth gradients and structured block edges — an edge pixel sits between two blocks whose colours straddle its own, so it is never the strict extremum of its 3×3 neighbourhood.

Returns impulse_count / samples_checked. An empty or sub-3×3 image returns 0.

func LocateRedaction added in v0.4.0

func LocateRedaction(img image.Image) (image.Rectangle, bool)

LocateRedaction returns the bounding box of a blurred region inside img — for zero-config recovery of a redaction embedded in a larger screenshot. A blurred edge has a bounded peak gradient (≈ contrast/(σ·√(2π)), far below sharp text), so a row/column is "blurred content" when it carries contrast yet its peak luminance gradient stays low. The result is the tight box of the largest such band; ok is false when no blurred region stands out (e.g. an all-sharp image).

It is the missing piece for screenshots: estimating sigma or recovering on the whole image is skewed by sharp surrounding text (on the Bishop Fox challenge, whole-image σ≈0.6 vs ≈5.6 on the blurred line), so crop to this box first.

func OnProgress

func OnProgress(ch <-chan Progress, fn func(Progress))

OnProgress drains the progress channel returned by Engine.Run, calling fn for every event in order. It blocks until the channel is closed (i.e. until EventDone has been delivered and consumed). It is a convenience wrapper around a range loop and does not start a goroutine.

Types

type BlockGrid added in v0.6.0

type BlockGrid struct {
	// Size is the block side length in pixels, equal to InferBlockSize.
	Size int
	// PhaseX is the horizontal grid origin offset: block boundaries fall at
	// column positions x ≡ PhaseX (mod Size).
	PhaseX int
	// PhaseY is the vertical grid origin offset: block boundaries fall at
	// row positions y ≡ PhaseY (mod Size).
	PhaseY int
	// Confidence is the fraction of detected boundary positions whose residue
	// (mod Size) matches the modal residue, averaged across X and Y. It is in
	// [0, 1]: 1.0 for a perfectly axis-aligned mosaic; near 0 for a rotated or
	// irregular image.
	Confidence float64
}

BlockGrid describes a detected axis-aligned mosaic lattice.

A mosaic image has piecewise-constant blocks of size Size×Size pixels. The block boundaries fall at positions x ≡ PhaseX (mod Size) in X and y ≡ PhaseY (mod Size) in Y. Confidence measures how consistently the detected colour-change positions agree with this lattice.

func InferBlockGrid added in v0.6.0

func InferBlockGrid(img image.Image) (BlockGrid, bool)

InferBlockGrid detects the mosaic lattice — block size and grid origin phase — of a pixelated image. It returns the detected BlockGrid and ok=true when a regular axis-aligned grid is found, or the zero BlockGrid and ok=false when the image is too small, uniform, or has no regular grid (same cases where InferBlockSize returns 0).

The block size is derived from the GCD of the gaps between colour-change columns (and rows). PhaseX is the most common residue of those column positions modulo Size; PhaseY likewise for rows. Confidence is the fraction of boundary positions consistent with that modal residue, averaged over X and Y axes.

type Config

type Config struct {
	// Renderer is the component that rasterises candidate strings to RGBA.
	// Defaults to the XImage renderer (wired by the defaults package).
	Renderer Renderer
	// Pixelator is the component that applies block-average pixelation.
	// Defaults to BlockAverage (wired by the defaults package).
	Pixelator Pixelator
	// Metric is the component that measures pixel-level image distance.
	// Defaults to Pixelmatch (wired by the defaults package).
	Metric Metric
	// Strategy is the component that drives the search algorithm.
	// Defaults to GuidedDFS (wired by the defaults package).
	Strategy Strategy

	// LanguageModel optionally scores a candidate string's linguistic
	// plausibility (higher = more plausible). When set, the final ranking breaks
	// ties between candidates of equal image distance toward plausible text — the
	// image cannot separate visually near-identical candidates (especially behind
	// heavy blur), but a language prior can. nil disables it (the default).
	LanguageModel func(string) float64

	// ThresholdFor returns the acceptance threshold for a given candidate
	// character. It defaults to a closure that returns SpaceThreshold for ' '
	// and Threshold for all other runes. Override to apply per-class thresholds
	// without modifying search logic.
	ThresholdFor func(rune) float64

	// Charset is the ordered set of candidate characters tried at each search
	// depth. Defaults to DefaultCharset (a–z plus space).
	Charset string

	// CharsetTopK, when > 0 and LanguageModel is set, prunes the per-position
	// charset to the K most-likely next characters (by the language prior) before
	// evaluating any — turning a wide charset (e.g. full ASCII) into K renders per
	// position. It trades a little recall (the true char must be in the top K) for
	// speed; 0 (the default) evaluates the whole charset and never loses recall.
	CharsetTopK int

	// Style controls the font size, weight, and padding used when rendering
	// candidates. Zero fields use the design defaults (32 pt, 8 px padding).
	Style Style

	// MaxLength is the maximum number of characters the search will attempt
	// before backtracking. Defaults to DefaultMaxLength.
	MaxLength int
	// BlockSize is the side length in pixels of each pixelation block. It must
	// match the block size used to produce the redacted image. When left
	// non-positive, New auto-detects it from the image via InferBlockSize,
	// falling back to DefaultBlockSize if the grid cannot be determined.
	BlockSize int
	// Threshold is the per-block score below which a non-space candidate is
	// accepted and the search descends deeper. Lower values are stricter.
	// Defaults to DefaultThreshold.
	Threshold float64
	// SpaceThreshold is the acceptance threshold applied specifically to the
	// space character, which is harder to distinguish visually. Defaults to
	// DefaultSpaceThreshold.
	SpaceThreshold float64
	// TopN is the maximum number of ranked candidates retained per grid offset
	// in Result.TopN. Candidates are sorted ascending by score (lowest first);
	// ties are broken by Guess string so results are deterministic. Zero or
	// negative values are replaced by DefaultTopN in applyDefaults.
	TopN int

	// BeamWidth is the maximum number of candidates retained per depth level
	// when using BeamStrategy. Values <= 0 are replaced by DefaultBeamWidth.
	// Only BeamStrategy reads this field; other strategies ignore it.
	BeamWidth int
	// CacheSize is the maximum number of stageImage results held by
	// CachingScorer. Zero disables prefix-render memoization; positive values
	// enable an LRU cache of that capacity shared across all Eval calls for a
	// single Search invocation. Defaults to DefaultCacheSize.
	CacheSize int
	// Workers is the maximum number of grid offsets probed and searched
	// concurrently. Values <= 0 default to runtime.GOMAXPROCS. Set to 1 to force
	// fully sequential execution. Results are merged deterministically regardless
	// of Workers, so this affects throughput only, never the output.
	Workers int
	// contains filtered or unexported fields
}

Config holds all tunable parameters for an Engine. Every field is optional: zero values are replaced by the package defaults (see the Default* constants) when New is called. Component fields (Renderer, Pixelator, Metric, Strategy) are filled by importing the defaults package for its side-effect, or can be supplied directly for custom implementations.

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine orchestrates the full unpixel pipeline: grid-origin discovery, candidate rendering, re-pixelation, distance measurement, and guided search. Create one with New and call Run to start the search.

func New

func New(redacted image.Image, cfg Config) (*Engine, error)

New creates an Engine for the given pixelated image. Scalar zero values in cfg are replaced by package defaults; nil component fields are left for Run to fill via DefaultComponents (or must be set explicitly). New returns ErrNilImage if redacted is nil.

func (*Engine) Config added in v0.2.0

func (e *Engine) Config() Config

Config returns the engine's resolved configuration, with scalar defaults applied and BlockSize auto-inferred. Component fields (Renderer, Pixelator, Metric, Strategy) are non-nil only after Run has wired them. It is intended for introspection — for example, reading the inferred BlockSize.

func (*Engine) DeskewedImage added in v0.6.0

func (e *Engine) DeskewedImage() *image.RGBA

DeskewedImage returns the internal RGBA image used by the Engine — after any dark-background inversion and deskew rotation performed by New. When no deskew was applied it is the same pixel data that was passed to New (after optional inversion); when a deskew was applied it is the rotated version. The returned pointer is the live internal buffer; callers must not modify it.

func (*Engine) Run

func (e *Engine) Run(ctx context.Context) (<-chan Progress, <-chan Result)

Run starts the search and returns a progress channel and a results channel. The progress channel carries Progress events and is closed after EventDone is delivered. The results channel receives one Result per surviving grid offset and is closed once all offsets have been searched. Cancelling ctx causes both channels to be closed promptly after the in-flight search step completes.

If any component field in cfg is nil, Run invokes DefaultComponents to fill it before starting. If DefaultComponents is also nil, Run panics with a descriptive message directing the caller to import the defaults package.

func (*Engine) SkewInfo added in v0.6.0

func (e *Engine) SkewInfo() SkewInfo

SkewInfo returns the skew-detection outcome recorded when the Engine was created by New. The zero SkewInfo (Detected=false, Applied=false) is returned when the image was axis-aligned or too small to analyse.

type Eval

type Eval struct {
	// GuessImage is a cropped, pixelated rendering of Guess, populated only
	// when the caller opts in via the Strategy implementation.
	GuessImage *image.RGBA
	// Guess is the candidate string evaluated at this node.
	Guess string
	// Score is the marginal region diff score for the most recently added
	// character (lower is better; compared against the threshold).
	Score float64
	// TooBig is true when the rendered candidate is wider than the redacted
	// image; such candidates are pruned regardless of score.
	TooBig bool
}

Eval holds the result of evaluating a single candidate string at one search node.

func (Eval) String added in v0.3.0

func (e Eval) String() string

String returns the candidate and its marginal score, e.g. `"hello" (0.0123)`.

type EventKind

type EventKind int

EventKind identifies the type of a Progress event delivered on the channel returned by Engine.Run.

const (
	// EventCandidate is emitted for every candidate string evaluated during
	// the search. It is high-frequency; the channel is non-blocking and events
	// are dropped when the buffer is full.
	EventCandidate EventKind = iota
	// EventOffsetProbed is emitted after each grid-origin probe during the
	// offset-discovery phase. It is high-frequency with the same drop semantics
	// as EventCandidate.
	EventOffsetProbed
	// EventNewBest is emitted whenever the search finds a new overall best
	// guess. Delivery is guaranteed: the sender blocks (with context) until the
	// consumer reads the event.
	EventNewBest
	// EventDone is the terminal event, delivered exactly once after all offsets
	// have been searched or the context is cancelled. Delivery is guaranteed.
	EventDone
)

type FontResult added in v0.3.0

type FontResult struct {
	// Result is the recovery produced with this renderer.
	Result Result
	// Index is the position of the renderer in the slice passed to
	// RecoverMultiFont, identifying which font produced this Result.
	Index int
}

FontResult pairs one candidate renderer with the Result it produced, as returned by RecoverMultiFont.

func RecoverMultiFont added in v0.3.0

func RecoverMultiFont(ctx context.Context, redacted image.Image, renderers []Renderer, opts ...Option) ([]FontResult, error)

RecoverMultiFont runs Recover once per renderer and returns the results ranked best-fit first, so the caller can recover a redaction without knowing its exact typeface — supply several candidate fonts and let the tool pick.

Ranking is by Result.BestTotal, the whole-image distance of the best guess: unlike BestScore (a per-character marginal score that ties at ~0 across fonts), BestTotal is comparable between separate runs, so the lowest-BestTotal font is the one that reconstructs the redaction most faithfully. ret[0] is the best fit. A renderer with an empty recovery is ranked last; a renderer whose run errors is omitted. RecoverMultiFont returns an error only when redacted is nil, renderers is empty, or every renderer failed.

The runs execute in parallel within a core budget — the resolved Config.Workers (set via WithWorkers or WithConfig), or runtime.GOMAXPROCS when unset: min(len(renderers), budget) run concurrently, each search using an equal share of workers, so the two layers never oversubscribe the CPU. The per-run renderer and worker count are set internally, overriding any WithRenderer/WithWorkers already in opts.

Build the renderers with github.com/oioio-space/unpixel/defaults.RendererFromFonts:

var rs []unpixel.Renderer
for _, data := range fontData {
    r, err := defaults.RendererFromFonts(data, nil)
    if err != nil { return err }
    rs = append(rs, r)
}
ranked, err := unpixel.RecoverMultiFont(ctx, img, rs, unpixel.WithBlockSize(5))
best := ranked[0]

type Metric

type Metric interface {
	Compare(a, b *image.RGBA) float64
}

Metric measures the visual distance between two same-sized RGBA images. Compare returns a value in [0, 1] where 0 means pixel-perfect identical.

type Offset

type Offset struct {
	// X is the horizontal grid origin in pixels.
	X int
	// Y is the vertical grid origin in pixels.
	Y int
	// Score is the image-distance score for this origin (lower is better).
	Score float64
}

Offset represents one candidate grid origin for the pixelation block alignment. The search probes multiple (X, Y) origins and ranks them by Score before committing to the full character-level search.

type Option added in v0.3.0

type Option func(*Config)

Option configures a Config for the high-level Recover helpers. Options are applied in order, so a later option overrides an earlier one; WithConfig (which replaces the whole Config) is therefore best passed first.

func WithBeamWidth added in v0.8.0

func WithBeamWidth(width int) Option

WithBeamWidth sets Config.BeamWidth (candidates retained per depth level by BeamStrategy). Values ≤ 0 defer to DefaultBeamWidth. Only BeamStrategy reads this field; other strategies ignore it.

func WithBlockSize added in v0.3.0

func WithBlockSize(n int) Option

WithBlockSize sets Config.BlockSize. A non-positive value lets New auto-detect the block size from the image (see InferBlockSize).

func WithCharset added in v0.3.0

func WithCharset(charset string) Option

WithCharset sets Config.Charset (the ordered candidate alphabet).

func WithCharsetTopK added in v0.5.0

func WithCharsetTopK(k int) Option

WithCharsetTopK sets Config.CharsetTopK: with a LanguageModel set, evaluate only the k most-likely next characters per position (k<=0 disables pruning).

func WithConfig added in v0.3.0

func WithConfig(cfg Config) Option

WithConfig uses cfg as the starting point before any other options are applied. Pass it first to layer further options on top of a full Config.

func WithL0Deblur added in v0.12.0

func WithL0Deblur(fns ...func(*deblur.L0Options)) Option

WithL0Deblur enables L0-regularised text deblurring (Pan et al., CVPR 2014) as an opt-in preprocessing step before blur recovery. It is only effective in RecoverBlurred; Recover and the mosaic path are unchanged.

When called with no arguments the defaults from the paper are used:

  • Lambda: 2×10⁻³ (gradient L0 sparsity weight)
  • Mu: 5×10⁻⁴ (two-tone intensity prior weight)
  • Iterations: 20 (outer HQS alternating-minimisation steps)

Pass functional options to override individual parameters, e.g.:

unpixel.WithL0Deblur(
    func(o *deblur.L0Options) { o.Iterations = 30 },
)

The sigma used for the PSF is the σ estimated by InferBlurSigma at the time RecoverBlurred is called (before the σ-search), so it does not need to be set by the caller. Result.L0Deblurred is set to true when active.

func WithLanguageModel added in v0.4.0

func WithLanguageModel(score func(string) float64) Option

WithLanguageModel sets Config.LanguageModel, a plausibility scorer used to break ties between candidates of equal image distance toward plausible text.

func WithMaxLength added in v0.3.0

func WithMaxLength(n int) Option

WithMaxLength sets Config.MaxLength (the maximum candidate length).

func WithMetric added in v0.3.0

func WithMetric(m Metric) Option

WithMetric sets Config.Metric (the image-distance metric).

func WithNormalize added in v0.9.0

func WithNormalize(fns ...func(*deblur.Options)) Option

WithNormalize enables input normalisation before blur recovery. It is opt-in and only affects RecoverBlurred; Recover and the mosaic path are unchanged.

When called with no arguments, sensible defaults are used:

  • BgDivide: divide out a multiplicative illumination estimate (vignette).
  • InvertAuto: invert when the image is predominantly dark (dark-theme support).
  • Stretch: false — enable a 1st/99th-percentile contrast stretch with o.Stretch = true.
  • Deblock: 0 (off) — set to -1 (auto) or a positive radius to median-filter JPEG blocking.
  • Binarize: false.

Pass deblur.Options values to customise individual steps, e.g.:

unpixel.WithNormalize(func(o *deblur.Options) { o.Deblock = 2 })

Result.Normalized is set to true when normalisation was applied.

func WithPixelator added in v0.4.0

func WithPixelator(p Pixelator) Option

WithPixelator sets Config.Pixelator (the redaction operator the search reproduces). Use it with defaults.GaussianBlur(sigma) and WithBlockSize(1) to recover blurred text instead of mosaic pixelation.

func WithPriors added in v0.5.0

func WithPriors(priors ...func(string) float64) Option

WithPriors composes one or more plausibility priors into Config.LanguageModel. Each prior is a function from candidate string to a non-negative log-space score (higher = more plausible). Nil priors are silently skipped.

The resulting LanguageModel is the sum of all provided priors AND any LanguageModel already set on the Config, so WithPriors composes correctly regardless of option order:

unpixel.Recover(ctx, img,
    unpixel.WithLanguageModel(myBigram),   // set first
    unpixel.WithPriors(secrets.Prior),     // adds on top
)

The combined prior is used in two places:

  • RankFinal breaks ties between candidates whose whole-image scores are within lmTieBand (0.01) of each other, preferring higher-prior strings.
  • WithCharsetTopK prunes the per-position charset to the K most-likely next characters before evaluating any candidates, trading a little recall for speed on wide charsets.

func WithRemosaic added in v0.10.0

func WithRemosaic() Option

WithRemosaic enables the Hill–Zhou–Saul–Shacham PETS-2016 §4 remosaic path inside RecoverBlurred. When active the forward operator becomes:

render(candidate) → GaussianBlur(σ) → BlockAverage(b)

and the target image is pre-mosaiced by BlockAverage(b) once before the search. The mosaic stage collapses small pixel-level differences caused by σ-mismatch or JPEG compression noise, making the comparison more robust than plain GaussianBlur alone.

The grid size b is chosen automatically as max(2, round(σ)); use WithRemosaicGrid to override it. The linear-light block average is used when WithRemosaicLinear is passed instead of WithRemosaic; by default sRGB means are used (matching the plain mosaic path).

WithRemosaic has no effect on Recover or the mosaic path.

func WithRemosaicGrid added in v0.10.0

func WithRemosaicGrid(b int) Option

WithRemosaicGrid enables the remosaic path (like WithRemosaic) and additionally pins the block grid size to b pixels. b ≤ 0 selects auto (b = max(2, round(σ)), same as WithRemosaic).

func WithRemosaicLinear added in v0.10.0

func WithRemosaicLinear() Option

WithRemosaicLinear enables the remosaic path and uses linear-light block averaging instead of the default sRGB means. Use it when the target was redacted by a GEGL/GIMP-based tool that computes its block average in linear light.

func WithRenderer added in v0.3.0

func WithRenderer(r Renderer) Option

WithRenderer sets Config.Renderer (the text rasteriser). Use it with a custom font, e.g. defaults.RendererFromFonts, to match the typeface of the redacted image.

func WithStrategy added in v0.3.0

func WithStrategy(s Strategy) Option

WithStrategy sets Config.Strategy (the search algorithm).

func WithStyle added in v0.3.0

func WithStyle(s Style) Option

WithStyle sets Config.Style (font size, weight, and padding). It must match the style of the redacted text for recovery to succeed.

func WithThreshold added in v0.3.0

func WithThreshold(t float64) Option

WithThreshold sets Config.Threshold (the per-block acceptance gate).

func WithTopN added in v0.3.0

func WithTopN(n int) Option

WithTopN sets Config.TopN (ranked candidates retained per offset).

func WithWorkers added in v0.3.0

func WithWorkers(n int) Option

WithWorkers sets Config.Workers (max grid offsets searched concurrently; non-positive means runtime.GOMAXPROCS).

type Pixelator

type Pixelator interface {
	Pixelate(img *image.RGBA, originX, originY int) *image.RGBA
}

Pixelator replaces every blockSize×blockSize region of an image with the mean RGBA colour of that region. The originX and originY parameters align the grid so that it matches the original pixelation applied to the source.

type Progress

type Progress struct {
	// Err carries any terminal error that caused the search to abort.
	Err error

	// PreviewImage is an optional deep copy of the pixelated guess image.
	// It is non-nil only when the Strategy implementation populates it.
	PreviewImage *image.RGBA

	// BestGuess is the candidate string with the lowest score found so far
	// across all offsets.
	BestGuess string

	// Guess is the candidate string that triggered this specific event.
	Guess string
	// Offset is the grid origin currently being searched.
	Offset Offset
	// Kind identifies which event this Progress value represents.
	Kind EventKind

	// BestScore is the total image-distance score of BestGuess (lower is better).
	BestScore float64

	// Score is the image-distance score of Guess.
	Score float64

	// Depth is the current search depth, equal to the character length of Guess.
	Depth int
	// Evaluated is the cumulative number of candidates evaluated since Run started.
	Evaluated int
	// OffsetsDone is the number of grid offsets fully searched so far.
	OffsetsDone int
	// OffsetsTotal is the total number of surviving grid offsets to be searched.
	OffsetsTotal int

	// Elapsed is the wall-clock duration since Engine.Run was called.
	Elapsed time.Duration
	// Done is true only on the EventDone event.
	Done bool
}

Progress carries a snapshot of search state, streamed on the channel returned by Engine.Run. Consumers should switch on Kind to decide which fields are meaningful for a given event.

type Renderer

type Renderer interface {
	Render(text string, style Style) (img *image.RGBA, sentinelX int, err error)
}

Renderer renders candidate text to an RGBA image, placing a blue sentinel block immediately to the right of the text. It returns the image and the x-coordinate of that sentinel, which marks the text's right edge in pixels.

type Result

type Result struct {
	// Err is non-nil if the search for this offset aborted due to an error.
	Err error
	// BestGuess is the candidate string with the lowest overall score found
	// during the search rooted at this offset.
	BestGuess string
	// Candidates holds every string that passed the threshold gate during the
	// search, in discovery order.
	Candidates []Eval
	// TopN holds the best candidates sorted ascending by score (lowest score
	// first), with ties broken by Guess string for determinism. Its length is
	// at most Config.TopN. TopN[0] is the same candidate as BestGuess when the
	// search produces any result. TopN is nil when no candidates were found.
	TopN []Eval
	// Offset is the grid origin that was searched to produce this result.
	Offset Offset
	// BestScore is the marginal-region diff score of BestGuess (lower is better).
	BestScore float64
	// BestTotal is the whole-image distance of BestGuess against the redaction
	// (0 = pixel-perfect, 1 = worst). Unlike BestScore — which is a per-character
	// marginal score that a correct prefix or a coincidental match can drive to
	// ~0 — BestTotal measures how well the full guess explains the entire
	// redaction, so it is comparable across separate runs. That makes it the
	// right signal for choosing between candidate fonts or styles: the run with
	// the lowest BestTotal recovered the redaction most faithfully.
	BestTotal float64
	// Confidence is 1 − TopN[0].Score, giving a value in [0, 1] where 1
	// represents a pixel-perfect match. It is 0 when TopN is empty.
	Confidence float64
	// Ambiguity is TopN[1].Score − TopN[0].Score: the score gap between the
	// best and second-best candidates. A larger gap means the best guess is
	// more clearly distinguished from alternatives. It is 0 when len(TopN) < 2.
	Ambiguity float64
	// BlurSigma is the Gaussian-blur standard deviation (in pixels) used to
	// produce this result. It is set by RecoverBlurred and zero for all other
	// recovery paths (block-mosaic, manual Pixelator, etc.).
	BlurSigma float64
	// BelowThreshold is true when BestGuess was promoted from the best-seen
	// sub-threshold candidate because no candidate actually passed the
	// acceptance gate. When false (the normal case), BestGuess/TopN/Confidence
	// are computed entirely from candidates that passed the gate and are
	// byte-identical to previous behaviour.
	//
	// Per-offset edge case: when the global winner is above-threshold
	// (BelowThreshold=false), a non-winning offset's per-offset TopN may still
	// contain a sub-threshold best-seen candidate. Callers iterating all
	// per-offset results should not treat every per-offset TopN entry as
	// having passed the acceptance gate; check the per-offset BelowThreshold
	// field individually.
	BelowThreshold bool
	// Normalized is true when RecoverBlurred applied input normalisation
	// (via WithNormalize) before the σ-search. It is always false for other
	// recovery paths (Recover, mosaic, etc.) and when WithNormalize was not
	// passed. Use it to confirm the normalisation step was active.
	Normalized bool
	// L0Deblurred is true when RecoverBlurred applied L0-regularised text
	// deblurring (via WithL0Deblur) as a preprocessing step before the σ-search.
	// Always false for the mosaic path and when WithL0Deblur was not passed.
	L0Deblurred bool
}

Result is the final output produced by Engine.Run for one surviving grid offset. One Result is sent per offset on the results channel returned by Run.

func Recover added in v0.3.0

func Recover(ctx context.Context, redacted image.Image, opts ...Option) (Result, error)

Recover is the one-call entry point: it runs the full search on the redacted image and returns the single best Result across all grid offsets. It builds a Config from opts, auto-detects what it can (e.g. the block size), wires the default components (when the defaults package is imported), runs the search, and drains the channels for the caller.

The common usage is a side-effect import of the defaults package:

import _ "github.com/oioio-space/unpixel/defaults"
res, err := unpixel.Recover(ctx, img)
fmt.Println(res.BestGuess)

Recover returns ErrNilImage for a nil image and ErrNoComponents when a component is missing and no defaults are wired. A search that finds nothing returns the zero Result and a nil error.

func RecoverBlurred added in v0.8.0

func RecoverBlurred(ctx context.Context, img image.Image, opts ...Option) (Result, error)

RecoverBlurred recovers text from a Gaussian-blurred redaction without being told the blur standard deviation σ. It is the zero-config counterpart of Recover with a manual WithPixelator(GaussianBlur(σ)) + WithBlockSize(1).

Default search strategy: beam search

RecoverBlurred defaults to beam search (BeamStrategy) rather than the guided DFS used by Recover. Behind blur the per-character image signal is weak: the correct character's score often beats the wrong one by only a small margin, and guided DFS expands every passing branch depth-first — producing O(|charset|^length) evaluations for longer words, which makes it impractical for ≥5-character targets. Beam search bounds work to O(length × BeamWidth) evaluations, making even 7-character recoveries finish in milliseconds.

The effective BeamWidth for blur recovery is 32 (set by defaults.DefaultBlurStrategy); DefaultBeamWidth = 16 is only the generic BeamStrategy fallback. Either value is wider than the typical blur charset, giving full per-level coverage for small alphabets. Combine with WithLanguageModel (e.g. WithLanguageModel(lang.PriorFor(lang.English))) for disambiguation when the image signal alone does not separate candidates.

Callers that need full guided-DFS recall can override with WithStrategy(defaults.GuidedStrategy()), which also disables the beam default.

Algorithm (adaptive σ-search)

Gaussian blur is a known, deterministic forward operator B_σ. The generate-and-test attack (render → blur → compare) inverts it: render a candidate, blur with the same σ, measure image distance — the true text will have the lowest distance.

RecoverBlurred uses an adaptive strategy that minimises the number of full Recover calls to the common case of ONE:

  1. Estimate σ₀ = InferBlurSigma(img). The estimator is accurate to within ~15% on typical text images (validated over the fixture set).
  2. Run ONE full Recover at σ₀ with exact GaussianBlur.
  3. If Result.BestGuess is non-empty and Result.BestTotal < blurAcceptThreshold the early-accept gate fires: σ₀ is close enough and the candidate is convincing — return immediately (the common case, ≈1× single Recover).
  4. Otherwise, expand to a bounded fallback sweep: build a coarse grid of ±2 steps around σ₀ (4 extra σ candidates at multipliers {0.7, 0.85, 1.18, 1.4}×σ₀, or a fixed wide grid when σ₀=0), run them in parallel, then fine-refine (±0.25, 5 steps, sequential) around the best coarse winner. This fallback adds ≤9 extra Recover calls — preserving robustness for the minority of images where σ₀ is off by more than ~15%.

Accept threshold (blurAcceptThreshold)

BestTotal is the whole-image pixel distance (in [0,1]) of the best guess against the redaction. For a correct recovery at near-exact σ, BestTotal is typically 0.01–0.04 (pixel-perfect near-match). An incorrect recovery or a σ mismatch produces BestTotal ≫ 0.08. The threshold 0.07 sits comfortably above the correct-recovery floor and below the wrong-σ ceiling, giving a robust early-exit signal without a recall risk.

Concurrency model

The initial single-σ search uses full Workers parallelism (same as a plain Recover call). The fallback sweep uses the same bounded-pool strategy as the old implementation: each σ gets 1 inner worker; up to NumCPU run at once.

All caller-supplied opts (WithCharset, WithMaxLength, WithStyle, WithLanguageModel, WithWorkers, WithStrategy, …) are forwarded to every inner Recover call; WithBlockSize and WithPixelator are set internally and override any value the caller passes.

Ctx cancellation is respected; a cancelled sweep returns the best result found so far.

func RecoverBlurredFile added in v0.8.2

func RecoverBlurredFile(ctx context.Context, path string, opts ...Option) (Result, error)

RecoverBlurredFile opens the image at path and calls RecoverBlurredReader. Pass "-" handling at the call site if stdin support is needed; RecoverBlurredFile always opens a file.

func RecoverBlurredReader added in v0.8.2

func RecoverBlurredReader(ctx context.Context, r io.Reader, opts ...Option) (Result, error)

RecoverBlurredReader decodes an image (PNG is registered; register other formats by importing their image/<fmt> package) from r and calls RecoverBlurred.

func RecoverFile added in v0.3.0

func RecoverFile(ctx context.Context, path string, opts ...Option) (Result, error)

RecoverFile opens the image at path and calls RecoverReader. Pass "-" handling at the call site if stdin support is needed; RecoverFile always opens a file.

func RecoverReader added in v0.3.0

func RecoverReader(ctx context.Context, r io.Reader, opts ...Option) (Result, error)

RecoverReader decodes an image (PNG is registered; register other formats by importing their image/<fmt> package) from r and calls Recover.

func (Result) Fidelity added in v0.5.0

func (r Result) Fidelity() float64

Fidelity reports how well BestGuess reproduces the whole redaction, in [0, 1] (1 = pixel-perfect), as 1 − BestTotal. It is the honest confidence signal: unlike Confidence (derived from the marginal score, which a prefix or fluke drives to ~1), Fidelity reflects the whole-image match, so a low value means "this recovery is probably wrong / unrecoverable". It is 0 for an empty guess.

func (Result) String added in v0.3.0

func (r Result) String() string

String returns a one-line human-readable summary of the result: the best guess with its score and confidence, or an error / "no result" note.

type SkewInfo added in v0.6.0

type SkewInfo struct {
	// Detected is true when the image appeared to have a non-axis-aligned mosaic
	// grid (InferBlockGrid confidence was low on the raw image).
	Detected bool
	// Applied is true when a deskew rotation was actually applied to the image
	// inside New — meaning the block-homogeneity gain exceeded deskewHomogMinGain
	// and the post-rotation score exceeded deskewHomogMinScore.
	Applied bool
	// AngleDeg is the rotation angle (in degrees, counter-clockwise) that best
	// corrected the skew. It is 0 when no skew was detected or applied.
	AngleDeg float64
	// BaselineConfidence is the InferBlockGrid confidence of the raw (unrotated)
	// image. Values below deskewGridMinConfidence trigger the search.
	BaselineConfidence float64
	// BestConfidence is the InferBlockGrid confidence after applying AngleDeg.
	// It is populated only when Applied is true.
	BestConfidence float64
}

SkewInfo carries the outcome of the automatic skew detection performed by New. It is available via Engine.SkewInfo for introspection and CLI warnings.

type Strategy

type Strategy interface {
	Search(ctx context.Context, redacted *image.RGBA, cfg Config, out chan<- Progress, results chan<- Result)
}

Strategy runs the full character-by-character search over the candidate space and streams progress and results on the provided channels. Implementations may use different algorithms (guided DFS, beam search, etc.). Search must close neither channel; Engine.Run owns their lifecycle.

type Style

type Style struct {
	// FontSize is the font size in points. Defaults to 32.
	FontSize float64
	// Bold selects the bold font variant when true.
	Bold bool
	// PaddingTop is the top padding applied before the text, in pixels. Defaults to 8.
	PaddingTop int
	// PaddingLeft is the left padding applied before the text, in pixels. Defaults to 8.
	PaddingLeft int
	// LetterSpacing is extra horizontal space, in pixels, inserted after each
	// glyph — the equivalent of the CSS letter-spacing property. It may be
	// negative to tighten text (e.g. -0.2 to match a Consolas redaction). The
	// default 0 leaves glyph advances untouched and preserves kerning; a
	// non-zero value renders glyph-by-glyph without kerning.
	LetterSpacing float64
}

Style describes the text rendering parameters that must match the typography of the original, pre-pixelated screenshot.

Directories

Path Synopsis
Package blind provides a zero-config API for blind recovery of mosaic-redacted text without knowing the font, block size, or grid offset in advance.
Package blind provides a zero-config API for blind recovery of mosaic-redacted text without knowing the font, block size, or grid offset in advance.
cmd
unpixel command
Command unpixel reconstructs text hidden behind pixelation.
Command unpixel reconstructs text hidden behind pixelation.
Package defaults wires the standard internal components into an unpixel.Config.
Package defaults wires the standard internal components into an unpixel.Config.
Package fonts bundles a small set of redistributable typefaces so UnPixel can recover a redaction zero-config — without the caller supplying any font file — by sweeping these candidates and keeping the best fit (see github.com/oioio-space/unpixel.RecoverMultiFont).
Package fonts bundles a small set of redistributable typefaces so UnPixel can recover a redaction zero-config — without the caller supplying any font file — by sweeping these candidates and keeping the best fit (see github.com/oioio-space/unpixel.RecoverMultiFont).
internal
blinddecode
Package blinddecode recovers individual words from a mosaic-pixelated redaction band by combining the forward image model (render → re-pixelate → image distance) with a language prior (dictionary membership + infini-gram).
Package blinddecode recovers individual words from a mosaic-pixelated redaction band by combining the forward image model (render → re-pixelate → image distance) with a language prior (dictionary membership + infini-gram).
capacity
Package capacity measures the information-theoretic recoverability of a pixelated-text image for a given rendering geometry (font size, block size, grid phase).
Package capacity measures the information-theoretic recoverability of a pixelated-text image for a given rendering geometry (font size, block size, grid phase).
deblur
Package deblur (l0text.go) implements non-blind L0-regularised text deblurring following Pan, Hu, Su, Yang — "Deblurring Text Images via L0-Regularized Intensity and Gradient Prior", CVPR 2014.
Package deblur (l0text.go) implements non-blind L0-regularised text deblurring following Pan, Hu, Su, Yang — "Deblurring Text Images via L0-Regularized Intensity and Gradient Prior", CVPR 2014.
did
Package did implements Document Image Decoding (DID) for mosaic-pixelated text.
Package did implements Document Image Decoding (DID) for mosaic-pixelated text.
fixture
Package fixture generates synthetic pixelated-text reference images for the recovery test matrix.
Package fixture generates synthetic pixelated-text reference images for the recovery test matrix.
fixture/blurfixture
Package blurfixture generates and describes synthetic Gaussian-blurred text images used by the blur-recovery matrix test.
Package blurfixture generates and describes synthetic Gaussian-blurred text images used by the blur-recovery matrix test.
fixture/gen command
Command gen renders the fixture.Matrix reference images to an output directory, alongside a manifest.json that records each image's filename and its original generation parameters (the image ↔ parameters link).
Command gen renders the fixture.Matrix reference images to an output directory, alongside a manifest.json that records each image's filename and its original generation parameters (the image ↔ parameters link).
fixture/gensick command
Command gensick renders the paper-parity (Hill-2016) sick-corpus reference images to an output directory alongside a manifest.json that records each image's filename, ground-truth text, charset, font, block size, kind ("sick" or "digits"), and a short annotation note.
Command gensick renders the paper-parity (Hill-2016) sick-corpus reference images to an output directory alongside a manifest.json that records each image's filename, ground-truth text, charset, font, block size, kind ("sick" or "digits"), and a short annotation note.
fontrank
Package fontrank ranks candidate fonts by how well their glyph exemplars visually match a mosaic-pixelated redaction image.
Package fontrank ranks candidate fonts by how well their glyph exemplars visually match a mosaic-pixelated redaction image.
imutil
Package imutil provides image manipulation helpers used by the unpixel pipeline.
Package imutil provides image manipulation helpers used by the unpixel pipeline.
lang
Package lang provides a tiny, pure-Go character bigram language model used as a prior over candidate strings.
Package lang provides a tiny, pure-Go character bigram language model used as a prior over candidate strings.
metric
Package metric provides image distance metrics for the unpixel pipeline.
Package metric provides image distance metrics for the unpixel pipeline.
multiframe
Package multiframe implements multi-frame sub-pixel fusion for mosaic-pixelated images.
Package multiframe implements multi-frame sub-pixel fusion for mosaic-pixelated images.
pixelate
Package pixelate (blur.go) adds a Gaussian-blur redaction operator alongside the block-average pixelator.
Package pixelate (blur.go) adds a Gaussian-blur redaction operator alongside the block-average pixelator.
refmatch
Package refmatch implements Depix-style reference-matching decoding for mosaic-pixelated text.
Package refmatch implements Depix-style reference-matching decoding for mosaic-pixelated text.
render
Package render provides the XImage text renderer for the unpixel pipeline.
Package render provides the XImage text renderer for the unpixel pipeline.
search
Package search (beam.go) adds BeamDFS, a beam-search variant of GuidedDFS that bounds the branching factor by retaining only the best cfg.BeamWidth candidates per depth level, and BeamStrategy, the unpixel.Strategy that drives it.
Package search (beam.go) adds BeamDFS, a beam-search variant of GuidedDFS that bounds the branching factor by retaining only the best cfg.BeamWidth candidates per depth level, and BeamStrategy, the unpixel.Strategy that drives it.
secrets
Package secrets scores candidate strings for their resemblance to real secrets and structured tokens (UUIDs, hex tokens, Luhn-valid card numbers, base64 tokens, common passwords).
Package secrets scores candidate strings for their resemblance to real secrets and structured tokens (UUIDs, hex tokens, Luhn-valid card numbers, base64 tokens, common passwords).
segment
Package segment partitions a mosaic/redaction image into text lines and words.
Package segment partitions a mosaic/redaction image into text lines and words.
varfont
Package varfont provides a variable-font renderer and a coordinate-descent axis fitter for the unpixel pipeline.
Package varfont provides a variable-font renderer and a coordinate-descent axis fitter for the unpixel pipeline.
varfont/embed
Package embed exposes the bundled Nunito variable font for use by production code.
Package embed exposes the bundled Nunito variable font for use by production code.
windowhmm
Package windowhmm provides the block-grid types, window-vector extraction, and the trained-HMM primitives (KMeans, Model, Viterbi, Concatenate) used by the sliding-window decoders in mosaictext.
Package windowhmm provides the block-grid types, window-vector extraction, and the trained-HMM primitives (KMeans, Model, Viterbi, Concatenate) used by the sliding-window decoders in mosaictext.
Package mosaictext recovers monospace text from a mosaic-pixelated redaction with zero configuration: given only the image, it locates the redaction, detects the block grid, calibrates the typography (font, size, tracking) from the image itself, and reconstructs the most plausible text.
Package mosaictext recovers monospace text from a mosaic-pixelated redaction with zero configuration: given only the image, it locates the redaction, detects the block grid, calibrates the typography (font, size, tracking) from the image itself, and reconstructs the most plausible text.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL