Skip to content

fix(scanner): re-land the badge-dot prune and the dead adaptive thresholds - #1309

Merged
bmc08gt merged 2 commits into
code/cashfrom
fix/scanner-badge-dot-prune-and-dead-thresholds
Aug 22, 2026
Merged

fix(scanner): re-land the badge-dot prune and the dead adaptive thresholds#1309
bmc08gt merged 2 commits into
code/cashfrom
fix/scanner-badge-dot-prune-and-dead-thresholds

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Re-lands the two scanner commits that never reached code/cash.

#1306 and #1307 were stacked on each other's head branches rather than on
code/cash. #1303 merged first, which retired the bottom of the stack, and the
two above it then merged into branches that were already spent. Both PRs show as
merged; neither one's content is on code/cash.

So code/cash today has the luminance fast path and nothing else from that
series. This puts the rest back, cherry-picked onto current code/cash rather
than by merging the old branch — that branch predates #1304 and #1305, and
merging it would revert them.

fix: the badge glyph's dot steals the centre ellipse

The detector prunes ellipse candidates that sit within 50px of each other,
keeping whichever it reaches first. The badge is a disc with the "F" knocked out
under the even-odd rule, and the glyph's dot is itself a near-perfect ellipse
about 0.08·D from the badge centre. Past a certain on-screen size both land in
the same prune bucket, and the forward-only 2 * area(i) > area(j) rule always
discards the larger candidate — the badge, which is the only one that can
yield nine finder points. The search then runs from the dot and finds one.

The prune now fires only when the two candidates are comparable in area, i.e.
genuine near-duplicates: the same physical circle fitted twice from the inner and
outer edge of its stroke. A much smaller neighbour is a different feature nested
inside, not a duplicate.

This is a hard failure band, not a slowdown — hold the phone too close and the
code does not decode until you back off.

The harness could not have caught it

KikCodeScanTest on code/cash renders the badge as ShapeDrawable(OvalShape()).
A featureless oval has no knocked-out dot, so the sweep passes with the bug fully
present. This restores the real ic_logo_round_white artwork, and adds the
drawable, which is absent from code/cash entirely.

perf: two full-frame adaptive thresholds nothing reads

detectKikCode opened with a pair of 21x21 Gaussian adaptive thresholds.
whitish is unconditionally overwritten by the global threshold before anything
reads it; blackish is only read on the inverted-code path, which recomputes it
itself behind blackish_created with a different algorithm. Measured on an S25
Ultra, 30 iterations per case, native detect only:

frame code present before after
1280x720 yes 10.44 ms 4.99 ms
1280x720 no 14.30 ms 8.90 ms
1920x1080 yes 8.27 ms 2.79 ms
1920x1080 no 10.01 ms 4.47 ms

About 5.5 ms off every analyzed frame, hit or miss — larger than the luminance
fast path it stacks on.

Both changes are already on iOS (code-ios-app #630); iOS never carried the
adaptive-threshold block.

…ipse

A code is located by its centre badge, but the badge is a disc with the
Flipcash "F" knocked out of it, and the glyph's round dot is itself a
near-perfect ellipse sitting ~0.08D from the badge centre. Once the code
is drawn large enough in frame, that dot clears the candidate filters and
the two ellipses end up in the same prune bucket.

The prune dropped the wrong one. Its rule was "discard this candidate if a
nearby one is at most twice its area", which is order-dependent and, for a
big/small pair, always discards the big one. The badge disappeared and the
scan was left searching from the dot, which yields one finder point instead
of nine. Measured on a 1920x1080 frame: a code occupying 540px decodes, 756
and 972 do not.

Prune only true near-duplicates -- the same physical circle fitted twice
from the inner and outer edge of its stroke, which are comparable in area.
A much smaller neighbour is a different feature nested inside, so keep both
and let the search decide; the badge sorts first and still hits on the first
iteration, so the happy path is unchanged.

The sweep harness masked this by substituting a plain white oval for the
badge. It now renders the real artwork, which is what put the dot in frame
in the first place.
…eads

detectKikCode opened with a pair of 21x21 Gaussian adaptive thresholds into
`whitish` and `blackish`. Neither result is ever read:

  - `whitish` is unconditionally overwritten a few dozen lines later by the
    global `threshold(greyscale, whitish, 170, 255, THRESH_BINARY)` before
    anything touches it.
  - `blackish` is only read on the inverted-code path (`!check_high`), and that
    path recomputes it itself, lazily, behind `blackish_created` -- with a
    different algorithm (ADAPTIVE_THRESH_MEAN_C at the quality-dependent width).

So the block was two full-frame adaptive thresholds per analyzed frame, thrown
away every time. Measured on an S25 Ultra over 30 iterations per case:

  1280x720   code present   10.44ms -> 4.99ms
  1280x720   no code        14.30ms -> 8.90ms
  1920x1080  code present    8.27ms -> 2.79ms
  1920x1080  no code        10.01ms -> 4.47ms

That is ~5.5ms off every frame the analyzer processes, hit or miss -- larger
than the luminance-plane fast path it stacks on. The instrumented sweep still
decodes 18/18 and stride parity is unchanged, which is what you would expect
from deleting values nothing consumes.

iOS never carried this block.
@github-actions github-actions Bot added type: fix Bug fix area: scanner QR/Kikcode scanning, camera and removed type: fix Bug fix labels Aug 22, 2026
@bmc08gt
bmc08gt merged commit 9a2ee5b into code/cash Aug 22, 2026
3 checks passed
@bmc08gt
bmc08gt deleted the fix/scanner-badge-dot-prune-and-dead-thresholds branch August 22, 2026 14:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: scanner QR/Kikcode scanning, camera

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant