Skip to content

Commit 093f5c5

Browse files
docs: update vision camera docs
1 parent db536d1 commit 093f5c5

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
@@ -37,10 +37,12 @@ Use `runOnFrame` when you need to process every camera frame. Use `forward` for
3737

3838
## How it works
3939

40-
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.
40+
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.
41+
42+
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.
4143

4244
:::warning
43-
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.
45+
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.
4446
:::
4547

4648
:::warning
@@ -51,11 +53,27 @@ You **must** set `pixelFormat: 'rgb'` in `useFrameOutput`. Our extraction pipeli
5153
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.
5254
:::
5355

54-
## Full example (Classification)
56+
## Camera configuration
57+
58+
The `Camera` component requires specific props for correct orientation handling:
59+
60+
```tsx
61+
<Camera
62+
device={device}
63+
outputs={[frameOutput]}
64+
isActive
65+
orientationSource="device"
66+
/>
67+
```
68+
69+
- **`orientationSource="device"`** — ensures frame orientation metadata reflects the physical device orientation, which the library uses to rotate model inputs and outputs correctly.
70+
- **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.
71+
72+
## Full example (Object Detection)
5573

5674
```tsx
57-
import { useState, useCallback } from 'react';
58-
import { Text, StyleSheet } from 'react-native';
75+
import { useState, useCallback, useRef } from 'react';
76+
import { StyleSheet, View, Text } from 'react-native';
5977
import {
6078
Camera,
6179
Frame,
@@ -64,43 +82,43 @@ import {
6482
useFrameOutput,
6583
} from 'react-native-vision-camera';
6684
import { scheduleOnRN } from 'react-native-worklets';
67-
import { useClassification, EFFICIENTNET_V2_S } from 'react-native-executorch';
85+
import {
86+
Detection,
87+
useObjectDetection,
88+
SSDLITE_320_MOBILENET_V3_LARGE,
89+
} from 'react-native-executorch';
6890

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

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

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

@@ -112,30 +130,80 @@ export default function App() {
112130
if (!device) return null;
113131

114132
return (
115-
<>
133+
<View style={styles.container}>
116134
<Camera
117-
style={styles.camera}
135+
style={StyleSheet.absoluteFill}
118136
device={device}
119137
outputs={[frameOutput]}
120138
isActive
139+
orientationSource="device"
121140
/>
122-
<Text style={styles.label}>{topLabel}</Text>
123-
</>
141+
{detections.map((det, i) => (
142+
<Text key={i} style={styles.label}>
143+
{det.label} {(det.score * 100).toFixed(1)}%
144+
</Text>
145+
))}
146+
</View>
124147
);
125148
}
126149

127150
const styles = StyleSheet.create({
128-
camera: { flex: 1 },
151+
container: { flex: 1 },
129152
label: {
130153
position: 'absolute',
131154
bottom: 40,
132155
alignSelf: 'center',
133156
color: 'white',
134-
fontSize: 20,
157+
fontSize: 16,
135158
},
136159
});
137160
```
138161

162+
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).
163+
164+
## Handling front/back camera
165+
166+
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`:
167+
168+
```tsx
169+
import { createSynchronizable } from 'react-native-worklets';
170+
171+
// Create outside the component so it's stable across renders
172+
const cameraPositionSync = createSynchronizable<'front' | 'back'>('back');
173+
174+
export default function App() {
175+
const [cameraPosition, setCameraPosition] = useState<'front' | 'back'>(
176+
'back'
177+
);
178+
179+
// Keep the synchronizable in sync with React state
180+
useEffect(() => {
181+
cameraPositionSync.setBlocking(cameraPosition);
182+
}, [cameraPosition]);
183+
184+
const frameOutput = useFrameOutput({
185+
pixelFormat: 'rgb',
186+
dropFramesWhileBusy: true,
187+
onFrame: useCallback(
188+
(frame: Frame) => {
189+
'worklet';
190+
try {
191+
if (!runOnFrame) return;
192+
const isFrontCamera = cameraPositionSync.getDirty() === 'front';
193+
const result = runOnFrame(frame, isFrontCamera);
194+
// ... handle result
195+
} finally {
196+
frame.dispose();
197+
}
198+
},
199+
[runOnFrame]
200+
),
201+
});
202+
203+
// ...
204+
}
205+
```
206+
139207
## Using the Module API
140208

141209
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`:
@@ -154,6 +222,7 @@ export default function App() {
154222
const [runOnFrame, setRunOnFrame] = useState<typeof module.runOnFrame | null>(
155223
null
156224
);
225+
const [topLabel, setTopLabel] = useState('');
157226

158227
useEffect(() => {
159228
module.load(EFFICIENTNET_V2_S).then(() => {
@@ -171,8 +240,9 @@ export default function App() {
171240
'worklet';
172241
if (!runOnFrame) return;
173242
try {
174-
const result = runOnFrame(frame);
175-
if (result) scheduleOnRN(setResult, result);
243+
const isFrontCamera = false;
244+
const result = runOnFrame(frame, isFrontCamera);
245+
if (result) scheduleOnRN(setTopLabel, 'detected');
176246
} finally {
177247
frame.dispose();
178248
}
@@ -181,28 +251,45 @@ export default function App() {
181251
),
182252
});
183253

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

188265
## Common issues
189266

190-
### Results look wrong or scrambled
267+
#### Bounding boxes or masks are rotated / misaligned
268+
269+
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.
270+
271+
Also verify that `enablePhysicalBufferRotation` is **not** set to `true` — this conflicts with the library's orientation handling.
272+
273+
#### Results look wrong or scrambled
191274

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

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

196283
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).
197284

198-
### Memory leak / crash after many frames
285+
#### Memory leak / crash after many frames
199286

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

202-
### `runOnFrame` is always null
289+
#### `runOnFrame` is always null
203290

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

206-
### TypeError: `module.runOnFrame` is not a function (Module API)
293+
#### TypeError: `module.runOnFrame` is not a function (Module API)
207294

208295
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)