Skip to content

Commit b3d71b6

Browse files
feat: working rotations on both ios and android
1 parent 390d38c commit b3d71b6

File tree

13 files changed

+82
-47
lines changed

13 files changed

+82
-47
lines changed

apps/computer-vision/app/vision_camera/index.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,10 @@ const TASKS: Task[] = [
7777
},
7878
];
7979

80-
// Module-level const so worklets in task components can always reference the same stable object.
80+
// Module-level consts so worklets in task components can always reference the same stable objects.
8181
// Never replaced — only mutated via setBlocking to avoid closure staleness.
8282
const frameKillSwitch = createSynchronizable(false);
83+
const cameraPositionSync = createSynchronizable<'front' | 'back'>('back');
8384

8485
export default function VisionCameraScreen() {
8586
const insets = useSafeAreaInsets();
@@ -120,6 +121,10 @@ export default function VisionCameraScreen() {
120121
return () => clearTimeout(id);
121122
}, [activeModel]);
122123

124+
useEffect(() => {
125+
cameraPositionSync.setBlocking(cameraPosition);
126+
}, [cameraPosition]);
127+
123128
const handleFpsChange = useCallback((newFps: number, newMs: number) => {
124129
setFps(newFps);
125130
setFrameMs(newMs);
@@ -163,6 +168,7 @@ export default function VisionCameraScreen() {
163168
activeModel,
164169
canvasSize,
165170
cameraPosition,
171+
cameraPositionSync,
166172
frameKillSwitch,
167173
onFrameOutputChange: setFrameOutput,
168174
onReadyChange: setIsReady,

apps/computer-vision/components/vision_camera/tasks/ClassificationTask.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export default function ClassificationTask({
4747
const frameOutput = useFrameOutput({
4848
pixelFormat: 'rgb',
4949
dropFramesWhileBusy: true,
50+
enablePreviewSizedOutputBuffers: true,
5051
onFrame: useCallback(
5152
(frame: Frame) => {
5253
'worklet';

apps/computer-vision/components/vision_camera/tasks/ObjectDetectionTask.tsx

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useCallback, useEffect, useRef, useState } from 'react';
2-
import { Platform, StyleSheet, Text, View } from 'react-native';
2+
import { StyleSheet, Text, View } from 'react-native';
33
import { Frame, useFrameOutput } from 'react-native-vision-camera';
44
import { scheduleOnRN } from 'react-native-worklets';
55
import {
@@ -18,7 +18,7 @@ type Props = TaskProps & { activeModel: ObjModelId };
1818
export default function ObjectDetectionTask({
1919
activeModel,
2020
canvasSize,
21-
cameraPosition,
21+
cameraPositionSync,
2222
frameKillSwitch,
2323
onFrameOutputChange,
2424
onReadyChange,
@@ -70,6 +70,7 @@ export default function ObjectDetectionTask({
7070
const frameOutput = useFrameOutput({
7171
pixelFormat: 'rgb',
7272
dropFramesWhileBusy: true,
73+
enablePreviewSizedOutputBuffers: true,
7374
onFrame: useCallback(
7475
(frame: Frame) => {
7576
'worklet';
@@ -80,10 +81,11 @@ export default function ObjectDetectionTask({
8081
try {
8182
if (!detRof) return;
8283
const orientation = frame.orientation;
83-
const swapAxes = orientation === 'left' || orientation === 'right';
84+
// "up"/"down" = landscape orientations where buffer axes are swapped vs screen
85+
const swapAxes = orientation === 'up' || orientation === 'down';
8486
const screenW = swapAxes ? frame.height : frame.width;
8587
const screenH = swapAxes ? frame.width : frame.height;
86-
const result = detRof(frame, 0.5);
88+
const result = detRof(frame, cameraPositionSync.getDirty(), 0.5);
8789
if (result) {
8890
scheduleOnRN(updateDetections, {
8991
results: result,
@@ -97,7 +99,7 @@ export default function ObjectDetectionTask({
9799
frame.dispose();
98100
}
99101
},
100-
[detRof, frameKillSwitch, updateDetections]
102+
[detRof, frameKillSwitch, updateDetections, cameraPositionSync]
101103
),
102104
});
103105

@@ -113,16 +115,7 @@ export default function ObjectDetectionTask({
113115
const offsetY = (canvasSize.height - imageSize.height * scale) / 2;
114116

115117
return (
116-
<View
117-
style={[
118-
StyleSheet.absoluteFill,
119-
// TODO: remove when VisionCamera fixes front camera orientation reporting on iOS
120-
// https://github.com/mrousavy/react-native-vision-camera/issues/124
121-
Platform.OS === 'ios' &&
122-
cameraPosition === 'front' && { transform: [{ scaleX: -1 }] },
123-
]}
124-
pointerEvents="none"
125-
>
118+
<View style={[StyleSheet.absoluteFill]} pointerEvents="none">
126119
{detections.map((det, i) => {
127120
const left = det.bbox.x1 * scale + offsetX;
128121
const top = det.bbox.y1 * scale + offsetY;
@@ -146,8 +139,6 @@ export default function ObjectDetectionTask({
146139
style={[
147140
styles.bboxLabel,
148141
{ backgroundColor: labelColorBg(det.label) },
149-
Platform.OS === 'ios' &&
150-
cameraPosition === 'front' && { transform: [{ scaleX: -1 }] },
151142
]}
152143
>
153144
<Text style={styles.bboxLabelText}>

apps/computer-vision/components/vision_camera/tasks/SegmentationTask.tsx

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useCallback, useEffect, useRef, useState } from 'react';
2-
import { Platform, StyleSheet, View } from 'react-native';
2+
import { StyleSheet, View } from 'react-native';
33
import { Frame, useFrameOutput } from 'react-native-vision-camera';
44
import { scheduleOnRN } from 'react-native-worklets';
55
import {
@@ -37,7 +37,7 @@ type Props = TaskProps & { activeModel: SegModelId };
3737
export default function SegmentationTask({
3838
activeModel,
3939
canvasSize,
40-
cameraPosition,
40+
cameraPositionSync,
4141
frameKillSwitch,
4242
onFrameOutputChange,
4343
onReadyChange,
@@ -139,6 +139,7 @@ export default function SegmentationTask({
139139
const frameOutput = useFrameOutput({
140140
pixelFormat: 'rgb',
141141
dropFramesWhileBusy: true,
142+
enablePreviewSizedOutputBuffers: true,
142143
onFrame: useCallback(
143144
(frame: Frame) => {
144145
'worklet';
@@ -148,7 +149,7 @@ export default function SegmentationTask({
148149
}
149150
try {
150151
if (!segRof) return;
151-
const result = segRof(frame, [], false);
152+
const result = segRof(frame, cameraPositionSync.getDirty(), [], false);
152153
if (result?.ARGMAX) {
153154
const argmax: Int32Array = result.ARGMAX;
154155
const side = Math.round(Math.sqrt(argmax.length));
@@ -179,7 +180,7 @@ export default function SegmentationTask({
179180
frame.dispose();
180181
}
181182
},
182-
[colors, frameKillSwitch, segRof, updateMask]
183+
[cameraPositionSync, colors, frameKillSwitch, segRof, updateMask]
183184
),
184185
});
185186

@@ -190,16 +191,7 @@ export default function SegmentationTask({
190191
if (!maskImage) return null;
191192

192193
return (
193-
<View
194-
style={[
195-
StyleSheet.absoluteFill,
196-
// TODO: remove when VisionCamera fixes front camera orientation reporting on iOS
197-
// https://github.com/mrousavy/react-native-vision-camera/issues/124
198-
Platform.OS === 'ios' &&
199-
cameraPosition === 'front' && { transform: [{ scaleX: -1 }] },
200-
]}
201-
pointerEvents="none"
202-
>
194+
<View style={StyleSheet.absoluteFill} pointerEvents="none">
203195
<Canvas style={StyleSheet.absoluteFill}>
204196
<SkiaImage
205197
image={maskImage}

apps/computer-vision/components/vision_camera/tasks/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export type TaskProps = {
55
activeModel: string;
66
canvasSize: { width: number; height: number };
77
cameraPosition: 'front' | 'back';
8+
cameraPositionSync: ReturnType<typeof createSynchronizable<'front' | 'back'>>;
89
frameKillSwitch: ReturnType<typeof createSynchronizable<boolean>>;
910
onFrameOutputChange: (frameOutput: ReturnType<typeof useFrameOutput>) => void;
1011
onReadyChange: (isReady: boolean) => void;

apps/computer-vision/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@
3232
"react-native-image-picker": "^7.2.2",
3333
"react-native-loading-spinner-overlay": "^3.0.1",
3434
"react-native-nitro-image": "0.13.0",
35-
"react-native-nitro-modules": "0.35.0",
35+
"react-native-nitro-modules": "0.35.2",
3636
"react-native-reanimated": "~4.2.2",
3737
"react-native-safe-area-context": "~5.6.0",
3838
"react-native-screens": "~4.16.0",
3939
"react-native-svg": "15.15.3",
4040
"react-native-svg-transformer": "^1.5.3",
41-
"react-native-vision-camera": "5.0.0-beta.6",
41+
"react-native-vision-camera": "5.0.0-beta.7",
4242
"react-native-worklets": "0.7.4"
4343
},
4444
"devDependencies": {

packages/react-native-executorch/common/rnexecutorch/utils/FrameProcessor.cpp

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,29 @@ FrameOrientation readFrameOrientation(jsi::Runtime &runtime,
5959
if (val.isNumber()) frameHeight = static_cast<int>(val.asNumber());
6060
}
6161

62-
return {orientation, isMirrored, frameWidth, frameHeight};
62+
std::string cameraPosition = "back";
63+
if (obj.hasProperty(runtime, "cameraPosition")) {
64+
auto val = obj.getProperty(runtime, "cameraPosition");
65+
if (val.isString()) cameraPosition = val.getString(runtime).utf8(runtime);
66+
}
67+
68+
// VisionCamera does not set isMirrored=true for front camera (known bug).
69+
bool rotate180 = false;
70+
if (cameraPosition == "front") {
71+
isMirrored = !isMirrored;
72+
#ifdef __ANDROID__
73+
// Android front camera reports orientation shifted by 180° vs iOS.
74+
if (orientation == "up") orientation = "down";
75+
else if (orientation == "down") orientation = "up";
76+
else if (orientation == "left") orientation = "right";
77+
else if (orientation == "right") orientation = "left";
78+
#else
79+
// iOS front camera needs an extra 180° rotation after the main transform.
80+
rotate180 = true;
81+
#endif
82+
}
83+
84+
return {orientation, isMirrored, frameWidth, frameHeight, rotate180};
6385
}
6486

6587
cv::Mat pixelsToMat(const JSTensorViewIn &pixelData) {

packages/react-native-executorch/common/rnexecutorch/utils/FrameTransform.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ void transformBbox(float &x1, float &y1, float &x2, float &y2,
4747
x1 = nx1; y1 = ny1;
4848
x2 = nx2; y2 = ny2;
4949
}
50+
51+
// Extra 180° in post-rotation screen space (screen dims are h x w after CW).
52+
if (orient.rotate180) {
53+
float nx1 = h - x2, ny1 = w - y2;
54+
float nx2 = h - x1, ny2 = w - y1;
55+
x1 = nx1; y1 = ny1;
56+
x2 = nx2; y2 = ny2;
57+
}
5058
}
5159

5260
cv::Mat transformMat(const cv::Mat &mat, const FrameOrientation &orient) {
@@ -69,6 +77,10 @@ cv::Mat transformMat(const cv::Mat &mat, const FrameOrientation &orient) {
6977
cv::rotate(result, result, cv::ROTATE_90_CLOCKWISE);
7078
}
7179

80+
if (orient.rotate180) {
81+
cv::rotate(result, result, cv::ROTATE_180);
82+
}
83+
7284
return result;
7385
}
7486

packages/react-native-executorch/common/rnexecutorch/utils/FrameTransform.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ struct FrameOrientation {
1313
bool isMirrored;
1414
int frameWidth; // raw frame width (sensor native, before any rotation)
1515
int frameHeight; // raw frame height (sensor native, before any rotation)
16+
bool rotate180 = false; // apply extra 180° after main rotation (front camera correction)
1617
};
1718

1819
/**
@@ -80,6 +81,11 @@ void transformPoints(std::array<P, 4> &points,
8081
}
8182
// "up" = landscape-left: no-op
8283

84+
if (orient.rotate180) {
85+
nx = h - nx;
86+
ny = w - ny;
87+
}
88+
8389
p.x = nx;
8490
p.y = ny;
8591
}

packages/react-native-executorch/src/modules/computer_vision/VisionModule.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { BaseModule } from '../BaseModule';
22
import { RnExecutorchErrorCode } from '../../errors/ErrorCodes';
33
import { RnExecutorchError } from '../../errors/errorUtils';
44
import { Frame, PixelData, ScalarType } from '../../types/common';
5+
import { Platform } from 'react-native';
56

67
export function isPixelData(input: unknown): input is PixelData {
78
return (
@@ -70,18 +71,20 @@ export abstract class VisionModule<TOutput> extends BaseModule {
7071
const nativeGenerateFromFrame = this.nativeModule.generateFromFrame;
7172

7273
// Return worklet that captures ONLY the JSI function
73-
return (frame: any, ...args: any[]): TOutput => {
74+
return (frame: any, cameraPosition: string, ...args: any[]): TOutput => {
7475
'worklet';
7576

7677
let nativeBuffer: any = null;
7778
try {
7879
nativeBuffer = frame.getNativeBuffer();
80+
console.log(frame.orientation);
7981
const frameData = {
8082
nativeBuffer: nativeBuffer.pointer,
8183
orientation: frame.orientation,
8284
isMirrored: frame.isMirrored,
8385
frameWidth: frame.width,
8486
frameHeight: frame.height,
87+
cameraPosition: cameraPosition ?? 'back',
8588
};
8689
return nativeGenerateFromFrame(frameData, ...args);
8790
} finally {

0 commit comments

Comments
 (0)