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
}
Output:
Index ¶
- Constants
- Variables
- func InferBlockSize(img image.Image) int
- func InferBlockSizeRobust(img image.Image) (blockSize int, support float64)
- func InferBlurSigma(img image.Image) float64
- func InferDarkBackground(img image.Image) bool
- func InferFontSize(img image.Image) float64
- func InferImpulseNoise(img image.Image) float64
- func LocateRedaction(img image.Image) (image.Rectangle, bool)
- func OnProgress(ch <-chan Progress, fn func(Progress))
- type BlockGrid
- type Config
- type Engine
- type Eval
- type EventKind
- type FontResult
- type Metric
- type Offset
- type Option
- func WithBeamWidth(width int) Option
- func WithBlockSize(n int) Option
- func WithCharset(charset string) Option
- func WithCharsetTopK(k int) Option
- func WithConfig(cfg Config) Option
- func WithLanguageModel(score func(string) float64) Option
- func WithMaxLength(n int) Option
- func WithMetric(m Metric) Option
- func WithNormalize(fns ...func(*deblur.Options)) Option
- func WithPixelator(p Pixelator) Option
- func WithPriors(priors ...func(string) float64) Option
- func WithRemosaic() Option
- func WithRemosaicGrid(b int) Option
- func WithRemosaicLinear() Option
- func WithRenderer(r Renderer) Option
- func WithStrategy(s Strategy) Option
- func WithStyle(s Style) Option
- func WithThreshold(t float64) Option
- func WithTopN(n int) Option
- func WithWorkers(n int) Option
- type Pixelator
- type Progress
- type Renderer
- type Result
- func Recover(ctx context.Context, redacted image.Image, opts ...Option) (Result, error)
- func RecoverBlurred(ctx context.Context, img image.Image, opts ...Option) (Result, error)
- func RecoverBlurredFile(ctx context.Context, path string, opts ...Option) (Result, error)
- func RecoverBlurredReader(ctx context.Context, r io.Reader, opts ...Option) (Result, error)
- func RecoverFile(ctx context.Context, path string, opts ...Option) (Result, error)
- func RecoverReader(ctx context.Context, r io.Reader, opts ...Option) (Result, error)
- type SkewInfo
- type Strategy
- type Style
Examples ¶
Constants ¶
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.
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.
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 ¶
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()).
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.
var ErrNilImage = errors.New("redacted image must not be nil")
ErrNilImage is returned by New when a nil image is passed.
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
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
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
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
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
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
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:
- |lum(p) – median8| > impulseNoiseThreshold (large deviation from context).
- 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
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 ¶
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
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 ¶
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
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
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 ¶
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.
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.
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 ¶
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
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
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
WithCharset sets Config.Charset (the ordered candidate alphabet).
func WithCharsetTopK ¶ added in v0.5.0
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
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 WithLanguageModel ¶ added in v0.4.0
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
WithMaxLength sets Config.MaxLength (the maximum candidate length).
func WithMetric ¶ added in v0.3.0
WithMetric sets Config.Metric (the image-distance metric).
func WithNormalize ¶ added in v0.9.0
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
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
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
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
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
WithStrategy sets Config.Strategy (the search algorithm).
func WithStyle ¶ added in v0.3.0
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
WithThreshold sets Config.Threshold (the per-block acceptance gate).
func WithWorkers ¶ added in v0.3.0
WithWorkers sets Config.Workers (max grid offsets searched concurrently; non-positive means runtime.GOMAXPROCS).
type Pixelator ¶
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
}
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
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
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:
- Estimate σ₀ = InferBlurSigma(img). The estimator is accurate to within ~15% on typical text images (validated over the fixture set).
- Run ONE full Recover at σ₀ with exact GaussianBlur.
- 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).
- 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
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
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
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
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
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.
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). |
|
deblur
Package deblur provides image normalisation helpers that prepare real-world blurred-text captures (textured background, vignette, dark theme, JPEG blocking) for the generate-and-test recovery loop, which assumes a clean Gaussian blur on a flat-white background.
|
Package deblur provides image normalisation helpers that prepare real-world blurred-text captures (textured background, vignette, dark theme, JPEG blocking) for the generate-and-test recovery loop, which assumes a clean Gaussian blur on a flat-white background. |
|
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. |
|
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. |
|
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. |
|
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. |