fix(scanner): ask for the analysis stream through ResolutionSelector - #1310
Merged
Conversation
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
force-pushed
the
fix/scanner-analysis-resolution
branch
from
August 22, 2026 15:30
ab15b12 to
efbd16c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Scanning on Android has to be held close. The cause is that the scanner has never actually received the frames it asks for.
setTargetResolutionis only a hint. On a 4:3 sensor CameraX resolves the1920x1080request down to a 720x720 square — measured on a Solana Seeker, and pinning the target rotation doesn't change it: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:≈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. AndsetTargetResolutionis deprecated.Tests
AnalysisResolutionTestbinds the same use casesCodeScannerbinds 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.KikCodeRangeTestproduces 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
scaling_ratein the detector. The larger structural win, and precisely what caps the resolution benefit above — butscanner.cppexists 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
.hd1920x1080on 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
alternativeConfigurationsForComparisonlogs what else that hardware offers.