Skip to content

Commit a06a8b5

Browse files
feat: add possibility to switch between front/back camera
1 parent 09b420f commit a06a8b5

File tree

2 files changed

+62
-12
lines changed
  • apps/computer-vision/app/vision_camera
  • packages/react-native-executorch/common/rnexecutorch/models

2 files changed

+62
-12
lines changed

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

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
Skia,
4343
SkImage,
4444
} from '@shopify/react-native-skia';
45+
import Svg, { Path, Polygon } from 'react-native-svg';
4546
import { GeneratingContext } from '../../context';
4647
import Spinner from '../../components/Spinner';
4748
import ColorPalette from '../../colors';
@@ -119,6 +120,9 @@ export default function VisionCameraScreen() {
119120
const [activeTask, setActiveTask] = useState<TaskId>('classification');
120121
const [activeModel, setActiveModel] = useState<ModelId>('classification');
121122
const [canvasSize, setCanvasSize] = useState({ width: 1, height: 1 });
123+
const [cameraPosition, setCameraPosition] = useState<'back' | 'front'>(
124+
'back'
125+
);
122126
const { setGlobalGenerating } = useContext(GeneratingContext);
123127

124128
const classification = useClassification({
@@ -149,7 +153,8 @@ export default function VisionCameraScreen() {
149153
const lastFrameTimeRef = useRef(Date.now());
150154
const cameraPermission = useCameraPermission();
151155
const devices = useCameraDevices();
152-
const device = devices.find((d) => d.position === 'back') ?? devices[0];
156+
const device =
157+
devices.find((d) => d.position === cameraPosition) ?? devices[0];
153158
const format = useMemo(() => {
154159
if (device == null) return undefined;
155160
try {
@@ -375,7 +380,10 @@ export default function VisionCameraScreen() {
375380
/>
376381

377382
<View
378-
style={StyleSheet.absoluteFill}
383+
style={[
384+
StyleSheet.absoluteFill,
385+
cameraPosition === 'front' && { transform: [{ scaleX: -1 }] },
386+
]}
379387
pointerEvents="none"
380388
onLayout={(e) =>
381389
setCanvasSize({
@@ -422,6 +430,9 @@ export default function VisionCameraScreen() {
422430
style={[
423431
styles.bboxLabel,
424432
{ backgroundColor: labelColorBg(det.label) },
433+
cameraPosition === 'front' && {
434+
transform: [{ scaleX: -1 }],
435+
},
425436
]}
426437
>
427438
<Text style={styles.bboxLabelText}>
@@ -518,6 +529,37 @@ export default function VisionCameraScreen() {
518529
))}
519530
</ScrollView>
520531
</View>
532+
533+
<View
534+
style={[styles.bottomOverlay, { paddingBottom: insets.bottom + 16 }]}
535+
pointerEvents="box-none"
536+
>
537+
<TouchableOpacity
538+
style={styles.flipButton}
539+
onPress={() =>
540+
setCameraPosition((p) => (p === 'back' ? 'front' : 'back'))
541+
}
542+
>
543+
<Svg width={28} height={28} viewBox="0 0 24 24" fill="none">
544+
{/* Camera body */}
545+
<Path
546+
d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"
547+
stroke="white"
548+
strokeWidth={1.8}
549+
strokeLinecap="round"
550+
strokeLinejoin="round"
551+
/>
552+
{/* Rotate arrows — arc with arrowhead around the lens */}
553+
<Path
554+
d="M9 13.5a3 3 0 1 0 3-3"
555+
stroke="white"
556+
strokeWidth={1.8}
557+
strokeLinecap="round"
558+
/>
559+
<Polygon points="8,11 9,13.5 11,12" fill="white" />
560+
</Svg>
561+
</TouchableOpacity>
562+
</View>
521563
</View>
522564
);
523565
}
@@ -662,4 +704,21 @@ const styles = StyleSheet.create({
662704
textShadowOffset: { width: 0, height: 1 },
663705
textShadowRadius: 6,
664706
},
707+
bottomOverlay: {
708+
position: 'absolute',
709+
bottom: 0,
710+
left: 0,
711+
right: 0,
712+
alignItems: 'center',
713+
},
714+
flipButton: {
715+
width: 56,
716+
height: 56,
717+
borderRadius: 28,
718+
backgroundColor: 'rgba(255,255,255,0.2)',
719+
justifyContent: 'center',
720+
alignItems: 'center',
721+
borderWidth: 1.5,
722+
borderColor: 'rgba(255,255,255,0.4)',
723+
},
665724
});

packages/react-native-executorch/common/rnexecutorch/models/VisionModel.cpp

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,7 @@ using namespace facebook;
1111
cv::Mat VisionModel::extractFromFrame(jsi::Runtime &runtime,
1212
const jsi::Value &frameData) const {
1313
auto frameObj = frameData.asObject(runtime);
14-
cv::Mat frame = ::rnexecutorch::utils::extractFrame(runtime, frameObj);
15-
16-
// Camera sensors natively deliver frames in landscape orientation.
17-
// Rotate 90° CW so all models receive upright portrait frames.
18-
if (frame.cols > frame.rows) {
19-
cv::Mat upright;
20-
cv::rotate(frame, upright, cv::ROTATE_90_CLOCKWISE);
21-
return upright;
22-
}
23-
return frame;
14+
return ::rnexecutorch::utils::extractFrame(runtime, frameObj);
2415
}
2516

2617
cv::Mat VisionModel::extractFromPixels(const JSTensorViewIn &tensorView) const {

0 commit comments

Comments
 (0)