Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0eae3f7
fix: correct frame data extraction
NorbertKlockiewicz Feb 11, 2026
65667ad
feat: frame extractor for zero-copy approach
NorbertKlockiewicz Feb 12, 2026
daed38a
chore: num minSdkVersion to 26
NorbertKlockiewicz Feb 13, 2026
3d534de
feat: unify frame extraction and preprocessing
NorbertKlockiewicz Feb 16, 2026
9ce35da
feat: remove unused bindJSIMethods
NorbertKlockiewicz Feb 16, 2026
66af65c
feat: initial version of vision model API
NorbertKlockiewicz Feb 17, 2026
6e413ac
refactor: errors, logs, unnecessary comments, use existing TensorPtr
NorbertKlockiewicz Feb 17, 2026
53bcd96
fix: change Frame import in BaseModule
NorbertKlockiewicz Feb 17, 2026
cd0b123
feat: use TensorPtrish type for Pixel data input
NorbertKlockiewicz Feb 18, 2026
e001142
refactor: add or remove empty lines
NorbertKlockiewicz Feb 18, 2026
ca60d88
fix: errors after rebase
NorbertKlockiewicz Feb 19, 2026
62df7ce
fix: remove redundant preprocessing step
NorbertKlockiewicz Feb 20, 2026
962f1c3
refactor: changes suggested in review
NorbertKlockiewicz Feb 23, 2026
7753bd1
fix: not existing error type, add comments to JSI code
NorbertKlockiewicz Feb 23, 2026
a9c01a9
feat: add new PlatformNotSupported error
NorbertKlockiewicz Feb 23, 2026
98395af
fix: compilation JSI error
NorbertKlockiewicz Feb 23, 2026
ffcf72f
feat: add tests for generateFromPixels method
NorbertKlockiewicz Feb 23, 2026
44676fc
feat: add example screen with vision camera to computer vision app
NorbertKlockiewicz Feb 23, 2026
983242e
feat: suggested changes / improve comments
NorbertKlockiewicz Feb 24, 2026
e0e8bca
fix(android): object detection not working on android
NorbertKlockiewicz Feb 25, 2026
3aa0f89
chore: remove unused ImageSegmentation.cpp
NorbertKlockiewicz Feb 25, 2026
fd5aca7
docs: add correct api references
NorbertKlockiewicz Feb 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .cspell-wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,6 @@ antonov
rfdetr
basemodule
IMAGENET
worklet
worklets
BGRA
16 changes: 14 additions & 2 deletions apps/computer-vision/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,23 @@
"foregroundImage": "./assets/icons/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "com.anonymous.computervision"
"package": "com.anonymous.computervision",
"permissions": ["android.permission.CAMERA"]
},
"web": {
"favicon": "./assets/icons/favicon.png"
},
"plugins": ["expo-font", "expo-router"]
"plugins": [
"expo-font",
"expo-router",
[
"expo-build-properties",
{
"android": {
"minSdkVersion": 26
}
}
]
]
}
}
8 changes: 8 additions & 0 deletions apps/computer-vision/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ export default function _layout() {
headerTitleStyle: { color: ColorPalette.primary },
}}
/>
<Drawer.Screen
name="object_detection_live/index"
options={{
drawerLabel: 'Object Detection (Live)',
title: 'Object Detection (Live)',
headerTitleStyle: { color: ColorPalette.primary },
}}
/>
<Drawer.Screen
name="ocr/index"
options={{
Expand Down
6 changes: 6 additions & 0 deletions apps/computer-vision/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ export default function Home() {
>
<Text style={styles.buttonText}>Object Detection</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.button}
onPress={() => router.navigate('object_detection_live/')}
>
<Text style={styles.buttonText}>Object Detection Live</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.button}
onPress={() => router.navigate('ocr/')}
Expand Down
223 changes: 223 additions & 0 deletions apps/computer-vision/app/object_detection_live/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import React, {
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import {
StatusBar,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

import {
Camera,
getCameraFormat,
Templates,
useCameraDevices,
useCameraPermission,
useFrameOutput,
} from 'react-native-vision-camera';
import { scheduleOnRN } from 'react-native-worklets';
import {
Detection,
SSDLITE_320_MOBILENET_V3_LARGE,
useObjectDetection,
} from 'react-native-executorch';
import { GeneratingContext } from '../../context';
import Spinner from '../../components/Spinner';
import ColorPalette from '../../colors';

export default function ObjectDetectionLiveScreen() {
const insets = useSafeAreaInsets();

const model = useObjectDetection({ model: SSDLITE_320_MOBILENET_V3_LARGE });
const { setGlobalGenerating } = useContext(GeneratingContext);

useEffect(() => {
setGlobalGenerating(model.isGenerating);
}, [model.isGenerating, setGlobalGenerating]);

const [detectionCount, setDetectionCount] = useState(0);
const [fps, setFps] = useState(0);
const lastFrameTimeRef = useRef(Date.now());

const cameraPermission = useCameraPermission();
const devices = useCameraDevices();
const device = devices.find((d) => d.position === 'back') ?? devices[0];

const format = useMemo(() => {
if (device == null) return undefined;
try {
return getCameraFormat(device, Templates.FrameProcessing);
} catch {
return undefined;
}
}, [device]);

const updateStats = useCallback((results: Detection[]) => {
setDetectionCount(results.length);
const now = Date.now();
const timeDiff = now - lastFrameTimeRef.current;
if (timeDiff > 0) {
setFps(Math.round(1000 / timeDiff));
}
lastFrameTimeRef.current = now;
}, []);

const frameOutput = useFrameOutput({
pixelFormat: 'rgb',
dropFramesWhileBusy: true,
onFrame(frame) {
'worklet';
if (!model.runOnFrame) {
frame.dispose();
return;
}
try {
const result = model.runOnFrame(frame, 0.5);
if (result) {
scheduleOnRN(updateStats, result);
}
} catch {
// ignore frame errors
} finally {
frame.dispose();
}
},
});

if (!model.isReady) {
return (
<Spinner
visible={!model.isReady}
textContent={`Loading the model ${(model.downloadProgress * 100).toFixed(0)} %`}
/>
);
}

if (!cameraPermission.hasPermission) {
return (
<View style={styles.centered}>
<Text style={styles.message}>Camera access needed</Text>
<TouchableOpacity
onPress={() => cameraPermission.requestPermission()}
style={styles.button}
>
<Text style={styles.buttonText}>Grant Permission</Text>
</TouchableOpacity>
</View>
);
}

if (device == null) {
return (
<View style={styles.centered}>
<Text style={styles.message}>No camera device found</Text>
</View>
);
}

return (
<View style={styles.container}>
<StatusBar barStyle="light-content" translucent />

<Camera
style={StyleSheet.absoluteFill}
device={device}
outputs={[frameOutput]}
isActive={true}
format={format}
/>

<View
style={[styles.bottomBarWrapper, { paddingBottom: insets.bottom + 12 }]}
pointerEvents="none"
>
<View style={styles.bottomBar}>
<View style={styles.statItem}>
<Text style={styles.statValue}>{detectionCount}</Text>
<Text style={styles.statLabel}>objects</Text>
</View>
<View style={styles.statDivider} />
<View style={styles.statItem}>
<Text style={styles.statValue}>{fps}</Text>
<Text style={styles.statLabel}>fps</Text>
</View>
</View>
</View>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'black',
},
centered: {
flex: 1,
backgroundColor: 'black',
justifyContent: 'center',
alignItems: 'center',
gap: 16,
},
message: {
color: 'white',
fontSize: 18,
},
button: {
paddingHorizontal: 24,
paddingVertical: 12,
backgroundColor: ColorPalette.primary,
borderRadius: 24,
},
buttonText: {
color: 'white',
fontSize: 15,
fontWeight: '600',
letterSpacing: 0.3,
},
bottomBarWrapper: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
alignItems: 'center',
},
bottomBar: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.55)',
borderRadius: 24,
paddingHorizontal: 28,
paddingVertical: 10,
gap: 24,
},
statItem: {
alignItems: 'center',
},
statValue: {
color: 'white',
fontSize: 22,
fontWeight: '700',
letterSpacing: -0.5,
},
statLabel: {
color: 'rgba(255,255,255,0.55)',
fontSize: 11,
fontWeight: '500',
textTransform: 'uppercase',
letterSpacing: 0.8,
},
statDivider: {
width: 1,
height: 32,
backgroundColor: 'rgba(255,255,255,0.2)',
},
});
10 changes: 7 additions & 3 deletions apps/computer-vision/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@react-navigation/native": "^7.1.6",
"@shopify/react-native-skia": "2.2.12",
"expo": "^54.0.27",
"expo-build-properties": "~1.0.10",
"expo-constants": "~18.0.11",
"expo-font": "~14.0.10",
"expo-linking": "~8.0.10",
Expand All @@ -30,17 +31,20 @@
"react-native-gesture-handler": "~2.28.0",
"react-native-image-picker": "^7.2.2",
"react-native-loading-spinner-overlay": "^3.0.1",
"react-native-reanimated": "~4.1.1",
"react-native-nitro-image": "^0.12.0",
"react-native-nitro-modules": "^0.33.9",
"react-native-reanimated": "~4.2.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
"react-native-svg": "15.12.1",
"react-native-svg-transformer": "^1.5.0",
"react-native-worklets": "0.5.1"
"react-native-vision-camera": "5.0.0-beta.2",
"react-native-worklets": "^0.7.2"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@types/pngjs": "^6.0.5",
"@types/react": "~19.1.10"
"@types/react": "~19.2.0"
},
"private": true
}
Loading
Loading