convertCameraPointToFramePoint ignores normalising extents on orientation swap (iOS) — square region maps to a 3.16:1 strip
Prerequisites
- I have read and followed every applicable step in the Nitro Modules Troubleshooting guide.
- I have read the VisionCamera Troubleshooting guide.
- I have searched existing issues and found nothing matching (no hits for
convertCameraPointToFramePoint). - I am on the latest version, or have a specific reason I cannot upgrade — see "VisionCamera version" below.
Reproduction
https://github.com/mrousavy/react-native-vision-camera/pull/4113 — adds one failing Harness test, maps a square Frame region onto a square View region, to apps/simple-camera/__tests__/visioncamera.coordinates.harness.tsx.
Steps to reproduce
- Check out the branch in #4113.
- Run the coordinates harness on an iOS device:
bun run test:harness:ios -- --testPathPatterns=coordinates. - Watch
maps a square Frame region onto a square View region. - Run the same file on an Android device — it passes there.
What did you expect to happen?
Composing the two public coordinate conversions should preserve aspect. A square region in Frame space should map to a square region in View space:
PreviewView.convertViewPointToCameraPoint() -> Frame.convertCameraPointToFramePoint()Every transform in between — sensor scale, orientation counter-rotation, the preview's aspect-preserving crop — preserves aspect. Camera space itself is allowed to be anisotropic (iOS normalises per-axis to [0, 1]), but converting out of it should undo the same per-axis scaling it applied.
What actually happened?
On iOS the square comes back as a wide, short strip. Measured on iPhone 15 Pro Max, frame 3840×2160, orientation: "left", using a square guide cell of identical ±19.7 dp in both view axes:
sensor Δx 0.0562 -> frame Δy 121.4 px
sensor Δy 0.0999 -> frame Δx 383.6 px383.6 × 121.4 px — a ratio of 3.159, where a correct mapping measures 1.000. Pixel 8 (1280×720) composes correctly at 1.000.
convertViewPointToCameraPoint looks correct on its own: for the square patch it returns Δx/Δy = 0.5625 = exactly 9/16, consistent with per-axis normalisation over a 16:9 image.
Suspected cause. ios/Utils/FrameCoordinateSystemConverter.getFrameToCameraMatrix rotates within [0, 1]² in step 1, then normalises in step 3 with matrix.scaledBy(x: 1 / width, y: 1 / height). scaledBy post-concatenates, so the point is normalised by the buffer's own extents before the rotation swaps the axes — and the extents never swap with it. The numbers match that reading exactly: 0.0562 × 2160 = 121.4, 0.0999 × 3840 = 383.6. Predicted anisotropy for 16:9 at orientation: left is (16/9)² = 3.1605 against the measured 3.159.
The existing tests can't catch it: the Frame→Camera→Frame round-trip is self-inverse (getCameraToFrameMatrix is getFrameToCameraMatrix().inverted()), and the end-to-end center test uses the frame center, which maps to camera (0.5, 0.5) under both a correct and an anisotropic matrix. Only an off-center region exposes it. Full analysis in the PR.
Affected platforms
iOS (device)
Device(s) affected
iPhone 15 Pro Max (iOS 26.5.2) — reproduces. Pixel 8 — does not reproduce (exact Android build not recorded at measurement time).
VisionCamera version
5.0.11. Not on the latest (5.2.0) because 5.2.0 is nitrogen-0.36.3-generated and our app is still on react-native-nitro-modules 0.35.x — that upgrade is scheduled separately.
This is very unlikely to matter here: ios/Utils/FrameCoordinateSystemConverter.swift is byte-identical across v5.0.11, v5.1.0, v5.1.1 and v5.2.0 (md5 7e406d9d979e0490f06ca05f7e52ea70, 74 lines each), and no 5.x release note touches it. Happy to re-measure on 5.2.0 if you'd like it confirmed on the current release.
React Native version
0.86.0 at measurement time (since upgraded to 0.86.2).
React Native architecture
New Architecture (Fabric / bridgeless)
Context
Found while mapping a Rubik's cube scan guide onto sampled frames in a shipping app. Our workaround validates the composed mapping with an isotropy check and falls back to deriving the geometry from the frame dimensions when it fails — no platform branch, it's measured at runtime. Android accepts the library transform; iOS rejects it at 3.159 and falls back. Glad to test a fix on device.
Source: mrousavy/react-native-vision-camera