-
Notifications
You must be signed in to change notification settings - Fork 83
[RNE Rewrite] chore: clean up and unify example app layout, styling, and fix dependencies #1273
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: rne-rewrite
Are you sure you want to change the base?
Changes from all commits
c311d48
928154d
caf1337
1aef571
1e0dc48
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -229,3 +229,4 @@ imgproc | |
| c10 | ||
| probas | ||
| Probas | ||
| Skia | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,20 +1,15 @@ | ||
| import React, { useState } from 'react'; | ||
| import { | ||
| View, | ||
| Text, | ||
| StyleSheet, | ||
| Image, | ||
| TouchableOpacity, | ||
| ScrollView, | ||
| ActivityIndicator, | ||
| Platform, | ||
| } from 'react-native'; | ||
| import { Skia } from '@shopify/react-native-skia'; | ||
| import { View, Text, StyleSheet, ScrollView, Platform } from 'react-native'; | ||
| import { commonStyles, ColorPalette } from '../../theme'; | ||
| import { type SkImage as SkiaImageType } from '@shopify/react-native-skia'; | ||
| import { useClassifier, models } from 'react-native-executorch'; | ||
| import ScreenWrapper from '../../ScreenWrapper'; | ||
| import ColorPalette from '../../colors'; | ||
| import { getImage } from '../../utils'; | ||
| import ScreenWrapper from '../../components/ScreenWrapper'; | ||
| import { getImage, loadSkImage } from '../../utils'; | ||
| import { ModelPicker, type ModelOption } from '../../components/ModelPicker'; | ||
| import { ImageViewport } from '../../components/ImageViewport'; | ||
| import { ModelStatus } from '../../components/ModelStatus'; | ||
| import { LatencyIndicator } from '../../components/LatencyIndicator'; | ||
| import { Button } from '../../components/Button'; | ||
|
|
||
| const MODEL_OPTIONS: ModelOption[] = [ | ||
| { | ||
|
|
@@ -32,25 +27,10 @@ const MODEL_OPTIONS: ModelOption[] = [ | |
| }, | ||
| ]; | ||
|
|
||
| async function loadImageBuffer(uri: string) { | ||
| const data = await Skia.Data.fromURI(uri); | ||
| const img = Skia.Image.MakeImageFromEncoded(data); | ||
| if (!img) { | ||
| throw new Error('Failed to decode image using Skia'); | ||
| } | ||
| return { | ||
| data: img.readPixels() as Uint8Array, | ||
| width: img.width(), | ||
| height: img.height(), | ||
| format: 'rgba' as const, | ||
| layout: 'hwc' as const, | ||
| }; | ||
| } | ||
|
|
||
| export default function ClassificationScreen() { | ||
| function ClassificationContent() { | ||
| const [selectedModel, setSelectedModel] = useState<any>(MODEL_OPTIONS[0].value); | ||
| const [imageUri, setImageUri] = useState<string | null>(null); | ||
| const [loading, setLoading] = useState(false); | ||
| const [skiaImage, setSkiaImage] = useState<SkiaImageType | null>(null); | ||
| const [isProcessing, setIsProcessing] = useState(false); | ||
| const [results, setResults] = useState<{ label: string; confidence: number }[]>([]); | ||
| const [latency, setLatency] = useState<number | null>(null); | ||
| const [error, setError] = useState<string | null>(null); | ||
|
|
@@ -66,226 +46,120 @@ export default function ClassificationScreen() { | |
| const handlePickImage = async (useCamera: boolean) => { | ||
| const asset = await getImage(useCamera); | ||
| if (asset?.uri) { | ||
| setImageUri(asset.uri); | ||
| const img = await loadSkImage(asset.uri); | ||
| setSkiaImage(img); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| setResults([]); | ||
| setLatency(null); | ||
| setError(null); | ||
| } | ||
| }; | ||
|
|
||
| const runClassification = async (sync: boolean) => { | ||
| if (!imageUri || !classify || !classifyWorklet) return; | ||
| if (!sync) setLoading(true); | ||
| if (!skiaImage || !classify || !classifyWorklet) return; | ||
| if (!sync) setIsProcessing(true); | ||
| setError(null); | ||
| try { | ||
| const inputBuffer = await loadImageBuffer(imageUri); | ||
| const buffer = { | ||
| data: skiaImage.readPixels() as Uint8Array, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| width: skiaImage.width(), | ||
| height: skiaImage.height(), | ||
| format: 'rgba' as const, | ||
| layout: 'hwc' as const, | ||
| }; | ||
| const start = Date.now(); | ||
| const output = sync | ||
| ? classifyWorklet(inputBuffer, { topk: 5 }) | ||
| : await classify(inputBuffer, { topk: 5 }); | ||
| ? classifyWorklet(buffer, { topk: 5 }) | ||
| : await classify(buffer, { topk: 5 }); | ||
|
|
||
| setLatency(Date.now() - start); | ||
| setResults(output); | ||
| } catch (e: any) { | ||
| setError(e.message || String(e)); | ||
| } finally { | ||
| if (!sync) setLoading(false); | ||
| if (!sync) setIsProcessing(false); | ||
| } | ||
| }; | ||
|
|
||
| const activeError = loadError ? String(loadError) : error; | ||
|
|
||
| return ( | ||
| <ScreenWrapper> | ||
| <ScrollView contentContainerStyle={styles.container}> | ||
| <Text style={styles.title}>Image Classification</Text> | ||
|
|
||
| <ModelPicker | ||
| label="Model" | ||
| options={MODEL_OPTIONS} | ||
| selectedValue={selectedModel} | ||
| onValueChange={(model) => { | ||
| setSelectedModel(model); | ||
| setResults([]); | ||
| setLatency(null); | ||
| setError(null); | ||
| }} | ||
| <ScrollView | ||
| style={commonStyles.container} | ||
| contentContainerStyle={commonStyles.contentContainer} | ||
| > | ||
| <Text style={commonStyles.description}> | ||
| Upload or capture an image to identify objects using a classifier. | ||
| </Text> | ||
|
|
||
| <ModelPicker | ||
| label="Model" | ||
| options={MODEL_OPTIONS} | ||
| selectedValue={selectedModel} | ||
| onValueChange={(model) => { | ||
| setSelectedModel(model); | ||
| setResults([]); | ||
| setLatency(null); | ||
| setError(null); | ||
| }} | ||
| /> | ||
|
|
||
| <ModelStatus | ||
| isReady={isReady} | ||
| downloadProgress={downloadProgress} | ||
| error={activeError} | ||
| modelTypeLabel="classification model" | ||
| /> | ||
|
|
||
| <ImageViewport skiaImage={skiaImage} onPressPlaceholder={() => handlePickImage(false)} /> | ||
|
|
||
| <View style={commonStyles.buttonRow}> | ||
| <Button title="Gallery" onPress={() => handlePickImage(false)} variant="secondary" /> | ||
| <Button title="Camera" onPress={() => handlePickImage(true)} variant="secondary" /> | ||
| </View> | ||
|
|
||
| <View style={commonStyles.buttonRow}> | ||
| <Button | ||
| title="Run Async" | ||
| onPress={() => runClassification(false)} | ||
| disabled={!skiaImage || !isReady || isProcessing} | ||
| loading={isProcessing} | ||
| /> | ||
|
|
||
| {!isReady && !activeError && ( | ||
| <View style={styles.progressContainer}> | ||
| <ActivityIndicator size="small" color={ColorPalette.primary} /> | ||
| <Text style={styles.progressText}> | ||
| Downloading Model... {downloadProgress ? `${Math.round(downloadProgress)}%` : '0%'} | ||
| </Text> | ||
| </View> | ||
| )} | ||
|
|
||
| {activeError && ( | ||
| <View style={styles.errorContainer}> | ||
| <Text style={styles.errorText}>{activeError}</Text> | ||
| </View> | ||
| )} | ||
|
|
||
| <View style={styles.imageCard}> | ||
| {imageUri ? ( | ||
| <Image source={{ uri: imageUri }} style={styles.image} resizeMode="contain" /> | ||
| ) : ( | ||
| <View style={styles.imagePlaceholder}> | ||
| <Text style={styles.placeholderText}>No image selected</Text> | ||
| <Button | ||
| title="Run Sync" | ||
| onPress={() => runClassification(true)} | ||
| disabled={!skiaImage || !isReady || isProcessing} | ||
| variant="accent" | ||
| /> | ||
| </View> | ||
|
|
||
| <LatencyIndicator latency={latency} /> | ||
|
|
||
| {results.length > 0 && ( | ||
| <View style={styles.resultsContainer}> | ||
| <Text style={styles.resultsTitle}>Results</Text> | ||
| {results.map((res, idx) => ( | ||
| <View key={idx} style={styles.resultRow}> | ||
| <Text style={styles.resultLabel} numberOfLines={1}> | ||
| {res.label} | ||
| </Text> | ||
| <Text style={styles.resultConfidence}>{Math.round(res.confidence * 100)}%</Text> | ||
| </View> | ||
| )} | ||
| </View> | ||
|
|
||
| <View style={styles.buttonRow}> | ||
| <TouchableOpacity style={styles.btnSecondary} onPress={() => handlePickImage(false)}> | ||
| <Text style={styles.btnTextSecondary}>Gallery</Text> | ||
| </TouchableOpacity> | ||
| <TouchableOpacity style={styles.btnSecondary} onPress={() => handlePickImage(true)}> | ||
| <Text style={styles.btnTextSecondary}>Camera</Text> | ||
| </TouchableOpacity> | ||
| </View> | ||
|
|
||
| <View style={styles.buttonRow}> | ||
| <TouchableOpacity | ||
| style={[styles.btnPrimary, (!imageUri || !isReady || loading) && styles.btnDisabled]} | ||
| onPress={() => runClassification(false)} | ||
| disabled={!imageUri || !isReady || loading} | ||
| > | ||
| {loading ? ( | ||
| <ActivityIndicator size="small" color="#fff" /> | ||
| ) : ( | ||
| <Text style={styles.btnTextPrimary}>Run Async</Text> | ||
| )} | ||
| </TouchableOpacity> | ||
| <TouchableOpacity | ||
| style={[styles.btnSecondary, (!imageUri || !isReady || loading) && styles.btnDisabled]} | ||
| onPress={() => runClassification(true)} | ||
| disabled={!imageUri || !isReady || loading} | ||
| > | ||
| <Text style={styles.btnTextSecondary}>Run Sync</Text> | ||
| </TouchableOpacity> | ||
| ))} | ||
| </View> | ||
| )} | ||
| </ScrollView> | ||
| ); | ||
| } | ||
|
|
||
| {latency !== null && ( | ||
| <Text style={styles.latencyText}>Inference Latency: {latency} ms</Text> | ||
| )} | ||
|
|
||
| {results.length > 0 && ( | ||
| <View style={styles.resultsContainer}> | ||
| <Text style={styles.resultsTitle}>Results</Text> | ||
| {results.map((res, idx) => ( | ||
| <View key={idx} style={styles.resultRow}> | ||
| <Text style={styles.resultLabel} numberOfLines={1}> | ||
| {res.label} | ||
| </Text> | ||
| <Text style={styles.resultConfidence}>{Math.round(res.confidence * 100)}%</Text> | ||
| </View> | ||
| ))} | ||
| </View> | ||
| )} | ||
| </ScrollView> | ||
| export default function ClassificationScreen() { | ||
| return ( | ||
| <ScreenWrapper> | ||
| <ClassificationContent /> | ||
| </ScreenWrapper> | ||
| ); | ||
| } | ||
|
|
||
| const styles = StyleSheet.create({ | ||
| container: { | ||
| padding: 16, | ||
| alignItems: 'center', | ||
| }, | ||
| title: { | ||
| fontSize: 22, | ||
| fontWeight: '700', | ||
| color: ColorPalette.strongPrimary, | ||
| marginBottom: 16, | ||
| }, | ||
| progressContainer: { | ||
| flexDirection: 'row', | ||
| alignItems: 'center', | ||
| marginVertical: 12, | ||
| gap: 8, | ||
| }, | ||
| progressText: { | ||
| fontSize: 14, | ||
| color: '#666', | ||
| }, | ||
| errorContainer: { | ||
| backgroundColor: '#ffe3e3', | ||
| padding: 12, | ||
| borderRadius: 8, | ||
| marginVertical: 8, | ||
| alignSelf: 'stretch', | ||
| }, | ||
| errorText: { | ||
| color: '#d63031', | ||
| fontSize: 14, | ||
| textAlign: 'center', | ||
| }, | ||
| imageCard: { | ||
| width: '100%', | ||
| height: 250, | ||
| backgroundColor: '#f1f3f5', | ||
| borderRadius: 12, | ||
| marginVertical: 16, | ||
| overflow: 'hidden', | ||
| borderWidth: 1, | ||
| borderColor: '#e9ecef', | ||
| justifyContent: 'center', | ||
| alignItems: 'center', | ||
| }, | ||
| image: { | ||
| width: '100%', | ||
| height: '100%', | ||
| }, | ||
| imagePlaceholder: { | ||
| justifyContent: 'center', | ||
| alignItems: 'center', | ||
| }, | ||
| placeholderText: { | ||
| color: '#868e96', | ||
| fontSize: 14, | ||
| }, | ||
| buttonRow: { | ||
| flexDirection: 'row', | ||
| gap: 8, | ||
| width: '100%', | ||
| marginBottom: 12, | ||
| }, | ||
| btnPrimary: { | ||
| flex: 1, | ||
| backgroundColor: ColorPalette.primary, | ||
| paddingVertical: 12, | ||
| borderRadius: 8, | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| }, | ||
| btnSecondary: { | ||
| flex: 1, | ||
| borderWidth: 1, | ||
| borderColor: ColorPalette.primary, | ||
| paddingVertical: 12, | ||
| borderRadius: 8, | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| }, | ||
| btnDisabled: { | ||
| opacity: 0.5, | ||
| }, | ||
| btnTextPrimary: { | ||
| color: '#fff', | ||
| fontSize: 15, | ||
| fontWeight: '600', | ||
| }, | ||
| btnTextSecondary: { | ||
| color: ColorPalette.primary, | ||
| fontSize: 15, | ||
| fontWeight: '600', | ||
| }, | ||
| latencyText: { | ||
| fontSize: 14, | ||
| color: '#666', | ||
| marginVertical: 12, | ||
| }, | ||
| resultsContainer: { | ||
| width: '100%', | ||
| backgroundColor: '#fff', | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
loadSkImagecan reject (manipulate/decode failure, or the explicitthrow), buthandlePickImagehas no try/catch — the error escapes uncaught instead of reaching thesetErrorUI. Previously decoding happened insiderunClassification's try/catch. Wrap this in try/catch andsetError.