Skip to content

fix(scanner): ask for the analysis stream through ResolutionSelector - #1310

Merged
bmc08gt merged 2 commits into
code/cashfrom
fix/scanner-analysis-resolution
Aug 22, 2026
Merged

fix(scanner): ask for the analysis stream through ResolutionSelector#1310
bmc08gt merged 2 commits into
code/cashfrom
fix/scanner-analysis-resolution

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Scanning on Android has to be held close. The cause is that the scanner has never actually received the frames it asks for.

setTargetResolution is only a hint. On a 4:3 sensor CameraX resolves the 1920x1080 request down to a 720x720 square — measured on a Solana Seeker, and pinning the target rotation doesn't change it:

deprecated setTargetResolution(1920x1080)            -> negotiated=720x720   rowStride=768  packed=false
deprecated setTargetResolution(1920x1080)+ROTATION_0 -> negotiated=720x720   rowStride=768  packed=false
shipping (ResolutionSelector 16:9 1080p)             -> negotiated=1920x1080 rowStride=1920 packed=true

1080p was always available on that device. It just was never asked for in a way CameraX honours, and nothing in the source read wrong — the request and the reality simply differed.

Why a square crop costs range twice

It keeps the sensor's full height but only three quarters of its width, so the frame covers 57.6° instead of 72.5°, and spans that narrower view with 720 pixels instead of 1920 — 12.5 px/degree where the lens can give 26.5.

Why 1080p and not more

The native detector normalises every threshold to a 480px baseline (scaling_rate = MIN(rows, cols) / 480, minimum_ellipse_area = 220 * scaling_rate), so the smallest decodable code climbs almost as fast as the frame does. Smallest decodable code, worst of three payloads:

analysis res hFOV min code angular range
720x720 (before) 57.6° 97px 7.76° 7.37x code width
1440x1080 72.5° 126px 6.34° 9.02x
1280x720 72.5° 114px 6.46° 8.86x
1920x1080 (after) 72.5° 143px 5.40° 10.60x
2560x1440 72.5° 190px 5.38° 10.64x
3840x2160 72.5° 319px 6.02° 9.5x

≈1.44x the scan range. 1440p is a wash; 4K measures slightly worse while quadrupling the per-frame work. So the win here is field of view, not pixels — "raise the resolution" would have been the wrong fix.

Throughput isn't a constraint either way: worst case across every resolution was 11.5ms a frame, against a 30fps camera.

Falls out for free

The negotiated 1920x1080 plane is tightly packed (rowStride == width), where the 720x720 one had a 768-byte stride — so the unpad copy that ran on every single frame becomes a no-op. And setTargetResolution is deprecated.

Tests

AnalysisResolutionTest binds the same use cases CodeScanner binds and asserts the delivered stream is full-width 16:9 with a short side of at least 1080. Both halves matter: the short side sets how small a code can be, the aspect ratio decides how much of the lens is in the frame at all. This bug was invisible by inspection, so it needs a test that asserts on what the camera delivers rather than on what the code requests.

KikCodeRangeTest produces the table above, plus the throughput bound.

Both measure synthetic, ideal frames — perfect focus, no motion blur, no sensor noise. The ratios between resolutions are the trustworthy part; real-world range is strictly worse than the absolute numbers.

Deliberately not in this change

  • Default zoom. Range scales near-linearly with it at no per-frame cost, but camera 0's minimum focus distance is 0.50m. Zooming narrows the frame, pushing users toward filling it — toward getting closer — exactly as the near limit becomes binding. Trades a common case for a rarer one.
  • Clamping scaling_rate in the detector. The larger structural win, and precisely what caps the resolution benefit above — but scanner.cpp exists in parallel in the iOS repo, so retuning thresholds on one side silently diverges scan behaviour across platforms. It also trades against false positives, which synthetic frames can't measure. Worth its own piece of work, mirrored, against a real-world corpus.

iOS sets .hd1920x1080 on the capture session directly, so it likely never had this problem — which fits the symptom being Android-only.

One device

Every number here is one camera's negotiation, on a Solana Seeker. Which resolution a given device returns for a given request is a property of that device, so the guard asserts the invariant — full-width 16:9, short side at least 1080 — rather than the measured sizes, and alternativeConfigurationsForComparison logs what else that hardware offers.

@github-actions github-actions Bot added type: fix Bug fix area: ui Compose UI, theme, components, resources area: scanner QR/Kikcode scanning, camera labels Aug 22, 2026
setTargetResolution is only a hint. On a 4:3 sensor CameraX resolved the
1920x1080 request down to a 720x720 square, and had done so for as long as the
line has existed -- nothing in the source read wrong, the request and the
reality simply differed.

A square crop costs range twice. It keeps the sensor's full height but only
three quarters of its width, so the frame covers 57.6 degrees instead of 72.5,
and it spans that narrower view with 720 pixels instead of 1920: 12.5 pixels
per degree where the lens can give 26.5. Naming the aspect ratio explicitly
gets the full-width 16:9 stream the scanner was always meant to have. Measured
on a Solana Seeker, the smallest decodable code goes from 7.76 to 5.40 degrees
of arc -- 1.44x the scan range.

1080p specifically, not more. The native detector normalises every threshold to
a 480px baseline (scaling_rate = MIN(rows, cols) / 480), so the smallest
decodable code grows almost as fast as the frame does: 1440p is a wash and 4K
measures slightly worse while quadrupling the per-frame work.

Two things fall out for free. The negotiated 1920x1080 plane is tightly packed
where the 720x720 one had a 768-byte stride, so the unpad copy that ran on
every frame becomes a no-op. And setTargetResolution is deprecated.

All of the above is one device's negotiation. Which resolution a given camera
returns is a property of that camera, so the guard added alongside this asserts
the invariant rather than the measured numbers.
The resolution bug was invisible by inspection -- the only symptom was that
scanning felt short-ranged -- so pin it with a test that asserts on what the
camera delivers rather than on what the code asks for. AnalysisResolutionTest
binds the same use cases CodeScanner binds and requires a full-width 16:9
stream with a short side of at least 1080; both halves matter, since the short
side sets how small a code can be and the aspect ratio decides how much of the
lens is in the frame at all. It also records what the alternatives negotiate,
so the choice can be re-checked on other hardware rather than taken on faith.

KikCodeRangeTest answers the question that motivated the change: how small a
code can get before the detector stops seeing it. It bisects the floor at each
candidate resolution and converts the result to angular size and range, which
is what makes "raise the resolution" visibly the wrong fix -- the pixel floor
climbs almost as fast as the frame, so the gain above 1080p is nil. A
throughput pass alongside it bounds the cost: worst case 11.5ms a frame against
a 30fps camera.

Both are measurements on synthetic, ideal frames -- perfect focus, no motion
blur, no sensor noise -- taken on a Solana Seeker. The ratios between
resolutions are the trustworthy part; real-world range is strictly worse than
the absolute numbers, and another camera may negotiate differently.
@bmc08gt
bmc08gt force-pushed the fix/scanner-analysis-resolution branch from ab15b12 to efbd16c Compare August 22, 2026 15:30
@bmc08gt
bmc08gt merged commit f23429e into code/cash Aug 22, 2026
3 checks passed
@bmc08gt
bmc08gt deleted the fix/scanner-analysis-resolution branch August 22, 2026 16:26
@bmc08gt
bmc08gt restored the fix/scanner-analysis-resolution branch August 22, 2026 16:27
@bmc08gt
bmc08gt deleted the fix/scanner-analysis-resolution branch August 22, 2026 16:28
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 area: ui Compose UI, theme, components, resources type: fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant