Skip to content

Commit d3ba923

Browse files
docs: update vision camera docs
1 parent 3b61d98 commit d3ba923

File tree

1 file changed

+124
-37
lines changed

1 file changed

+124
-37
lines changed

docs/docs/03-hooks/02-computer-vision/visioncamera-integration.md

Lines changed: 124 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,12 @@ Use `runOnFrame` when you need to process every camera frame. Use `forward` for
3636

3737
## How it works
3838

39-
VisionCamera v5 delivers frames via [`useFrameOutput`](https://react-native-vision-camera-v5-docs.vercel.app/docs/frame-output). Inside the `onFrame` worklet you call `runOnFrame(frame)` synchronously, then use `scheduleOnRN` from `react-native-worklets` to post the result back to React state on the main thread.
39+
VisionCamera v5 delivers frames via [`useFrameOutput`](https://react-native-vision-camera-v5-docs.vercel.app/docs/frame-output). Inside the `onFrame` worklet you call `runOnFrame(frame, isFrontCamera)` synchronously, then use `scheduleOnRN` from `react-native-worklets` to post the result back to React state on the main thread.
40+
41+
The `isFrontCamera` parameter tells the native side whether the front camera is active so it can correctly mirror the results. The library handles all device orientation rotation internally — results are always returned in screen-space coordinates regardless of how the user holds their device.
4042

4143
:::warning
42-
You **must** set `pixelFormat: 'rgb'` in `useFrameOutput`. Our extraction pipeline expect RGB pixel data — any other format (e.g. the default `yuv`) will produce incorrect results.
44+
You **must** set `pixelFormat: 'rgb'` in `useFrameOutput`. Our extraction pipeline expects RGB pixel data — any other format (e.g. the default `yuv`) will produce incorrect results.
4345
:::
4446

4547
:::warning
@@ -50,11 +52,27 @@ You **must** set `pixelFormat: 'rgb'` in `useFrameOutput`. Our extraction pipeli
5052
Always call `frame.dispose()` after processing to release the frame buffer. Wrap your inference in a `try/finally` to ensure it's always called even if `runOnFrame` throws.
5153
:::
5254

53-
## Full example (Classification)
55+
## Camera configuration
56+
57+
The `Camera` component requires specific props for correct orientation handling:
58+
59+
```tsx
60+
<Camera
61+
device={device}
62+
outputs={[frameOutput]}
63+
isActive
64+
orientationSource="device"
65+
/>
66+
```
67+
68+
- **`orientationSource="device"`** — ensures frame orientation metadata reflects the physical device orientation, which the library uses to rotate model inputs and outputs correctly.
69+
- **Do not set `enablePhysicalBufferRotation`** — this prop must remain `false` (the default). If enabled, VisionCamera pre-rotates the pixel buffer, which conflicts with the library's own orientation handling and produces incorrect results.
70+
71+
## Full example (Object Detection)
5472

5573
```tsx
56-
import { useState, useCallback } from 'react';
57-
import { Text, StyleSheet } from 'react-native';
74+
import { useState, useCallback, useRef } from 'react';
75+
import { StyleSheet, View, Text } from 'react-native';
5876
import {
5977
Camera,
6078
Frame,
@@ -63,43 +81,43 @@ import {
6381
useFrameOutput,
6482
} from 'react-native-vision-camera';
6583
import { scheduleOnRN } from 'react-native-worklets';
66-
import { useClassification, EFFICIENTNET_V2_S } from 'react-native-executorch';
84+
import {
85+
Detection,
86+
useObjectDetection,
87+
SSDLITE_320_MOBILENET_V3_LARGE,
88+
} from 'react-native-executorch';
6789

6890
export default function App() {
6991
const { hasPermission, requestPermission } = useCameraPermission();
7092
const devices = useCameraDevices();
7193
const device = devices.find((d) => d.position === 'back');
72-
const model = useClassification({ model: EFFICIENTNET_V2_S });
73-
const [topLabel, setTopLabel] = useState('');
94+
const model = useObjectDetection({ model: SSDLITE_320_MOBILENET_V3_LARGE });
95+
const [detections, setDetections] = useState<Detection[]>([]);
7496

75-
// Extract runOnFrame so it can be captured by the useCallback dependency array
76-
const runOnFrame = model.runOnFrame;
97+
const detRof = model.runOnFrame;
98+
99+
const updateDetections = useCallback((results: Detection[]) => {
100+
setDetections(results);
101+
}, []);
77102

78103
const frameOutput = useFrameOutput({
79104
pixelFormat: 'rgb',
80105
dropFramesWhileBusy: true,
81106
onFrame: useCallback(
82107
(frame: Frame) => {
83108
'worklet';
84-
if (!runOnFrame) return;
85109
try {
86-
const scores = runOnFrame(frame);
87-
if (scores) {
88-
let best = '';
89-
let bestScore = -1;
90-
for (const [label, score] of Object.entries(scores)) {
91-
if ((score as number) > bestScore) {
92-
bestScore = score as number;
93-
best = label;
94-
}
95-
}
96-
scheduleOnRN(setTopLabel, best);
110+
if (!detRof) return;
111+
const isFrontCamera = false; // using back camera
112+
const result = detRof(frame, isFrontCamera, 0.5);
113+
if (result) {
114+
scheduleOnRN(updateDetections, result);
97115
}
98116
} finally {
99117
frame.dispose();
100118
}
101119
},
102-
[runOnFrame]
120+
[detRof, updateDetections]
103121
),
104122
});
105123

@@ -111,30 +129,80 @@ export default function App() {
111129
if (!device) return null;
112130

113131
return (
114-
<>
132+
<View style={styles.container}>
115133
<Camera
116-
style={styles.camera}
134+
style={StyleSheet.absoluteFill}
117135
device={device}
118136
outputs={[frameOutput]}
119137
isActive
138+
orientationSource="device"
120139
/>
121-
<Text style={styles.label}>{topLabel}</Text>
122-
</>
140+
{detections.map((det, i) => (
141+
<Text key={i} style={styles.label}>
142+
{det.label} {(det.score * 100).toFixed(1)}%
143+
</Text>
144+
))}
145+
</View>
123146
);
124147
}
125148

126149
const styles = StyleSheet.create({
127-
camera: { flex: 1 },
150+
container: { flex: 1 },
128151
label: {
129152
position: 'absolute',
130153
bottom: 40,
131154
alignSelf: 'center',
132155
color: 'white',
133-
fontSize: 20,
156+
fontSize: 16,
134157
},
135158
});
136159
```
137160

161+
For a complete example showing how to render bounding boxes, segmentation masks, OCR overlays, and style transfer results on top of the camera preview, see the [example app's VisionCamera tasks](https://github.com/software-mansion/react-native-executorch/tree/main/apps/computer-vision/components/vision_camera).
162+
163+
## Handling front/back camera
164+
165+
When switching between front and back cameras, you need to pass the correct `isFrontCamera` value to `runOnFrame`. Since worklets cannot read React state directly, use a `Synchronizable` from `react-native-worklets`:
166+
167+
```tsx
168+
import { createSynchronizable } from 'react-native-worklets';
169+
170+
// Create outside the component so it's stable across renders
171+
const cameraPositionSync = createSynchronizable<'front' | 'back'>('back');
172+
173+
export default function App() {
174+
const [cameraPosition, setCameraPosition] = useState<'front' | 'back'>(
175+
'back'
176+
);
177+
178+
// Keep the synchronizable in sync with React state
179+
useEffect(() => {
180+
cameraPositionSync.setBlocking(cameraPosition);
181+
}, [cameraPosition]);
182+
183+
const frameOutput = useFrameOutput({
184+
pixelFormat: 'rgb',
185+
dropFramesWhileBusy: true,
186+
onFrame: useCallback(
187+
(frame: Frame) => {
188+
'worklet';
189+
try {
190+
if (!runOnFrame) return;
191+
const isFrontCamera = cameraPositionSync.getDirty() === 'front';
192+
const result = runOnFrame(frame, isFrontCamera);
193+
// ... handle result
194+
} finally {
195+
frame.dispose();
196+
}
197+
},
198+
[runOnFrame]
199+
),
200+
});
201+
202+
// ...
203+
}
204+
```
205+
138206
## Using the Module API
139207

140208
If you use the TypeScript Module API (e.g. `ClassificationModule`) directly instead of a hook, `runOnFrame` is a worklet function and **cannot** be passed directly to `useState` — React would invoke it as a state initializer. Use the functional updater form `() => module.runOnFrame`:
@@ -153,6 +221,7 @@ export default function App() {
153221
const [runOnFrame, setRunOnFrame] = useState<typeof module.runOnFrame | null>(
154222
null
155223
);
224+
const [topLabel, setTopLabel] = useState('');
156225

157226
useEffect(() => {
158227
module.load(EFFICIENTNET_V2_S).then(() => {
@@ -170,8 +239,9 @@ export default function App() {
170239
'worklet';
171240
if (!runOnFrame) return;
172241
try {
173-
const result = runOnFrame(frame);
174-
if (result) scheduleOnRN(setResult, result);
242+
const isFrontCamera = false;
243+
const result = runOnFrame(frame, isFrontCamera);
244+
if (result) scheduleOnRN(setTopLabel, 'detected');
175245
} finally {
176246
frame.dispose();
177247
}
@@ -180,28 +250,45 @@ export default function App() {
180250
),
181251
});
182252

183-
return <Camera outputs={[frameOutput]} isActive device={device} />;
253+
return (
254+
<Camera
255+
outputs={[frameOutput]}
256+
isActive
257+
device={device}
258+
orientationSource="device"
259+
/>
260+
);
184261
}
185262
```
186263

187264
## Common issues
188265

189-
### Results look wrong or scrambled
266+
#### Bounding boxes or masks are rotated / misaligned
267+
268+
Make sure you have set `orientationSource="device"` on the `Camera` component. Without it, the frame orientation metadata won't match the actual device orientation, causing misaligned results.
269+
270+
Also verify that `enablePhysicalBufferRotation` is **not** set to `true` — this conflicts with the library's orientation handling.
271+
272+
#### Results look wrong or scrambled
190273

191274
You forgot to set `pixelFormat: 'rgb'`. The default VisionCamera pixel format is `yuv` — our frame extraction works only with RGB data.
192275

193-
### App freezes or camera drops frames
276+
#### Results are mirrored on front camera
277+
278+
You are not passing `isFrontCamera: true` when using the front camera. See [Handling front/back camera](#handling-frontback-camera) above.
279+
280+
#### App freezes or camera drops frames
194281

195282
Your model's inference time exceeds the frame interval. Enable `dropFramesWhileBusy: true` in `useFrameOutput`, or move inference off the worklet thread using VisionCamera's [async frame processing](https://react-native-vision-camera-v5-docs.vercel.app/docs/async-frame-processing).
196283

197-
### Memory leak / crash after many frames
284+
#### Memory leak / crash after many frames
198285

199286
You are not calling `frame.dispose()`. Always dispose the frame in a `finally` block.
200287

201-
### `runOnFrame` is always null
288+
#### `runOnFrame` is always null
202289

203290
The model hasn't finished loading yet. Guard with `if (!runOnFrame) return` inside `onFrame`, or check `model.isReady` before enabling the camera.
204291

205-
### TypeError: `module.runOnFrame` is not a function (Module API)
292+
#### TypeError: `module.runOnFrame` is not a function (Module API)
206293

207294
You passed `module.runOnFrame` directly to `setState` instead of `() => module.runOnFrame`. React invoked it as a state initializer — see the [Module API section](#using-the-module-api) above.

0 commit comments

Comments
 (0)