detect

package
v0.0.0-...-b4425be Latest Latest
Warning

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

Go to latest
Published: Aug 22, 2026 License: MIT Imports: 23 Imported by: 0

Documentation

Overview

Package detect locates JAB Code symbols in an image: channel balancing and binarization (with descreen retries sized from the image's own lattice pitch), finder- and alignment-pattern detection, side-size estimation, perspective sampling of the module grid, and the region-of-interest proposer. Decoding the sampled matrix lives in the sibling decode package; the read package coordinates the two.

Index

Constants

View Source
const (
	IntensiveDetect = 2
)

Detection modes. The shared status codes live in the core package.

View Source
const ROIAnnexThreshold = 0.05

ROIAnnexThreshold is the lower level of a two-level (hysteresis) threshold: a component must contain a tile at ROIThreshold to count, but extends through connected tiles down to this fraction of the peak. A rotated symbol's corner covers only a sliver of its tiles, which score below ROIThreshold but far above background (measured on the dev capture: corner tiles 0.05-0.20 of peak, noise under 0.01), so annexing them keeps the corner inside the component's box.

View Source
const ROIThreshold = 0.20

ROIThreshold keeps a tile when its joint score is at least this fraction of the peak tile score. It is the floor separating the symbol's dense, colourful texture from background clutter; a starting value, refined by measurement, not a copied constant tied to any pixel size.

Variables

View Source
var SideEdges = [4][2]int{{0, 1}, {3, 2}, {0, 3}, {1, 2}}

SideEdges is the four finder-to-finder edges the side size is decided from, as index pairs into a located quad: the two that span X, then the two that span Y. It is the contract between whoever walks the edges and whoever weighs the results, so a device walk and the host's agree on the order.

Functions

func AutomaticGPURouteAvailable

func AutomaticGPURouteAvailable(width, height int) error

AutomaticGPURouteAvailable reports why the automatic device route cannot engage for an image of this size, or nil when it can.

vulki.Open answers a different question. It succeeds on a software or integrated adapter, which the route then declines for not being discrete, so a caller that probes only for Vulkan concludes the device route is live where it is not. Callers asserting on device work ask here instead of restating the route's rule and drifting from it.

Answering is not free and not read-only: it runs the route's own lazy discovery, so on an eligible machine the process-wide device is opened here if it was not open already and stays open for the rest of the process, exactly as the first eligible decode would have left it.

func BalanceRGB

func BalanceRGB(bm *core.Bitmap)

BalanceRGB stretches each channel's histogram to the full 0..255 range, in place.

func BinarizerRGB

func BinarizerRGB(bm *core.Bitmap, blkThs []float32) [3]*core.Bitmap

BinarizerRGB binarizes the image into three channel bitmaps using per-pixel color analysis. When blkThs is nil, a scale-adaptive grid of bilinearly-interpolated per-channel block means is used as the local black threshold; otherwise blkThs is a flat per-channel threshold.

func BinarizerRGBPrint

func BinarizerRGBPrint(bm *core.Bitmap) [3]*core.Bitmap

BinarizerRGBPrint binarizes at print levels for the detector's print retry: the black gate tests against each block's black anchor instead of its mean, so dark saturated print colours (subtractive gamut) classify as colour rather than black. Not the default because the two level choices conflict: heavy blur lifts true black into the same value band where print inks live, and a half-blurred black pixel next to a yellow module is ratio-identical to a dark printed yellow - only the retry ladder's evidence separates the two regimes image-wide.

func BinarizerRGBUntil

func BinarizerRGBUntil(bm *core.Bitmap, blkThs []float32, quit func() bool) ([3]*core.Bitmap, bool)

BinarizerRGBUntil is BinarizerRGB with cancellation, for callers that run it speculatively: quit is polled per scanline, and a cancelled call reports no channels rather than a half-binarized frame, because a partial mask samples as real data and hard LDPC success is not payload integrity.

func CalculateModuleNumber

func CalculateModuleNumber(fp1, fp2 FinderPattern) int

CalculateModuleNumber estimates the number of modules between two patterns, correcting the finder scale for the edge's projection onto the image axes.

func CalculateSideSize

func CalculateSideSize(bm *core.Bitmap, fps []FinderPattern) image.Point

CalculateSideSize derives the symbol's side size in modules from the four finder-pattern positions. When a bitmap is given, the modules along each edge are counted by the local-sampling walk, which stays accurate on large and rectangular symbols; nil, or a bitmap whose pixels are still resident on a device, restricts it to the finder-distance estimate. The layout is FP0 FP1 / FP3 FP2.

func CalculateSideSizeWalked

func CalculateSideSizeWalked(counts [4]int, fps []FinderPattern) image.Point

CalculateSideSizeWalked decides the side size from already-walked module counts, so the walk itself can happen wherever the pixels are. A count of -1 is the walk declining to answer, and leaves the edge to the finder-distance estimate.

func ConsistentFinderQuad

func ConsistentFinderQuad(fps []FinderPattern) bool

ConsistentFinderQuad reports whether the four selected finder patterns (in FP0..FP3 cyclic type order) are consistent enough to sample directly: convex, with module sizes and opposite-edge lengths agreeing within the gross-inconsistency thresholds. The per-type selection scores each type's best by foundCount with no cross-type geometry, so it can pick corners that pass the side-size arithmetic yet form a degenerate quad that samples off the grid; a false result here routes the selection to SelectFinderQuadByGeometry.

func CropImage

func CropImage(img image.Image, r image.Rectangle) *image.NRGBA

CropImage returns the r portion of img as a standalone image, clipped to img's bounds. The per-region decode retry probes orientation on such crops, so the probe's downscale works at the region's scale rather than the whole frame's.

func DetectSecondary

func DetectSecondary(bm *core.Bitmap, ch [3]*core.Bitmap, host, secondary *core.DecodedSymbol, dockedPosition int) *core.Bitmap

DetectSecondary finds and samples a secondary symbol docked at the given position of a host symbol.

func DownscaleToMax

func DownscaleToMax(src image.Image, maxDim int) *image.NRGBA

DownscaleToMax returns src reduced so its longer side is at most maxDim, by averaging each destination pixel over the source box it covers (a box filter, which preserves the finder rings better than point sampling at the reduction the coarse search needs). An image already within the bound is returned unreduced - aliased when it already is a zero-origin NRGBA, as an NRGBA copy otherwise - so treat the result read-only.

func EstimatePitch

func EstimatePitch(bm *core.Bitmap) (px, py int)

EstimatePitch estimates the dominant lattice pitch of bm in pixels along the x and y axes, by 1-D autocorrelation of evenly sampled scanlines (for px) and columns (for py). On a screen capture this recovers the display's subpixel / diode-grid period without any prior detection, so the descreen low-pass can size its kernel per image rather than from a fixed radius, which would be wrong at some capture distance or display resolution. A returned 0 on an axis means no periodic structure was found in the search range, so the caller should treat that as "no descreen on that axis".

func GPURoutesDisabled

func GPURoutesDisabled() bool

GPURoutesDisabled reports the switch's state.

func HalveNRGBA

func HalveNRGBA(in *image.NRGBA) *image.NRGBA

HalveNRGBA returns in box-filtered to half size, rounding odd sides up - the constructor for successive resolution-pyramid levels. Each destination pixel averages the 2x2 source box it covers, so every halving is also a mild low-pass: coarse levels arrive pre-smoothed, which is why they can decode captures whose full-resolution noise defeats detection.

func LocalModuleCount

func LocalModuleCount(bm *core.Bitmap, fpA, fpB FinderPattern) int

LocalModuleCount counts the modules between two finder-pattern centers by walking their connecting line one module at a time: each step advances by the locally interpolated module size and is then re-centered on the most homogeneous window along the line. The module-size measurement error is corrected at every module instead of accumulating over the whole distance, which is what makes the plain distance/module-size estimate miss the side version on large and rectangular symbols. It returns -1 when no trustworthy count can be produced (degenerate or shape-only input, or a diverged walk).

This is the Local Sampling method of Bugert, Heeger and Berchtold, "Version Detection of JAB Codes" (Electronic Imaging 2025, IPAS-229).

func LocalModuleCounts

func LocalModuleCounts(bm *core.Bitmap, fps []FinderPattern) [4]int

LocalModuleCounts walks all four edges on the host, in SideEdges order.

func ProbeDegrees

func ProbeDegrees() []float64

ProbeDegrees exposes the sweep so a consumer can tell a reported direction from an arbitrary angle without duplicating the list.

func ProposeROIsTraced

func ProposeROIsTraced(img image.Image, maxN int) ([]ROICandidate, ROITileMap)

ProposeROIsTraced is ProposeROIs with the exact tile map used to produce the candidates. The map is returned for observation only; proposal decisions are made once by the shared implementation.

func SampleAlignmentBlocks

func SampleAlignmentBlocks(bm *core.Bitmap, side image.Point, blocks []AlignmentBlock) *core.Bitmap

SampleAlignmentBlocks assembles the module grid on the host, sampling each block through its own perspective. Blocks are written in the order given because they overlap: the selection sorts the widest rectangle first, so a later, tighter block is the one whose modules should survive.

func SampleSymbol

func SampleSymbol(bm *core.Bitmap, pt core.Perspective, side image.Point) *core.Bitmap

SampleSymbol samples a side.X by side.Y matrix of module colors from the image using the perspective transform. Small-module symbols use the ported 3x3 centre kernel; larger modules use the tent-weighted footprint mean (see the geometry constants above). On a clean flat-colour module both equal the centre value, so clean decodes stay byte-identical. It returns an RGBA bitmap of the sampled module values, or nil if a module maps too far outside the image.

func SampleSymbolByAlignmentPattern

func SampleSymbolByAlignmentPattern(sample BlockSampler, ch [3]*core.Bitmap, symbol *core.DecodedSymbol, fps []FinderPattern) *core.Bitmap

SampleSymbolByAlignmentPattern detects all alignment patterns, splits the symbol into blocks bounded by four found patterns, and samples each block with its own perspective transform.

func SampleSymbolByAlignmentPatternTraced

func SampleSymbolByAlignmentPatternTraced(sample BlockSampler, ch [3]*core.Bitmap, symbol *core.DecodedSymbol, fps []FinderPattern, trace *AlignmentTrace) *core.Bitmap

SampleSymbolByAlignmentPatternTraced is SampleSymbolByAlignmentPattern with detailed observation of the same sampling run.

func SampleSymbolOffset

func SampleSymbolOffset(bm *core.Bitmap, pt core.Perspective, side image.Point, delta [3]core.PointF) *core.Bitmap

SampleSymbolOffset is SampleSymbol with a per-channel source-pixel offset added to every sampling position: channel c of each module is read at the warped point plus delta[c]. Misregistered colorant planes displace each channel's content from the nominal grid, so sampling each channel where its plane actually landed recovers the module colour the overlay destroyed. Zero deltas reproduce SampleSymbol exactly.

func ScoreFinderQuad

func ScoreFinderQuad(p0, p1, p2, p3 FinderPattern) (float64, bool)

ScoreFinderQuad gates and scores a candidate quad (p0,p1,p2,p3 in FP0/FP1/FP2/FP3 type order, which is cyclic TL,TR,BR,BL around the symbol). It returns a badness score (lower is better, 0 = ideal) and whether the quad passes every geometric gate.

func SearchChannelOffsets

func SearchChannelOffsets(bm *core.Bitmap, pt core.Perspective, side image.Point) [3]core.PointF

SearchChannelOffsets searches, independently per colour channel, for the source-pixel sampling offset that makes the channel's module values most bimodal, over a grid of candidate offsets spanning half a module. A module's channel value is ideally one of two levels (ink or no ink in that plane); colorant-plane misregistration slides the plane off the module grid, so the centre samples mix neighbours and collapse toward mid-range. The offset that restores bimodality is where the plane actually landed. Zero is in the candidate grid and wins ties, so channels without real misregistration keep their nominal positions and the result degrades to plain sampling.

func SetGPURoutesDisabled

func SetGPURoutesDisabled(disabled bool)

SetGPURoutesDisabled turns the automatic GPU routes off or on. GPU routing enabled is the supported default; disabling it is a debugging control for comparing the two routes, not a tuning knob.

It is read only where a route decides to acquire or keep a device session: the native and browser automatic constructors, and a stream's session refresh. The refresh checks it separately because a live stream reuses its session rather than reacquiring one, so an acquisition-time check alone would never be consulted again; a stream therefore changes route on its next frame. Nothing further down reads it - no detection, sampling or correction step branches on it - so it selects a route and never alters what a route does. It does not reach a session a caller opened explicitly against a device it supplied, and it does not interrupt a decode already running.

func SideSize

func SideSize(size int) (int, int)

SideSize rounds a raw module count to the nearest valid side size and returns a reliability flag. flag: 1 reliable, 0 guessed, -1 invalid.

func SmallestVerifiableFrame

func SmallestVerifiableFrame() int

SmallestVerifiableFrame returns the shorter-side length a frame needs before a maximum-size primary symbol can present its modules at the scale the finder cross-checks require: the largest primary side (side version 32, 145 modules) at that module scale, edge to edge with no margin. Below it, no primary symbol placement resolves - the frame itself is the limit, not the framing - which is the one case where enlarging the pixels is worth its cost.

Deliberately primary-only: a clustered or docked arrangement spans several symbols and would raise this bound far past what a single symbol needs, turning a floor into an excuse to enlarge ordinary captures.

func UpscaleNRGBA

func UpscaleNRGBA(in *image.NRGBA, factor int) *image.NRGBA

UpscaleNRGBA returns in enlarged by an integer factor with a separable Catmull-Rom kernel. Nearest-neighbour enlargement would be pointless here - it multiplies every run length and every quantization error alike, leaving the finder ratio checks exactly where they were - so the interpolation is the point: it places module edges from the surrounding samples instead of the source grid. The kernel's mild sharpening keeps those edges from smearing across the extra pixels the way a bilinear enlargement does.

func WaitAutomaticGPUDecodeWarm

func WaitAutomaticGPUDecodeWarm()

WaitAutomaticGPUDecodeWarm blocks until an in-flight warm-up has published its workspace. A read never needs this - the route joins the preparation itself - but a caller measuring a decode does: without it the device's one-time transfers land inside whatever window the measurement opened, and the per-image cost cannot be told from the session's.

func WarmAutomaticGPUDecode

func WarmAutomaticGPUDecode(width, height, levelCount int)

WarmAutomaticGPUDecode prepares the automatic decode route for a frame of this geometry and returns immediately. Opening a Vulkan device, compiling the kernel set and building the size-matched workspace all happen on the critical path of a single-shot read otherwise, and none of them need the image - only the pixel upload does. A caller that knows the frame size before it has decoded the image can therefore overlap all of it; the route's own acquisition joins this preparation rather than repeating it. A caller that never reaches a device route has spent one goroutine, and a frame below the automatic threshold still never initializes Vulkan.

Types

type AlignmentBlock

type AlignmentBlock struct {
	Transform core.Perspective
	Size      image.Point
	Origin    image.Point
}

AlignmentBlock is one alignment-grid rectangle's sampling request: the perspective that carries the block's own module coordinates into the image, the block's module extent, and where its top-left module lands in the assembled symbol grid.

type AlignmentRectangle

type AlignmentRectangle struct {
	TopLeft     image.Point
	BottomRight image.Point
}

AlignmentRectangle identifies one AP-grid rectangle used to sample a block.

type AlignmentTrace

type AlignmentTrace struct {
	Attempted  bool
	ReuseCount int
	Reason     string
	Grid       image.Point
	Expected   []FinderPattern
	Patterns   []FinderPattern
	Rectangles []AlignmentRectangle
	Matrix     *core.Bitmap
}

AlignmentTrace records the expected and resolved alignment-pattern grid and the sampling rectangles selected from it.

type BlockSampler

type BlockSampler func(side image.Point, blocks []AlignmentBlock) *core.Bitmap

BlockSampler assembles one symbol's module grid from its alignment blocks, returning nil when a block maps outside the image. The alignment resample takes one rather than an image because pattern detection reads the binarized channels while only the block sampling needs source colour, and on a device route that colour never leaves the device.

It takes the whole block set at once rather than a block at a time so that a device can scatter each block into the grid it already holds. Handing blocks back one by one made the assembled matrix a host artefact, which the payload chain then had to decline because it had never held that grid.

type CornerSource

type CornerSource uint8

CornerSource says where a completed quad's fourth corner came from. A construction is exact while the capture stays affine and a guess once it does not; the other values carry progressively stronger image evidence. The coarse consistency gates cannot tell those cases apart because interpolation completes a parallelogram whose convexity and opposite-edge agreement come from the arithmetic itself.

const (
	CornerFound       CornerSource = iota // every type had its own detection
	CornerConstructed                     // interpolated from the other three
	CornerPooled                          // a strict candidate another direction or pass found
	CornerSought                          // the local seek confirmed the estimate
	CornerContextual                      // a branch-confirmed candidate completed a strong triple
)

func (CornerSource) String

func (c CornerSource) String() string

String names the source for diagnostics.

type DetectorStats

type DetectorStats struct {
	Passes    []FinderPassStats // one entry per prepared image pass
	RGBAvg    [3]float32        // retry thresholds from averagePixelValue, between passes
	Consensus FinderConsensusStats
}

DetectorStats aggregates finder-detection instrumentation across the raw, average-RGB, descreen and conditional print passes LocateFinders runs.

func (DetectorStats) PublishedScanDegrees

func (s DetectorStats) PublishedScanDegrees(family FinderFamily) float64

PublishedScanDegrees reports the same direction from a recorded snapshot, which is what a consumer holding stats rather than a live detector has: the diagnostic trace keeps the stats and the family of every attempt, so a route that claims a direction can be checked against the scan that actually produced its quad instead of against the sweep it merely belongs to.

type DetectorTrace

type DetectorTrace struct {
	PassInputs   []*core.Bitmap
	PassChannels [][3]*core.Bitmap
	FinderPasses []FinderPassTrace

	// RejectCounts and Rejections answer "what stops finders here" from the
	// detector itself rather than from a replica. The counts are the funnel;
	// the samples carry the run window, which is the only part that cannot be
	// reconstructed afterwards from a centre and a direction.
	//
	// Scope: the current-family directional scan only. The axis-aligned row walk
	// and the BSI signature do not report here, so a zero count is not evidence
	// that those paths accepted anything.
	RejectCounts [FinderStageCount]int
	Rejections   []FinderRejection
	// contains filtered or unexported fields
}

DetectorTrace retains the binarized channels used by each finder pass. Its entries align with DetectorStats.Passes. It is populated only when attached to a PrimaryDetector by the detailed read trace.

type FinderConsensusStats

type FinderConsensusStats struct {
	GeometryTuples      int
	GeometryScores      int
	InterpolatedTriples int
	InterpolatedSeeks   int
}

FinderConsensusStats records work done only by the fallback quad searches. These counters make expensive candidate volume visible without influencing the deterministic selection order.

type FinderFamily

type FinderFamily uint8

FinderFamily identifies one physical primary-finder signature.

const (
	// FinderFamilyCurrent is the ISO/current-C finder signature.
	FinderFamilyCurrent FinderFamily = iota
	// FinderFamilyBSI is the primary finder signature defined by BSI
	// TR-03137 and retained by pre-v2.0 releases of the C reference. Its
	// classifier is compiled only when one of those wire variants is enabled.
	FinderFamilyBSI
)

func (FinderFamily) Mask

func (family FinderFamily) Mask() FinderFamilySet

Mask returns the one-signature set for family.

type FinderFamilyPassStats

type FinderFamilyPassStats struct {
	RawHits        int    // n-1-1-1-m run-length hits (horizontal + conditional vertical scan)
	BranchBlue     int    // green seeds where the blue cross-check fired (-> {FP0,FP3} path)
	BranchRed      int    // green seeds where blue failed and the red cross-check fired (-> {FP1,FP2} path)
	RedColor       int    // red-path candidates passing the inner core-colour check (fp2found)
	RedClassified  int    // red-path candidates matched to fp1/fp2 by core-colour classification
	CrossSurvivors [4]int // candidates passing crossCheckPattern, by finder type
	Scans          []FinderFamilyScanStats
	Preprune       [4]int          // published scan's group sizes before the prune
	Selected       [4]int          // published scan's post-prune selection
	Missing        int             // published scan's absent types
	Status         int             // published scan's status
	Corner         CornerSource    // published scan's fourth-corner source
	Candidates     []FinderPattern // merged finder candidates this pass (pre-prune)
}

FinderFamilyPassStats records one physical signature's counters inside a shared image pass. They are observation only and never influence detection.

RawHits through CrossSurvivors accumulate over every scan direction the pass tried. The selection fields cannot be accumulated that way and would be last-writer-wins if they were assigned per direction, so they and Candidates mirror the published scan and the per-direction values live in Scans.

type FinderFamilyScanStats

type FinderFamilyScanStats struct {
	Degrees    float64      // scan direction, 0 for the row walk
	Preprune   [4]int       // group sizes before the 0.5*maxFound prune
	Preselect  [4]int       // FoundCount of each type's best pattern before that prune
	Selected   [4]int       // FoundCount of the selected pattern per type after the prune (0 = absent)
	Missing    int          // types absent after selection
	Status     int          // this direction's findPrimarySymbol status
	Corner     CornerSource // where the fourth corner came from
	Consistent bool         // whether the quad passed ConsistentFinderQuad
	Published  bool         // whether this is the quad the pass published
}

FinderFamilyScanStats records one scan direction's selection outcome. A pass sweeps several directions, so these are the only per-direction numbers; the pass-level copies below describe the published one alone.

type FinderFamilySet

type FinderFamilySet uint8

FinderFamilySet is the set of physical finder signatures located by one integrated detector pass.

func (FinderFamilySet) Has

func (set FinderFamilySet) Has(family FinderFamily) bool

Has reports whether set contains family.

type FinderPassStats

type FinderPassStats struct {
	Label string // raw, avg-RGB, descreen or print input shared by all signatures
	FinderFamilyPassStats
	// contains filtered or unexported fields
}

FinderPassStats records one shared finder-detection pass. The embedded counters are for the current signature so existing diagnostics retain their field names; tagged builds add the optional BSI-era signature's counters to the same pass without enlarging the untagged structure.

func (FinderPassStats) BSIFamilyStats

func (FinderPassStats) BSIFamilyStats() (FinderFamilyPassStats, bool)

BSIFamilyStats reports no optional signature in an untagged detector.

type FinderPassTrace

type FinderPassTrace struct {
	Families FinderFamilySet
	Finders  [finderFamilyCount][]FinderPattern
	Scans    []FinderScanTrace
}

FinderPassTrace retains the requested signatures, each successful signature's published quad, and the quad every other scan direction offered. It is allocated only for an attached diagnostic trace, so ordinary decoding does not retain this rendering state.

type FinderPattern

type FinderPattern struct {
	Typ        int
	ModuleSize float64
	Center     core.PointF
	FoundCount int
	// contains filtered or unexported fields
}

FinderPattern is a detected finder or alignment pattern.

type FinderQuadHypothesis

type FinderQuadHypothesis struct {
	Patterns [4]FinderPattern
	Corner   CornerSource
}

FinderQuadHypothesis is one bounded primary-quad candidate carried from detection into sampling. Corner records the evidence behind its weakest corner so a construction cannot silently outrank an image-backed candidate.

type FinderRejection

type FinderRejection struct {
	Stage    FinderStage
	Pass     int
	Typ      int
	Channel  int // channel walked or sampled, -1 where the stage is not per-channel
	BaseDeg  float64
	WalkDeg  float64
	Centre   core.PointF
	Module   float64
	Confirms int
	Runs     [5]int
	Reason   WalkReject
}

FinderRejection is one representative rejected candidate. Runs is the five-run window of the walk that decided it and Reason is why that walk stopped, both empty where the stage compares module sizes or colours rather than walking.

Pass is the index into DetectorStats.Passes. Retries re-binarize the same frame, so a rejection means nothing without knowing which binarization produced it. Centre and WalkDeg describe the walk that failed, not the candidate: the cross-checks refine the centre in place and the diagonal turns away from the scan direction, so reporting the candidate's own centre and base direction would name a position and a line the failing walk never sampled. Confirms is the diagonal confirmation count, which distinguishes "no diagonal held" from "one held and one was needed".

type FinderScanTrace

type FinderScanTrace struct {
	Family FinderFamily
	Scan   int
	Quad   []FinderPattern // the four selected patterns, entries with FoundCount 0 absent
	Pre    []FinderPattern // the same four before the outvoted-type prune
}

FinderScanTrace is one scan direction's selected quad. The sweep reuses its working patterns for the next direction, so a quad that was found and not published exists nowhere afterwards unless it is copied here - and "the direction that would have won was never compared" is a failure mode this detector has already had.

Scan indexes the direction's entry in that family's FinderFamilyScanStats, which carries its angle, status and published flag; keeping one index rather than copies of those keeps the two records from disagreeing.

type FinderStage

type FinderStage uint8

FinderStage names where a candidate left the directional cross-check chain. The order is the chain's, so a histogram of RejectCounts reads as a funnel.

const (
	// StageBranchPattern and StageBranchColor are kept apart because a single
	// "the branch failed" bucket cannot distinguish a candidate whose blue and
	// red pattern walks both failed from one whose pattern walk passed and whose
	// core-colour check then rejected it. Merging them attributes colour
	// rejections to the run window of a walk that succeeded.
	StageBranchPattern FinderStage = iota
	StageBranchColor
	StageBranchModuleSize
	StageClassify
	StageChainSignal
	StageChainBase // the perpendicular passed and moved the centre, the base walk then failed
	StageChainDiagonal
	StageChainModuleSize
	StageChainColor

	// FinderStageCount bounds RejectCounts, whose element count is part of that
	// exported field's type.
	FinderStageCount
)

func (FinderStage) String

func (s FinderStage) String() string

String names the stage for diagnostics.

type GPUDecodeSession

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

GPUDecodeSession leases the process-wide resident image workspace to one decode. Its methods may be called by concurrent pyramid and rotation routes; each route leases its own context from the workspace pool, so one route's CPU scan overlaps another route's device kernels.

func NewAutomaticGPUDecodeSession

func NewAutomaticGPUDecodeSession(base *core.Bitmap, levelCount int) (*GPUDecodeSession, error)

NewAutomaticGPUDecodeSession starts a resident decode workspace when the image crosses the measured GPU threshold and a measured discrete Vulkan adapter is available. A nil session means the caller should use the CPU path.

func NewGPUDecodeSessionWithDevice

func NewGPUDecodeSessionWithDevice(
	device *vulki.Device,
	base *core.Bitmap,
	levelCount int,
) (*GPUDecodeSession, error)

NewGPUDecodeSessionWithDevice starts a resident session on a borrowed device. Closing the session releases its buffers and pipelines but leaves the device open. It is the explicit parity and embedding seam; normal reads use NewAutomaticGPUDecodeSession.

func NewGPUDecodeSessionWithDeviceScanOnly

func NewGPUDecodeSessionWithDeviceScanOnly(
	device *vulki.Device,
	base *core.Bitmap,
	levelCount int,
) (*GPUDecodeSession, error)

NewGPUDecodeSessionWithDeviceScanOnly creates a borrowed-device session whose route contexts never replay on the device, so the bit-identical CPU per-hit chain and pitch fold classify every hit. It exists to exercise those twins deterministically, and as the degraded mode every session already falls back to before the chain kernels finish compiling; it is slower than the default on everything but a small symbol with large modules.

func (*GPUDecodeSession) Close

func (session *GPUDecodeSession) Close() error

Close waits for every registered session operation and in-flight route to finish, then releases the workspace. Automatic sessions cache it for another same-sized decode; borrowed-device sessions release their buffers and pipelines. The operation gate covers the window between a method's entry and its context acquisition, so no operation can straddle the release and touch a torn-down or reused workspace.

func (*GPUDecodeSession) CurrentPyramidBatchHasWork

func (session *GPUDecodeSession) CurrentPyramidBatchHasWork() bool

CurrentPyramidBatchHasWork reports whether any prepared level returned a current-family hypothesis. Empty sibling levels can then stop without starting historical materializing routes while that hypothesis is parsed.

func (*GPUDecodeSession) DecodeLevelCurrentBatch

func (session *GPUDecodeSession) DecodeLevelCurrentBatch(
	level int,
	variants []wire.Variant,
	mode int,
	quit func() bool,
) (detector *PrimaryDetector, attempts []PrimaryBatchAttempt, release func(), err error)

DecodeLevelCurrentBatch keeps the ordinary current-family route resident from its row-first finder decision through primary admission. The caller receives one fixed result batch and retains the context only for final message parsing or a docked continuation.

func (*GPUDecodeSession) DownloadLevel

func (session *GPUDecodeSession) DownloadLevel(level int) (*core.Bitmap, error)

DownloadLevel copies one retained pyramid level back to the host as a packed RGBA bitmap. The levels are read-only once the session's build finished, so downloads may run concurrently with route work.

No decode route calls it, and none may: a level is the device route's own working set, and moving one to the host to spare a CPU route its halving chain was the largest single line in the transfer census. It survives as the ladder's accessor for the parity and close-race gates.

func (*GPUDecodeSession) LocateLevelFamilies

func (session *GPUDecodeSession) LocateLevelFamilies(
	level int,
	wanted FinderFamilySet,
	mode int,
	quit func() bool,
	trace *DetectorTrace,
) (detector *PrimaryDetector, found FinderFamilySet, release func(), err error)

LocateLevelFamilies runs the complete integrated finder retry ladder on one retained pyramid level. Every retry reuses the leased context's resident balanced pixels and returns only packed masks or compact reductions until pixels are needed downstream.

**The returned release must be called when the caller is done with the detector**, and it is never nil once the detector is. The lease used to end here, which forced every level that located anything to download its whole balanced image before returning - tens of megabytes per level, on every level, when only the level that goes on to sample ever reads them. Holding the lease across the caller's decode is what makes that download lazy. It costs no extra contexts: the pool is warmed with one per pyramid level, and a level only ever holds its own.

func (*GPUDecodeSession) PrepareCurrentPyramidBatch

func (session *GPUDecodeSession) PrepareCurrentPyramidBatch(
	variants []wire.Variant,
	mode int,
) error

PrepareCurrentPyramidBatch runs every current-family pyramid level into one device-resident result allocation and downloads that allocation once. The ordinary level routes consume the prepared entries through DecodeLevelCurrentBatch, retaining their context leases only as long as final message parsing can still need resident pixels.

func (*GPUDecodeSession) ReplaceBase

func (session *GPUDecodeSession) ReplaceBase(base *core.Bitmap) error

ReplaceBase refreshes the retained image pyramid without rebuilding its sized workspace. A stream reuses this seam for coherent frames with stable geometry; the upload and every halving pass overwrite all frame-owned pixels before another route can acquire the workspace.

func (*GPUDecodeSession) Retire

func (session *GPUDecodeSession) Retire() error

Retire returns an automatic session's successful result after transferring its workspace lease to a reaper. The reaper preserves Close's operation and context lifetime guarantees; a later automatic decode waits for that lease instead of racing reuse or falling back to the CPU. Borrowed sessions retain synchronous Close semantics because their caller controls the device.

func (*GPUDecodeSession) WaitReplayKernels

func (session *GPUDecodeSession) WaitReplayKernels() error

WaitReplayKernels blocks until every kernel the replay policy switches on is compiled and usable, or returns the first compilation error.

Sessions warm these in a goroutine nothing waits on, because a cold driver pipeline cache can take minutes on the largest modules this package submits. That makes replay a race: a pass that starts before the warm finishes silently takes the CPU twin instead. Anything comparing the two routes has to settle that race first, and by waiting on the compile rather than by sleeping, since a sleep long enough to be safe on a cold cache is one that also hides what it is waiting for.

It waits for the pitch-lag kernels as well as the finder chains because scanOnly gates both, and waiting on only one leaves the other free to switch mechanism mid-measurement. Compilation is per-kernel idempotent, so joining the warm costs nothing once it has already run.

type PrimaryBatchAttempt

type PrimaryBatchAttempt struct {
	Result         core.PrimaryDeviceResult
	Variant        wire.Variant
	Side           image.Point
	Patterns       [4]FinderPattern
	Corner         CornerSource
	Degrees        float64
	Slot           int
	Geometry       int
	AlignmentRetry bool
	PrintDetected  bool
}

PrimaryBatchAttempt is one evidence-bearing primary interpretation from the resident row-first route. An alignment retry is paired with the direct slot whose complete message parse must fail before the retry is considered. The final message parser remains above detection: syntax can admit an attempt, but it cannot rank two disagreeing payloads.

type PrimaryDetector

type PrimaryDetector struct {
	BM         *core.Bitmap
	Ch         [3]*core.Bitmap
	Mode       int
	FPs        []FinderPattern
	Candidates []FinderPattern // last pass's pre-prune candidates, for the geometric quad fallback
	Stats      DetectorStats

	Trace *DetectorTrace

	// Quit, when set, is polled between binarization passes; once it reports
	// true the search abandons its remaining retries and fails. The resolution
	// pyramid cancels levels that can no longer win this way, so an abandoned
	// level stops burning cores within one pass instead of finishing its whole
	// retry ladder in the background.
	Quit func() bool

	// AxisAlignedScan confines the finder search to image rows, suppressing the
	// directional retry. The coarse orientation probe needs it: that probe
	// measures a frame's orientation by pre-rotating it and comparing which
	// rotation the row walk likes best, so a scan that locates the symbol at
	// every orientation would leave it ranking noise. Ordinary reads leave it
	// off and let one prepared frame answer every direction.
	AxisAlignedScan bool
	// contains filtered or unexported fields
}

PrimaryDetector orchestrates primary-symbol finder detection over the three binarized channels. Its findPrimarySymbol/selectBestPatterns/scanPatternVertical methods populate stats, the single source of truth for the diagnostic. The Ch field is a by-value [3]*core.Bitmap: the retry's re-binarization (LocateFinders) is scoped to this detector and never leaks into secondary decoding.

func (*PrimaryDetector) ActiveFinderFamily

func (d *PrimaryDetector) ActiveFinderFamily() (FinderFamily, bool)

ActiveFinderFamily reports which signature produced the current FPs, and whether one was ever selected. A quad means nothing without it: the two families have different finder geometry, so attributing one family's quad to the other misreads the geometry that produced every downstream number.

func (*PrimaryDetector) Balanced

func (d *PrimaryDetector) Balanced() *core.Bitmap

Balanced materializes the balanced image for a stage that will read pixels, and reports nil rather than failing outright when it cannot. It is the on-demand form every remaining whole-frame consumer takes, so a read that never reaches one never pays for the download.

func (*PrimaryDetector) ChannelExpansionCount

func (d *PrimaryDetector) ChannelExpansionCount() int

ChannelExpansionCount reports how many times deferred mask channels have been materialized for this detector. It makes residency tests distinguish a bounded Pixel read from a full-mask expansion.

func (*PrimaryDetector) ChannelOffsets

func (d *PrimaryDetector) ChannelOffsets(pt core.Perspective, side image.Point) [3]core.PointF

SideSize derives the symbol's side size in modules from a located quad, walking the four finder-to-finder edges on the device when the pixels are still resident there. The walk reads only small windows along four lines, so running it where the image already is keeps a whole-frame download from being the price of a few hundred of them. ChannelOffsets searches the per-channel plane displacement a print capture needs, on the device when the balanced pixels are still resident there and on the host otherwise.

The host search reads every module's footprint on all three channels for a few hundred candidates, so running it here means downloading the whole balanced frame - by far the largest transfer a print read makes. Scoring where the pixels already are leaves only the score table to cross.

func (*PrimaryDetector) DirectionalScanError

func (d *PrimaryDetector) DirectionalScanError() error

DirectionalScanError reports the first directional device sweep failure of the last locate, or nil if none failed. A fallback that reports nothing is indistinguishable from a machine without a GPU, which makes a kernel regression invisible for as long as it takes someone to notice the speed.

func (*PrimaryDetector) EnsureBalanced

func (d *PrimaryDetector) EnsureBalanced() bool

EnsureBalanced materializes the balanced image and reports whether BM now holds pixels. A device-backed detector keeps them resident until a host stage reads one, and that materialization is only possible while the route lease is held, so a caller that lets BM outlive the lease must ask for them first.

func (*PrimaryDetector) EnsureChannels

func (d *PrimaryDetector) EnsureChannels() bool

EnsureChannels fills the located pass's channel bitmaps with mask pixels on first need and reports whether channel pixels are available. CPU-built detectors always carry full channels; a GPU-located detector expands its packed snapshot only when a consumer actually reads mask pixels, such as alignment resampling, the docked traversal or a historical wire route.

func (*PrimaryDetector) FinderQuadHypotheses

func (d *PrimaryDetector) FinderQuadHypotheses(family FinderFamily) []FinderQuadHypothesis

FinderQuadHypotheses returns the located quad and any contextual alternatives in sampling order. Image-backed alternatives precede an affine construction; otherwise the detector's published quad remains first.

func (*PrimaryDetector) GridDevice

func (d *PrimaryDetector) GridDevice() core.GridDevice

GridDevice reports what can fill a sampled grid's module data, or nil when this detector's samples already carry theirs.

func (*PrimaryDetector) LocateFinderFamilies

func (d *PrimaryDetector) LocateFinderFamilies(wanted FinderFamilySet) FinderFamilySet

LocateFinderFamilies runs one finder traversal per prepared image pass and classifies every requested physical signature inside that traversal. The retry re-binarizes d.Ch in place; because the channel array is held by value, that swap is scoped to this detector and does not propagate to secondary detection. The C reference differs here: its detectMaster overwrites the caller's channel array, so it detects docked secondaries on the retry's re-binarization while this port detects them on the first-pass channels. The two can diverge only for a multi-symbol code whose primary needed the retry; the wire format is unaffected.

func (*PrimaryDetector) LocateFinders

func (d *PrimaryDetector) LocateFinders() bool

LocateFinders locates the current ISO/current-C finder signature. Optional signatures are not enabled by this compatibility wrapper.

func (*PrimaryDetector) LocateInitialFinderFamilies

func (d *PrimaryDetector) LocateInitialFinderFamilies(wanted FinderFamilySet) FinderFamilySet

LocateInitialFinderFamilies runs only the first balanced-image finder pass. It is the compact-mask boundary used by the automatic GPU path: a successful pass can materialize pixels for geometry and sampling, while a failed pass falls back to the complete CPU retry ladder without changing its behavior.

func (*PrimaryDetector) MaterializeGrid

func (d *PrimaryDetector) MaterializeGrid(matrix *core.Bitmap, reason core.GridReason) bool

MaterializeGrid fills a sampled grid's module data, reporting whether the modules are readable afterwards. A host-sampled grid already carries them.

func (*PrimaryDetector) MetadataDevice

func (d *PrimaryDetector) MetadataDevice() core.MetadataDevice

MetadataDevice reports the walker that can interpret a sampled grid's metadata strip where it already lies, or nil when this detector has none and the host walks it.

func (*PrimaryDetector) PayloadDevice

func (d *PrimaryDetector) PayloadDevice() core.PayloadDevice

PayloadDevice reports the corrector that can interpret a sampled grid where it already lies, or nil when this detector has none and the host chain owns the whole post-sampling stage.

func (*PrimaryDetector) PrimaryDevice

func (d *PrimaryDetector) PrimaryDevice() core.PrimaryDevice

PrimaryDevice reports the fused primary decoder for an ordinary resident sample, or nil when this detector has no device route for the whole stage.

func (*PrimaryDetector) PrintDetected

func (d *PrimaryDetector) PrintDetected() bool

PrintDetected reports whether the successful finder pass was a print-level one, which is the gate for the per-channel sampling-offset search.

func (*PrimaryDetector) PublishPrimaryBatchAttempt

func (d *PrimaryDetector) PublishPrimaryBatchAttempt(attempt PrimaryBatchAttempt)

PublishPrimaryBatchAttempt makes the physical evidence behind a successful resident interpretation observable to later sampling and diagnostics. The device already sampled the primary, but docked symbols and diagnostic paths still consult the detector and must inherit the winning print regime.

func (*PrimaryDetector) PublishedScanDegrees

func (d *PrimaryDetector) PublishedScanDegrees() float64

PublishedScanDegrees reports the scan direction that produced the active family's published quad, or -1 when nothing was published. A route label alone cannot say whether the search stayed on image rows: every whole-frame pass sweeps scanDirections when its row walk does not settle, so this is the only thing that distinguishes a row-settled read from one that turned.

func (*PrimaryDetector) Quitting

func (d *PrimaryDetector) Quitting() bool

Quitting reports whether an installed Quit hook has cancelled this search. Consumers poll it at their own stage boundaries so a route that already lost stops before work whose result can no longer be used.

func (*PrimaryDetector) SampleBlocks

func (d *PrimaryDetector) SampleBlocks(side image.Point, blocks []AlignmentBlock) *core.Bitmap

SampleBlocks assembles one alignment resample's module grid, on the device when the balanced pixels are still resident there and on the host otherwise. The whole block set goes in one call so that the device route's assembled grid is the buffer the payload chain already holds, rather than a matrix stitched together out of blocks that each crossed the bus.

func (*PrimaryDetector) SampleByAlignment

func (d *PrimaryDetector) SampleByAlignment(
	sample BlockSampler,
	symbol *core.DecodedSymbol,
	fps []FinderPattern,
	trace *AlignmentTrace,
) *core.Bitmap

SampleByAlignment resamples the symbol from its alignment grid, locating the grid on the device when this detector has one. The device path never touches d.Ch, so a route that located, sampled and decoded on the device never has a reason to bring the binarized masks back at all.

func (*PrimaryDetector) SampleGrid

func (d *PrimaryDetector) SampleGrid(
	pt core.Perspective, side image.Point, delta [3]core.PointF,
) *core.Bitmap

SampleGrid samples the module grid through the perspective transform, on the device when the balanced pixels are still resident there and on the host otherwise. It returns nil when a module maps too far outside the image, which is a failed sample rather than an error.

The distinction matters for the transfer budget: a device sample moves the grid, a host sample moves the frame that produces it, and the two differ by three orders of magnitude on a phone capture.

func (*PrimaryDetector) SelectConsensusQuad

func (d *PrimaryDetector) SelectConsensusQuad() bool

SelectConsensusQuad assembles a finder quad from the cross-pass candidate union when the greedy per-type selection located nothing on any pass: the full geometric consensus first, then the consistent-triple interpolation. On success it installs the quad as the current-family finder list so the sampler can proceed; a wrong grid it assembles is caught downstream by the admission gate. Reports whether a quad was installed.

func (*PrimaryDetector) SelectFinderFamily

func (d *PrimaryDetector) SelectFinderFamily(family FinderFamily) bool

SelectFinderFamily selects one located signature as the detector's active finder list for geometry and sampling. It returns false when that signature did not form a usable finder quad in the last integrated search.

func (*PrimaryDetector) SelectFinderQuadByGeometry

func (d *PrimaryDetector) SelectFinderQuadByGeometry() ([4]FinderPattern, bool)

SelectFinderQuadByGeometry searches all finder candidates for the four - one per type, in the FP0 FP1 / FP3 FP2 layout - that best form a valid symbol quad. The per-type selection in selectBestPatterns scores each type's best by foundCount with no cross-type geometry, so on a noisy capture it can pick four candidates that do not form a symbol; this consensus search is the fallback. It runs only after the normal path fails to yield a valid side size, so clean decodes are untouched.

func (*PrimaryDetector) SelectFinderQuadByInterpolatedTriple

func (d *PrimaryDetector) SelectFinderQuadByInterpolatedTriple() ([4]FinderPattern, bool)

SelectFinderQuadByInterpolatedTriple handles the case where one finder type has no consistent candidate at all: three types agree on a consistent triple while the fourth is absent or present only as an off-scale spurious hit, so no full four-candidate quad is consistent and SelectFinderQuadByGeometry finds nothing. It searches for the best-scoring consistent triple, interpolates the missing corner from it with the same geometry finishCurrentFamilyScan uses for a single missing finder, and returns the completed quad when it passes the ScoreFinderQuad gates. Like the full consensus it only runs after the per-type selection is already rejected as inconsistent, so a clean selection is never disturbed; a wrong grid it might still assemble is caught downstream by the palette-coherence admission gate. The interpolation seek averages a bounded window of the balanced image, so this needs the bitmap but never the mask pixels, and a deferred GPU snapshot stays packed across the whole search.

func (*PrimaryDetector) SideSize

func (d *PrimaryDetector) SideSize(fps []FinderPattern) image.Point

type ROICandidate

type ROICandidate struct {
	Bounds     image.Rectangle
	Score      float64 // summed joint tile score over the region, the ranking key
	ChromaVar  float64 // mean tile chroma variance over the region
	GradEnergy float64 // mean tile gradient energy over the region
	Tiles      int
}

ROICandidate is a proposed region likely to hold a symbol, in full-resolution pixel coordinates, with the joint score that ranked it.

func ProposeROIs

func ProposeROIs(img image.Image, maxN int) []ROICandidate

ProposeROIs ranks regions of img by how much they look like a JAB symbol: a dense patch that is both high in local chroma variance (many different saturated colours, unlike a flat coloured UI bar) and high in gradient energy (a fine module grid, unlike a plain background or document). The two features are combined multiplicatively so a region must satisfy both at once: flat coloured chrome scores high chroma but near-zero variance, and a plain rectangle scores low gradient, so each drops out of the product. It returns at most maxN candidates, best first, or nil if nothing stands out. It reads img only; it never modifies it or the decode.

func ProposeROIsWithin

func ProposeROIsWithin(img image.Image, maxN, maxDim, grid int) []ROICandidate

ProposeROIsWithin is ProposeROIs under a custom working-resolution bound and tile-grid density (see BuildROITileMapWithin): the seam for proposing at a scale matched to the content, e.g. a denser grid so the gutters of a multi-code sheet can separate the codes into distinct components.

type ROITileMap

type ROITileMap struct {
	Score, Chroma, Grad []float64
	GX, GY, Tile        int
	W, H                int
}

ROITileMap is the per-tile joint-score grid ProposeROIs thresholds: the max-normalized chroma-variance and gradient-energy features and their product over the GX by GY tile grid of the W by H downscaled working image.

func BuildROITileMap

func BuildROITileMap(img image.Image) ROITileMap

func BuildROITileMapWithin

func BuildROITileMapWithin(img image.Image, maxDim, grid int) ROITileMap

BuildROITileMapWithin is BuildROITileMap under a custom working-resolution bound and tile-grid density: both analysis scales are parameters, so a proposer variant can match them to the content instead of the flat defaults. The grid density is what bounds how narrow a separating gap (e.g. the white gutter between codes on a printed sheet) can still form a score valley: a gap narrower than one tile never can.

func (ROITileMap) Peak

func (m ROITileMap) Peak() float64

type WalkReject

type WalkReject uint8

WalkReject names why a cross-check walk gave up. The run window alone does not say: a window that satisfies checkPatternCross is still rejected when the module size it implies exceeds the candidate's ceiling, and the two are opposite findings - "no finder here" against "a finder, but not one this candidate could be".

const (
	WalkNotWalked  WalkReject = iota // the stage compared sizes or colours instead of walking
	WalkIncomplete                   // the five-run window never completed inside the frame
	WalkTooWide                      // the middle runs outgrew the module ceiling mid-walk
	WalkSignature                    // checkPatternCross rejected the run ratios
	WalkModuleSize                   // the ratios held, the module size they imply did not
)

func (WalkReject) String

func (r WalkReject) String() string

String names the reason for diagnostics.

Jump to

Keyboard shortcuts

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