| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689 |
- ===== Folder Structure =====
- Folder PATH listing for volume New Volume
- Volume serial number is 36B1-447D
- E:\TASK\RESEARCH AND DEVELOPMENT\PALM-OIL-AI\MOBILE\SRC
- | App.tsx
- |
- +---components
- | DetectionOverlay.tsx
- | TallyDashboard.tsx
- |
- +---hooks
- +---navigation
- | AppNavigator.tsx
- |
- +---screens
- | DashboardScreen.tsx
- | GalleryAnalysisScreen.tsx
- | HistoryScreen.tsx
- | ScannerScreen.tsx
- |
- +---theme
- | index.ts
- |
- \---utils
- storage.ts
- yoloParser.ts
-
-
- ==================================================
- FILE: E:\Task\Research and Development\palm-oil-ai\mobile\src\App.tsx
- ==================================================
- import React from 'react';
- import { NavigationContainer } from '@react-navigation/native';
- import { AppNavigator } from './navigation/AppNavigator';
- export default function App() {
- return (
- <NavigationContainer>
- <AppNavigator />
- </NavigationContainer>
- );
- }
-
-
- ==================================================
- FILE: E:\Task\Research and Development\palm-oil-ai\mobile\src\components\DetectionOverlay.tsx
- ==================================================
- import React from 'react';
- import { View, StyleSheet, Text } from 'react-native';
- import Animated, { useAnimatedStyle } from 'react-native-reanimated';
- import { Colors } from '../theme';
- import { BoundingBox } from '../utils/yoloParser';
- interface DetectionOverlayProps {
- detections: BoundingBox[];
- containerWidth?: number;
- containerHeight?: number;
- }
- export const DetectionOverlay: React.FC<DetectionOverlayProps> = ({ detections, containerWidth, containerHeight }) => {
- return (
- <View style={StyleSheet.absoluteFill}>
- {detections.map((det) => {
- const x = containerWidth ? det.relX * containerWidth : det.x;
- const y = containerHeight ? det.relY * containerHeight : det.y;
- const width = containerWidth ? det.relWidth * containerWidth : det.width;
- const height = containerHeight ? det.relHeight * containerHeight : det.height;
- return (
- <View
- key={det.id}
- style={[
- styles.box,
- {
- left: x,
- top: y,
- width: width,
- height: height,
- borderColor: Colors.classes[det.classId as keyof typeof Colors.classes] || Colors.text,
- }
- ]}
- >
- <View style={[
- styles.labelContainer,
- { backgroundColor: Colors.classes[det.classId as keyof typeof Colors.classes] || Colors.text }
- ]}>
- <Text style={styles.labelText}>
- {det.label} ({Math.round(det.confidence * 100)}%)
- </Text>
- </View>
- </View>
- );
- })}
- </View>
- );
- };
- const styles = StyleSheet.create({
- box: {
- position: 'absolute',
- borderWidth: 2,
- borderRadius: 4,
- },
- labelContainer: {
- position: 'absolute',
- top: -24,
- left: -2,
- paddingHorizontal: 6,
- paddingVertical: 2,
- borderRadius: 4,
- },
- labelText: {
- color: '#FFF',
- fontSize: 12,
- fontWeight: 'bold',
- }
- });
-
-
- ==================================================
- FILE: E:\Task\Research and Development\palm-oil-ai\mobile\src\components\TallyDashboard.tsx
- ==================================================
- import React from 'react';
- import { View, StyleSheet, Text } from 'react-native';
- import { Colors, Typography } from '../theme';
- interface TallyCounts {
- [key: string]: number;
- }
- interface TallyDashboardProps {
- counts: TallyCounts;
- }
- export const TallyDashboard: React.FC<TallyDashboardProps> = ({ counts }) => {
- const classNames = [
- 'Empty_Bunch',
- 'Underripe',
- 'Abnormal',
- 'Ripe',
- 'Unripe',
- 'Overripe'
- ];
- return (
- <View style={styles.container}>
- {classNames.map((name, index) => (
- <View key={name} style={styles.item}>
- <Text style={[styles.count, { color: Colors.classes[index as keyof typeof Colors.classes] }]}>
- {counts[name] || 0}
- </Text>
- <Text style={styles.label}>{name}</Text>
- </View>
- ))}
- </View>
- );
- };
- const styles = StyleSheet.create({
- container: {
- flexDirection: 'row',
- flexWrap: 'wrap',
- backgroundColor: 'rgba(15, 23, 42, 0.8)',
- padding: 12,
- borderRadius: 12,
- margin: 16,
- position: 'absolute',
- bottom: 40,
- left: 0,
- right: 0,
- justifyContent: 'space-around',
- borderWidth: 1,
- borderColor: 'rgba(255, 255, 255, 0.1)',
- },
- item: {
- alignItems: 'center',
- minWidth: '30%',
- marginVertical: 4,
- },
- count: {
- fontSize: 18,
- fontWeight: 'bold',
- },
- label: {
- fontSize: 10,
- color: Colors.textSecondary,
- marginTop: 2,
- textTransform: 'uppercase',
- }
- });
-
-
- ==================================================
- FILE: E:\Task\Research and Development\palm-oil-ai\mobile\src\navigation\AppNavigator.tsx
- ==================================================
- import React from 'react';
- import { createNativeStackNavigator } from '@react-navigation/native-stack';
- import { DashboardScreen } from '../screens/DashboardScreen';
- import { ScannerScreen } from '../screens/ScannerScreen';
- import { HistoryScreen } from '../screens/HistoryScreen';
- import { GalleryAnalysisScreen } from '../screens/GalleryAnalysisScreen';
- import { Colors } from '../theme';
- const Stack = createNativeStackNavigator();
- export const AppNavigator = () => {
- return (
- <Stack.Navigator
- initialRouteName="Dashboard"
- screenOptions={{
- headerStyle: {
- backgroundColor: Colors.background,
- },
- headerTintColor: '#FFF',
- headerTitleStyle: {
- fontWeight: 'bold',
- },
- headerShadowVisible: false,
- }}
- >
- <Stack.Screen
- name="Dashboard"
- component={DashboardScreen}
- options={{ headerShown: false }}
- />
- <Stack.Screen
- name="Scanner"
- component={ScannerScreen}
- options={{
- title: 'Industrial Scanner',
- headerTransparent: true,
- headerTitleStyle: { color: '#FFF' }
- }}
- />
- <Stack.Screen
- name="History"
- component={HistoryScreen}
- options={{
- title: 'Field Journal',
- headerLargeTitle: true,
- }}
- />
- <Stack.Screen
- name="GalleryAnalysis"
- component={GalleryAnalysisScreen}
- options={{
- headerShown: false,
- }}
- />
- </Stack.Navigator>
- );
- };
-
-
- ==================================================
- FILE: E:\Task\Research and Development\palm-oil-ai\mobile\src\screens\DashboardScreen.tsx
- ==================================================
- import React from 'react';
- import { StyleSheet, View, Text, TouchableOpacity, SafeAreaView, StatusBar, ScrollView } from 'react-native';
- import { Scan, Image as ImageIcon, History, ShieldAlert } from 'lucide-react-native';
- import { Colors } from '../theme';
- export const DashboardScreen = ({ navigation }: any) => {
- return (
- <SafeAreaView style={styles.container}>
- <StatusBar barStyle="light-content" />
-
- <ScrollView
- contentContainerStyle={styles.scrollContent}
- showsVerticalScrollIndicator={false}
- >
- <View style={styles.header}>
- <Text style={styles.title}>Palm Oil AI</Text>
- <Text style={styles.subtitle}>Industrial Management Hub</Text>
- </View>
- <View style={styles.grid}>
- <TouchableOpacity
- style={styles.card}
- onPress={() => navigation.navigate('Scanner')}
- >
- <View style={[styles.iconContainer, { backgroundColor: 'rgba(52, 199, 89, 0.1)' }]}>
- <Scan color={Colors.success} size={32} />
- </View>
- <Text style={styles.cardTitle}>Live Field Scan</Text>
- <Text style={styles.cardDesc}>Real-time ripeness detection & health alerts</Text>
- </TouchableOpacity>
- <TouchableOpacity
- style={styles.card}
- onPress={() => navigation.navigate('GalleryAnalysis')}
- >
- <View style={[styles.iconContainer, { backgroundColor: 'rgba(0, 122, 255, 0.1)' }]}>
- <ImageIcon color={Colors.info} size={32} />
- </View>
- <Text style={styles.cardTitle}>Analyze Gallery</Text>
- <Text style={styles.cardDesc}>Upload & analyze harvested bunches from storage</Text>
- </TouchableOpacity>
- <TouchableOpacity
- style={styles.card}
- onPress={() => navigation.navigate('History')}
- >
- <View style={[styles.iconContainer, { backgroundColor: 'rgba(148, 163, 184, 0.1)' }]}>
- <History color={Colors.textSecondary} size={32} />
- </View>
- <Text style={styles.cardTitle}>Detection History</Text>
- <Text style={styles.cardDesc}>Review past logs and industrial field journal</Text>
- </TouchableOpacity>
- <View style={[styles.card, styles.alertCard]}>
- <View style={[styles.iconContainer, { backgroundColor: 'rgba(255, 59, 48, 0.1)' }]}>
- <ShieldAlert color={Colors.error} size={32} />
- </View>
- <Text style={styles.cardTitle}>System Health</Text>
- <Text style={styles.cardDesc}>AI Inference: ACTIVE | Model: V11-INT8</Text>
- </View>
- </View>
- <View style={styles.footer}>
- <Text style={styles.versionText}>Industrial Suite v4.2.0-stable</Text>
- </View>
- </ScrollView>
- </SafeAreaView>
- );
- };
- const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: Colors.background,
- },
- scrollContent: {
- paddingBottom: 32,
- },
- header: {
- padding: 32,
- paddingTop: 48,
- },
- title: {
- color: '#FFF',
- fontSize: 32,
- fontWeight: 'bold',
- },
- subtitle: {
- color: Colors.textSecondary,
- fontSize: 16,
- marginTop: 4,
- },
- grid: {
- flex: 1,
- padding: 24,
- gap: 16,
- },
- card: {
- backgroundColor: Colors.surface,
- padding: 20,
- borderRadius: 20,
- borderWidth: 1,
- borderColor: 'rgba(255,255,255,0.05)',
- },
- alertCard: {
- borderColor: 'rgba(255, 59, 48, 0.2)',
- },
- iconContainer: {
- width: 64,
- height: 64,
- borderRadius: 16,
- justifyContent: 'center',
- alignItems: 'center',
- marginBottom: 16,
- },
- cardTitle: {
- color: '#FFF',
- fontSize: 18,
- fontWeight: 'bold',
- },
- cardDesc: {
- color: Colors.textSecondary,
- fontSize: 14,
- marginTop: 4,
- },
- footer: {
- padding: 24,
- alignItems: 'center',
- },
- versionText: {
- color: 'rgba(255,255,255,0.3)',
- fontSize: 12,
- fontWeight: '500',
- }
- });
-
-
- ==================================================
- FILE: E:\Task\Research and Development\palm-oil-ai\mobile\src\screens\GalleryAnalysisScreen.tsx
- ==================================================
- import React, { useState, useEffect } from 'react';
- import { StyleSheet, View, Text, Image, TouchableOpacity, SafeAreaView, ActivityIndicator, Alert, Dimensions } from 'react-native';
- import { useNavigation, useRoute } from '@react-navigation/native';
- import { launchImageLibrary } from 'react-native-image-picker';
- import { useTensorflowModel } from 'react-native-fast-tflite';
- import { ArrowLeft, Upload, CheckCircle2, History as HistoryIcon } from 'lucide-react-native';
- import { NativeModules } from 'react-native';
- const { PixelModule } = NativeModules;
- import { Colors } from '../theme';
- import { parseYoloResults, calculateTally, BoundingBox } from '../utils/yoloParser';
- import { saveDetectionRecord } from '../utils/storage';
- import { DetectionOverlay } from '../components/DetectionOverlay';
- const { width: SCREEN_WIDTH } = Dimensions.get('window');
- const base64ToUint8Array = (base64: string) => {
- if (!base64 || typeof base64 !== 'string') return new Uint8Array(0);
-
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
- const lookup = new Uint8Array(256);
- for (let i = 0; i < chars.length; i++) {
- lookup[chars.charCodeAt(i)] = i;
- }
- const len = base64.length;
- // Calculate buffer length (approximate is fine for Uint8Array assignment)
- const buffer = new Uint8Array(Math.floor((len * 3) / 4));
- let p = 0;
-
- for (let i = 0; i < len; i += 4) {
- const encoded1 = lookup[base64.charCodeAt(i)];
- const encoded2 = lookup[base64.charCodeAt(i + 1)];
- const encoded3 = lookup[base64.charCodeAt(i + 2)] || 0;
- const encoded4 = lookup[base64.charCodeAt(i + 3)] || 0;
- buffer[p++] = (encoded1 << 2) | (encoded2 >> 4);
- if (p < buffer.length) buffer[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
- if (p < buffer.length) buffer[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
- }
- return buffer;
- };
- // Removed manual base64ToUint8Array as we now use imageToRgb
- export const GalleryAnalysisScreen = () => {
- const navigation = useNavigation<any>();
- const route = useRoute<any>();
- const [imageUri, setImageUri] = useState<string | null>(null);
- const [fileName, setFileName] = useState<string | null>(null);
- const [isAnalyzing, setIsAnalyzing] = useState(false);
- const [detections, setDetections] = useState<BoundingBox[]>([]);
- const [counts, setCounts] = useState<Record<string, number>>({});
- const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
- const model = useTensorflowModel(require('../../assets/best.tflite'));
- useEffect(() => {
- if (route.params?.imageUri) {
- setImageUri(route.params.imageUri);
- setDetections([]);
- setCounts({});
- } else {
- handlePickImage();
- }
- }, [route.params]);
- const handlePickImage = async () => {
- try {
- const result = await launchImageLibrary({
- mediaType: 'photo',
- includeBase64: true,
- quality: 1,
- });
- if (result.assets && result.assets[0]) {
- setImageUri(result.assets[0].uri || null);
- setFileName(result.assets[0].fileName || null);
- setDetections([]);
- setCounts({});
- } else {
- navigation.goBack();
- }
- } catch (error) {
- console.error('Pick Image Error:', error);
- Alert.alert('Error', 'Failed to pick image');
- navigation.goBack();
- }
- };
- const analyzeImage = async (uri: string | null) => {
- if (!uri || model.state !== 'loaded') return;
- setIsAnalyzing(true);
- try {
- // 1. & 2. CRITICAL FIX: Use internal native bridge to get 640x640 RGB pixels
- const base64Data = await PixelModule.getPixelsFromUri(uri);
- const uint8Array = base64ToUint8Array(base64Data);
-
- // Convert to Int8Array for the quantized model
- const inputTensor = new Int8Array(uint8Array.buffer);
- if (inputTensor.length !== 640 * 640 * 3) {
- console.warn(`Buffer size mismatch: ${inputTensor.length} vs 1228800.`);
- }
- const resultsRaw = model.model.runSync([inputTensor]);
- const results = parseYoloResults(resultsRaw[0], 640, 640);
-
- if (results.length === 0) {
- Alert.alert('No Detections', 'No palm oil bunches were detected in this image.');
- setDetections([]);
- setCounts({});
- return;
- }
- setDetections(results);
- const tally = calculateTally(results);
- setCounts(tally);
- // Save to history
- saveDetectionRecord({
- label: results[0].label,
- confidence: results[0].confidence,
- classId: results[0].classId,
- imageUri: uri,
- fileName: fileName || undefined,
- detections: results,
- counts: tally,
- });
- } catch (error) {
- console.error('Inference Error:', error);
- Alert.alert('Analysis Error', 'Failed to analyze the image');
- } finally {
- setIsAnalyzing(false);
- }
- };
- return (
- <SafeAreaView style={styles.container}>
- <View style={styles.header}>
- <TouchableOpacity onPress={() => navigation.goBack()} style={styles.backButton}>
- <ArrowLeft color="#FFF" size={24} />
- </TouchableOpacity>
- <View style={{ alignItems: 'center' }}>
- <Text style={styles.title}>Gallery Analysis</Text>
- {fileName && <Text style={styles.fileNameText}>{fileName}</Text>}
- </View>
- <View style={{ width: 40 }} />
- </View>
- <View style={styles.content}>
- {imageUri ? (
- <View
- style={styles.imageContainer}
- onLayout={(event) => {
- const { width, height } = event.nativeEvent.layout;
- setContainerSize({ width, height });
- }}
- >
- <Image source={{ uri: imageUri }} style={styles.image} resizeMode="contain" />
- {!isAnalyzing && <DetectionOverlay detections={detections} containerWidth={containerSize.width} containerHeight={containerSize.height} />}
- {isAnalyzing && (
- <View style={styles.loadingOverlay}>
- <ActivityIndicator size="large" color={Colors.info} />
- <Text style={styles.loadingText}>AI ANALYZING...</Text>
- </View>
- )}
- </View>
- ) : (
- <View style={styles.emptyContainer}>
- <ActivityIndicator size="large" color={Colors.info} />
- </View>
- )}
- {!isAnalyzing && detections.length > 0 && (
- <View style={styles.resultCard}>
- <View style={styles.resultHeader}>
- <CheckCircle2 color={Colors.success} size={24} />
- <Text style={styles.resultTitle}>Analysis Complete</Text>
- </View>
-
- <View style={styles.statsContainer}>
- {Object.entries(counts).map(([label, count]) => (
- <View key={label} style={styles.statRow}>
- <Text style={styles.statLabel}>{label}:</Text>
- <Text style={styles.statValue}>{count}</Text>
- </View>
- ))}
- </View>
- <TouchableOpacity
- style={styles.historyButton}
- onPress={() => navigation.navigate('History')}
- >
- <HistoryIcon color="#FFF" size={20} />
- <Text style={styles.historyButtonText}>View in Field Journal</Text>
- </TouchableOpacity>
- </View>
- )}
- </View>
- {imageUri && !isAnalyzing && detections.length === 0 && (
- <TouchableOpacity
- style={styles.analyzeButton}
- onPress={() => analyzeImage(imageUri)}
- >
- <CheckCircle2 color="#FFF" size={24} />
- <Text style={styles.analyzeButtonText}>Start Analysis</Text>
- </TouchableOpacity>
- )}
- {(detections.length > 0 || !imageUri) && (
- <TouchableOpacity style={styles.reUploadButton} onPress={handlePickImage} disabled={isAnalyzing}>
- <Upload color="#FFF" size={24} />
- <Text style={styles.reUploadText}>{imageUri ? 'Pick Another Image' : 'Select Image'}</Text>
- </TouchableOpacity>
- )}
- </SafeAreaView>
- );
- };
- const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: Colors.background,
- },
- header: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- padding: 16,
- },
- backButton: {
- padding: 8,
- backgroundColor: 'rgba(255,255,255,0.05)',
- borderRadius: 12,
- },
- title: {
- color: '#FFF',
- fontSize: 20,
- fontWeight: 'bold',
- },
- fileNameText: {
- color: Colors.textSecondary,
- fontSize: 12,
- fontWeight: '500',
- },
- content: {
- flex: 1,
- padding: 16,
- },
- imageContainer: {
- width: '100%',
- aspectRatio: 1,
- backgroundColor: '#000',
- borderRadius: 20,
- overflow: 'hidden',
- position: 'relative',
- borderWidth: 1,
- borderColor: 'rgba(255,255,255,0.1)',
- },
- image: {
- width: '100%',
- height: '100%',
- },
- loadingOverlay: {
- ...StyleSheet.absoluteFillObject,
- backgroundColor: 'rgba(15, 23, 42, 0.8)',
- justifyContent: 'center',
- alignItems: 'center',
- gap: 16,
- },
- loadingText: {
- color: '#FFF',
- fontSize: 14,
- fontWeight: '800',
- letterSpacing: 2,
- },
- emptyContainer: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- },
- resultCard: {
- marginTop: 24,
- backgroundColor: Colors.surface,
- padding: 24,
- borderRadius: 24,
- borderWidth: 1,
- borderColor: 'rgba(255,255,255,0.05)',
- },
- resultHeader: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 12,
- marginBottom: 20,
- },
- resultTitle: {
- color: '#FFF',
- fontSize: 18,
- fontWeight: 'bold',
- },
- statsContainer: {
- gap: 12,
- marginBottom: 24,
- },
- statRow: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- paddingVertical: 8,
- borderBottomWidth: 1,
- borderBottomColor: 'rgba(255,255,255,0.05)',
- },
- statLabel: {
- color: Colors.textSecondary,
- fontSize: 16,
- },
- statValue: {
- color: '#FFF',
- fontSize: 16,
- fontWeight: 'bold',
- },
- historyButton: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- backgroundColor: 'rgba(255,255,255,0.05)',
- padding: 16,
- borderRadius: 16,
- gap: 10,
- },
- historyButtonText: {
- color: '#FFF',
- fontSize: 14,
- fontWeight: '600',
- },
- reUploadButton: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- backgroundColor: Colors.info,
- margin: 24,
- padding: 18,
- borderRadius: 18,
- gap: 12,
- },
- reUploadText: {
- color: '#FFF',
- fontSize: 16,
- fontWeight: 'bold',
- },
- analyzeButton: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- backgroundColor: Colors.success,
- margin: 24,
- padding: 18,
- borderRadius: 18,
- gap: 12,
- },
- analyzeButtonText: {
- color: '#FFF',
- fontSize: 16,
- fontWeight: 'bold',
- }
- });
-
-
- ==================================================
- FILE: E:\Task\Research and Development\palm-oil-ai\mobile\src\screens\HistoryScreen.tsx
- ==================================================
- import React, { useState, useCallback } from 'react';
- import { StyleSheet, View, Text, FlatList, TouchableOpacity, RefreshControl, Image, Alert } from 'react-native';
- import { useFocusEffect } from '@react-navigation/native';
- import { Trash2, Clock, CheckCircle, AlertTriangle, Square, CheckSquare, X, Trash } from 'lucide-react-native';
- import { getHistory, clearHistory, deleteRecords, DetectionRecord } from '../utils/storage';
- import { Colors } from '../theme';
- import { DetectionOverlay } from '../components/DetectionOverlay';
- const HistoryCard = ({ item, expandedId, setExpandedId, toggleSelect, isSelectMode, selectedIds, handleLongPress }: any) => {
- const [imgSize, setImgSize] = useState({ w: 0, h: 0 });
- const isExpanded = expandedId === item.id;
- const isSelected = selectedIds.includes(item.id);
- const toggleExpand = (id: string) => {
- if (isSelectMode) {
- toggleSelect(id);
- } else {
- setExpandedId(expandedId === id ? null : id);
- }
- };
- const date = new Date(item.timestamp);
- const timeStr = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
- const dateStr = date.toLocaleDateString();
- return (
- <TouchableOpacity
- activeOpacity={0.9}
- onPress={() => toggleExpand(item.id)}
- onLongPress={() => handleLongPress(item.id)}
- style={[
- styles.card,
- item.isHealthAlert && styles.alertCard,
- isSelected && styles.selectedCard
- ]}
- >
- <View style={styles.cardHeader}>
- <View style={styles.labelContainer}>
- {isSelectMode ? (
- isSelected ? (
- <CheckSquare color={Colors.info} size={20} />
- ) : (
- <Square color={Colors.textSecondary} size={20} />
- )
- ) : item.isHealthAlert ? (
- <AlertTriangle color={Colors.error} size={18} />
- ) : (
- <CheckCircle color={Colors.success} size={18} />
- )}
- <Text style={[styles.label, { color: isSelected ? Colors.info : item.isHealthAlert ? Colors.error : Colors.success }]}>
- {item.label}
- </Text>
- </View>
- <Text style={styles.confidence}>{(item.confidence * 100).toFixed(1)}% Conf.</Text>
- </View>
-
- {isExpanded && item.imageUri && (
- <View style={styles.expandedContent}>
- <View
- style={styles.imageWrapper}
- onLayout={(e) => setImgSize({ w: e.nativeEvent.layout.width, h: e.nativeEvent.layout.height })}
- >
- <Image
- source={{ uri: item.imageUri }}
- style={styles.detailImage}
- resizeMode="contain"
- />
- {imgSize.w > 0 && (
- <DetectionOverlay
- detections={item.detections}
- containerWidth={imgSize.w}
- containerHeight={imgSize.h}
- />
- )}
- </View>
- </View>
- )}
- <View style={styles.cardBody}>
- <View style={styles.tallyContainer}>
- {Object.entries(item.counts).map(([label, count]: [string, any]) => (
- <View key={label} style={styles.tallyItem}>
- <Text style={styles.tallyLabel}>{label}:</Text>
- <Text style={styles.tallyCount}>{count}</Text>
- </View>
- ))}
- </View>
- </View>
- <View style={styles.cardFooter}>
- <Clock color={Colors.textSecondary} size={14} />
- <Text style={styles.footerText}>{dateStr} at {timeStr}</Text>
- {item.fileName && (
- <Text style={[styles.footerText, { marginLeft: 'auto' }]}>{item.fileName}</Text>
- )}
- </View>
- </TouchableOpacity>
- );
- };
- export const HistoryScreen = () => {
- const [history, setHistory] = useState<DetectionRecord[]>([]);
- const [refreshing, setRefreshing] = useState(false);
- const [expandedId, setExpandedId] = useState<string | null>(null);
- const [isSelectMode, setIsSelectMode] = useState(false);
- const [selectedIds, setSelectedIds] = useState<string[]>([]);
- const fetchHistory = async () => {
- const data = await getHistory();
- setHistory(data);
- };
- useFocusEffect(
- useCallback(() => {
- fetchHistory();
- }, [])
- );
- const onRefresh = async () => {
- setRefreshing(true);
- await fetchHistory();
- setRefreshing(false);
- };
- const handleClearAll = () => {
- Alert.alert(
- "Delete All Logs",
- "This action will permanently wipe your entire industrial field journal. Are you sure?",
- [
- { text: "Cancel", style: "cancel" },
- {
- text: "Delete All",
- style: "destructive",
- onPress: async () => {
- await clearHistory();
- setHistory([]);
- setIsSelectMode(false);
- setSelectedIds([]);
- }
- }
- ]
- );
- };
- const handleDeleteSelected = () => {
- Alert.alert(
- "Delete Selected",
- `Are you sure you want to delete ${selectedIds.length} records?`,
- [
- { text: "Cancel", style: "cancel" },
- {
- text: "Delete",
- style: "destructive",
- onPress: async () => {
- await deleteRecords(selectedIds);
- setSelectedIds([]);
- setIsSelectMode(false);
- fetchHistory();
- }
- }
- ]
- );
- };
- const toggleSelect = (id: string) => {
- if (selectedIds.includes(id)) {
- setSelectedIds(selectedIds.filter((idx: string) => idx !== id));
- } else {
- setSelectedIds([...selectedIds, id]);
- }
- };
- const toggleExpand = (id: string) => {
- if (isSelectMode) {
- toggleSelect(id);
- } else {
- setExpandedId(expandedId === id ? null : id);
- }
- };
- const handleLongPress = (id: string) => {
- if (!isSelectMode) {
- setIsSelectMode(true);
- setSelectedIds([id]);
- }
- };
- const exitSelectionMode = () => {
- setIsSelectMode(false);
- setSelectedIds([]);
- };
- const renderItem = ({ item }: { item: DetectionRecord }) => (
- <HistoryCard
- item={item}
- expandedId={expandedId}
- setExpandedId={setExpandedId}
- toggleSelect={toggleSelect}
- isSelectMode={isSelectMode}
- selectedIds={selectedIds}
- handleLongPress={handleLongPress}
- />
- );
- return (
- <View style={styles.container}>
- <View style={styles.header}>
- <View>
- <Text style={styles.title}>Field Journal</Text>
- {isSelectMode && (
- <Text style={styles.selectionCount}>{selectedIds.length} Selected</Text>
- )}
- </View>
-
- <View style={styles.headerActions}>
- {history.length > 0 && (
- isSelectMode ? (
- <TouchableOpacity onPress={exitSelectionMode} style={styles.iconButton}>
- <X color={Colors.textSecondary} size={24} />
- </TouchableOpacity>
- ) : (
- <>
- <TouchableOpacity onPress={handleClearAll} style={styles.clearHeaderButton}>
- <Trash2 color={Colors.error} size={20} />
- <Text style={styles.clearHeaderText}>Delete All</Text>
- </TouchableOpacity>
- <TouchableOpacity onPress={() => setIsSelectMode(true)} style={styles.iconButton}>
- <CheckSquare color={Colors.textSecondary} size={22} />
- </TouchableOpacity>
- </>
- )
- )}
- </View>
- </View>
- {history.length === 0 ? (
- <View style={styles.emptyState}>
- <Clock color={Colors.textSecondary} size={48} strokeWidth={1} />
- <Text style={styles.emptyText}>No detections recorded yet.</Text>
- <Text style={styles.emptySubtext}>Perform detections in the Scanner tab to see them here.</Text>
- </View>
- ) : (
- <View style={{ flex: 1 }}>
- <FlatList
- data={history}
- keyExtractor={(item) => item.id}
- renderItem={renderItem}
- contentContainerStyle={styles.listContent}
- refreshControl={
- <RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={Colors.success} />
- }
- />
-
- {isSelectMode && selectedIds.length > 0 && (
- <View style={styles.bottomActions}>
- <TouchableOpacity
- style={styles.deleteSelectionButton}
- onPress={handleDeleteSelected}
- >
- <Trash color="#FFF" size={20} />
- <Text style={styles.deleteButtonText}>Delete Selected ({selectedIds.length})</Text>
- </TouchableOpacity>
- <TouchableOpacity
- style={styles.clearAllButton}
- onPress={handleClearAll}
- >
- <Trash2 color="#FFF" size={20} />
- <Text style={styles.deleteButtonText}>Delete All</Text>
- </TouchableOpacity>
- </View>
- )}
- </View>
- )}
- </View>
- );
- };
- const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: Colors.background,
- },
- header: {
- padding: 24,
- paddingBottom: 16,
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- },
- title: {
- color: '#FFF',
- fontSize: 28,
- fontWeight: 'bold',
- },
- selectionCount: {
- color: Colors.info,
- fontSize: 14,
- fontWeight: '500',
- marginTop: 2,
- },
- headerActions: {
- flexDirection: 'row',
- gap: 8,
- },
- iconButton: {
- padding: 8,
- backgroundColor: 'rgba(255,255,255,0.05)',
- borderRadius: 12,
- },
- clearButton: {
- padding: 8,
- },
- listContent: {
- padding: 16,
- paddingTop: 0,
- },
- card: {
- backgroundColor: Colors.surface,
- borderRadius: 16,
- padding: 16,
- marginBottom: 16,
- borderWidth: 1,
- borderColor: 'rgba(255,255,255,0.05)',
- },
- selectedCard: {
- borderColor: Colors.info,
- borderWidth: 2,
- backgroundColor: 'rgba(0, 122, 255, 0.05)',
- },
- alertCard: {
- borderColor: 'rgba(255, 59, 48, 0.3)',
- borderLeftWidth: 4,
- borderLeftColor: Colors.error,
- },
- expandedContent: {
- marginVertical: 12,
- borderRadius: 12,
- overflow: 'hidden',
- backgroundColor: '#000',
- },
- imageWrapper: {
- width: '100%',
- aspectRatio: 1,
- position: 'relative',
- },
- detailImage: {
- width: '100%',
- height: '100%',
- },
- cardHeader: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- marginBottom: 12,
- },
- labelContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 8,
- },
- label: {
- fontSize: 18,
- fontWeight: 'bold',
- },
- confidence: {
- color: Colors.textSecondary,
- fontSize: 14,
- },
- cardBody: {
- paddingVertical: 12,
- borderTopWidth: 1,
- borderBottomWidth: 1,
- borderColor: 'rgba(255,255,255,0.05)',
- },
- tallyContainer: {
- flexDirection: 'row',
- flexWrap: 'wrap',
- gap: 12,
- },
- tallyItem: {
- flexDirection: 'row',
- gap: 4,
- },
- tallyLabel: {
- color: Colors.textSecondary,
- fontSize: 12,
- },
- tallyCount: {
- color: '#FFF',
- fontSize: 12,
- fontWeight: 'bold',
- },
- cardFooter: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 6,
- marginTop: 12,
- },
- footerText: {
- color: Colors.textSecondary,
- fontSize: 12,
- },
- emptyState: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- padding: 32,
- },
- emptyText: {
- color: '#FFF',
- fontSize: 18,
- fontWeight: 'bold',
- marginTop: 16,
- },
- emptySubtext: {
- color: Colors.textSecondary,
- textAlign: 'center',
- marginTop: 8,
- },
- bottomActions: {
- position: 'absolute',
- bottom: 24,
- left: 24,
- right: 24,
- backgroundColor: Colors.error,
- borderRadius: 16,
- elevation: 8,
- shadowColor: '#000',
- shadowOffset: { width: 0, height: 4 },
- shadowOpacity: 0.3,
- shadowRadius: 8,
- flexDirection: 'row',
- overflow: 'hidden',
- },
- deleteSelectionButton: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- padding: 16,
- gap: 12,
- flex: 1.5,
- },
- deleteButtonText: {
- color: '#FFF',
- fontSize: 14,
- fontWeight: 'bold',
- },
- clearHeaderButton: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 6,
- paddingVertical: 8,
- paddingHorizontal: 12,
- backgroundColor: 'rgba(255, 59, 48, 0.1)',
- borderRadius: 12,
- },
- clearHeaderText: {
- color: Colors.error,
- fontSize: 12,
- fontWeight: 'bold',
- },
- clearAllButton: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- padding: 16,
- gap: 12,
- borderLeftWidth: 1,
- borderLeftColor: 'rgba(255,255,255,0.2)',
- flex: 1,
- },
- });
-
-
- ==================================================
- FILE: E:\Task\Research and Development\palm-oil-ai\mobile\src\screens\ScannerScreen.tsx
- ==================================================
- import React, { useState, useEffect } from 'react';
- import { StyleSheet, View, Text, StatusBar, SafeAreaView, TouchableOpacity, Image } from 'react-native';
- import { useIsFocused } from '@react-navigation/native';
- import { Camera, useCameraDevice, useCameraPermission, useFrameProcessor, useCameraFormat } from 'react-native-vision-camera';
- import { useTensorflowModel } from 'react-native-fast-tflite';
- import { runOnJS } from 'react-native-reanimated';
- import { launchImageLibrary } from 'react-native-image-picker';
- import { parseYoloResults, calculateTally, BoundingBox } from '../utils/yoloParser';
- import { saveDetectionRecord } from '../utils/storage';
- import { DetectionOverlay } from '../components/DetectionOverlay';
- import { TallyDashboard } from '../components/TallyDashboard';
- import { Colors } from '../theme';
- import { Image as ImageIcon, Upload } from 'lucide-react-native';
- export const ScannerScreen = ({ route }: any) => {
- const isFocused = useIsFocused();
- const { hasPermission, requestPermission } = useCameraPermission();
- const device = useCameraDevice('back');
- const [detections, setDetections] = useState<BoundingBox[]>([]);
- const [counts, setCounts] = useState<Record<string, number>>({});
- const [cameraInitialized, setCameraInitialized] = useState(false);
- const [lastSavedTime, setLastSavedTime] = useState(0);
- // Load the model
- const model = useTensorflowModel(require('../../assets/best.tflite'));
-
- // Find a format that matches 640x640 or closest small resolution
- const format = useCameraFormat(device, [
- { videoResolution: { width: 640, height: 480 } },
- { fps: 30 }
- ]);
- useEffect(() => {
- if (!hasPermission) {
- requestPermission();
- }
- }, [hasPermission]);
- const frameProcessor = useFrameProcessor((frame) => {
- 'worklet';
- if (model.state === 'loaded') {
- try {
- // FALLBACK: Without the resize plugin, we pass the raw buffer.
- // Fast-TFLite might handle resizing if we are lucky with the input.
- // In the next step, we will select a 640x480 format to get closer to 640x640.
- const buffer = frame.toArrayBuffer();
- const result = model.model.runSync([new Int8Array(buffer)]);
- const boxes = parseYoloResults(result[0], frame.width, frame.height);
- runOnJS(setDetections)(boxes);
-
- const currentCounts = calculateTally(boxes);
- runOnJS(setCounts)(currentCounts);
- if (boxes.length > 0) {
- runOnJS(handleAutoSave)(boxes, currentCounts);
- }
- } catch (e) {
- console.error('AI Inference Detail:', e);
- }
- }
- }, [model]);
- const handleAutoSave = (boxes: BoundingBox[], currentCounts: Record<string, number>) => {
- const now = Date.now();
- if (now - lastSavedTime > 5000) {
- const topDet = boxes.reduce((prev, current) => (prev.confidence > current.confidence) ? prev : current);
- saveDetectionRecord({
- label: topDet.label,
- confidence: topDet.confidence,
- classId: topDet.classId,
- detections: boxes,
- counts: currentCounts
- });
- setLastSavedTime(now);
- }
- };
- if (!hasPermission) return (
- <View style={[styles.container, { backgroundColor: Colors.error, justifyContent: 'center' }]}>
- <Text style={styles.text}>ERROR: No Camera Permission</Text>
- </View>
- );
- if (!device) return (
- <View style={[styles.container, { backgroundColor: Colors.info, justifyContent: 'center' }]}>
- <Text style={styles.text}>ERROR: No Camera Device Found</Text>
- </View>
- );
- return (
- <View style={styles.container}>
- <StatusBar barStyle="light-content" />
- {isFocused && (
- <Camera
- style={StyleSheet.absoluteFill}
- device={device}
- isActive={isFocused}
- frameProcessor={frameProcessor}
- format={format}
- pixelFormat="rgb"
- onInitialized={() => {
- console.log('Camera: Initialized');
- setCameraInitialized(true);
- }}
- onError={(error) => console.error('Camera: Error', error)}
- />
- )}
-
- <SafeAreaView style={styles.overlay} pointerEvents="none">
- <View style={[styles.header, { backgroundColor: 'rgba(15, 23, 42, 0.6)' }]}>
- <Text style={styles.title}>Live Scanner</Text>
- <Text style={styles.status}>
- {model.state === 'loaded' ? '● AI ACTIVE' : `○ ${model.state.toUpperCase()}`}
- </Text>
- </View>
- <DetectionOverlay detections={detections} />
- <TallyDashboard counts={counts} />
- </SafeAreaView>
- <View style={styles.debugBox}>
- <Text style={styles.debugText}>
- Cam: {cameraInitialized ? 'READY' : 'STARTING...'} |
- Model: {model.state.toUpperCase()} |
- Dets: {detections.length}
- </Text>
- </View>
- </View>
- );
- };
- const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: Colors.background,
- },
- overlay: {
- flex: 1,
- },
- header: {
- padding: 16,
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- },
- title: {
- color: '#FFF',
- fontSize: 16,
- fontWeight: 'bold',
- letterSpacing: 0.5,
- },
- status: {
- color: Colors.success,
- fontSize: 11,
- fontWeight: '800',
- },
- text: {
- color: '#FFF',
- textAlign: 'center',
- fontSize: 18,
- fontWeight: 'bold',
- },
- galleryButton: {
- position: 'absolute',
- bottom: 100,
- right: 20,
- backgroundColor: 'rgba(30, 41, 59, 0.8)',
- padding: 16,
- borderRadius: 30,
- borderWidth: 1,
- borderColor: 'rgba(255,255,255,0.2)',
- },
- debugBox: {
- position: 'absolute',
- top: 60,
- left: 20,
- right: 20,
- backgroundColor: 'rgba(255,255,255,0.9)',
- padding: 8,
- borderRadius: 8,
- },
- debugText: {
- color: '#000',
- fontSize: 12,
- fontWeight: '600',
- textAlign: 'center',
- }
- });
-
-
- ==================================================
- FILE: E:\Task\Research and Development\palm-oil-ai\mobile\src\theme\index.ts
- ==================================================
- export const Colors = {
- // Industrial Alert Colors
- error: '#FF3B30', // High-visibility Red for Abnormal/Empty_Bunch
- warning: '#FFCC00', // Yellow for Penalty/Underripe
- success: '#34C759', // Green for Ripe
- info: '#007AFF', // Blue for Overripe (processing focus)
-
- // Base Palette
- background: '#0F172A', // Deep Slate
- surface: '#1E293B',
- text: '#F8FAFC',
- textSecondary: '#94A3B8',
-
- // Class Mapping Colors
- classes: {
- 0: '#FF3B30', // Empty_Bunch (Alert)
- 1: '#FFCC00', // Underripe (Warning)
- 2: '#FF3B30', // Abnormal (Health Alert)
- 3: '#34C759', // Ripe (Success)
- 4: '#FF9500', // Unripe (Penalty)
- 5: '#AF52DE', // Overripe (FFA Prevention)
- }
- };
- export const Typography = {
- header: {
- fontSize: 24,
- fontWeight: 'bold',
- color: Colors.text,
- },
- body: {
- fontSize: 16,
- color: Colors.textSecondary,
- },
- label: {
- fontSize: 12,
- fontWeight: '600',
- textTransform: 'uppercase',
- }
- };
-
-
- ==================================================
- FILE: E:\Task\Research and Development\palm-oil-ai\mobile\src\utils\storage.ts
- ==================================================
- import AsyncStorage from '@react-native-async-storage/async-storage';
- import { BoundingBox } from './yoloParser';
- export interface DetectionRecord {
- id: string;
- timestamp: string;
- label: string;
- confidence: number;
- classId: number;
- isHealthAlert: boolean;
- imageUri?: string;
- fileName?: string;
- detections: BoundingBox[];
- counts: Record<string, number>;
- }
- const STORAGE_KEY = 'palm_history';
- /**
- * Saves a new detection record to local storage.
- */
- export const saveDetectionRecord = async (record: Omit<DetectionRecord, 'id' | 'timestamp' | 'isHealthAlert'>) => {
- try {
- const existing = await AsyncStorage.getItem(STORAGE_KEY);
- const history: DetectionRecord[] = existing ? JSON.parse(existing) : [];
-
- const newRecord: DetectionRecord = {
- ...record,
- id: Date.now().toString(),
- timestamp: new Date().toISOString(),
- isHealthAlert: record.detections.some(d => d.classId === 0 || d.classId === 2)
- };
-
- await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify([newRecord, ...history]));
- console.log('Storage: Record saved successfully');
- } catch (error) {
- console.error('Storage: Error saving record', error);
- }
- };
- /**
- * Retrieves all detection records from local storage.
- */
- export const getHistory = async (): Promise<DetectionRecord[]> => {
- try {
- const existing = await AsyncStorage.getItem(STORAGE_KEY);
- return existing ? JSON.parse(existing) : [];
- } catch (error) {
- console.error('Storage: Error fetching history', error);
- return [];
- }
- };
- /**
- * Clears all detection records from local storage.
- */
- export const clearHistory = async () => {
- try {
- await AsyncStorage.removeItem(STORAGE_KEY);
- console.log('Storage: History cleared');
- } catch (error) {
- console.error('Storage: Error clearing history', error);
- }
- };
- /**
- * Deletes specific records from local storage.
- */
- export const deleteRecords = async (ids: string[]) => {
- try {
- const existing = await AsyncStorage.getItem(STORAGE_KEY);
- if (!existing) return;
-
- const history: DetectionRecord[] = JSON.parse(existing);
- const updated = history.filter(record => !ids.includes(record.id));
-
- await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
- console.log(`Storage: ${ids.length} records deleted`);
- } catch (error) {
- console.error('Storage: Error deleting records', error);
- }
- };
-
-
- ==================================================
- FILE: E:\Task\Research and Development\palm-oil-ai\mobile\src\utils\yoloParser.ts
- ==================================================
- export interface BoundingBox {
- id: string;
- x: number;
- y: number;
- width: number;
- height: number;
- relX: number;
- relY: number;
- relWidth: number;
- relHeight: number;
- label: string;
- confidence: number;
- classId: number;
- }
- const CLASS_NAMES = [
- 'Empty_Bunch',
- 'Underripe',
- 'Abnormal',
- 'Ripe',
- 'Unripe',
- 'Overripe'
- ];
- /**
- * Parses YOLOv8/v11 output tensor into BoundingBox objects.
- * Format: [x1, y1, x2, y2, score, classId]
- * Quantization: scale=0.019916336983442307, zeroPoint=-124
- */
- /**
- * Normalizes a raw pixel buffer to 0.0-1.0 range for Float32 models.
- */
- export function normalizeTensor(buffer: ArrayBuffer, width: number, height: number): Float32Array {
- 'worklet';
- const data = new Uint8Array(buffer);
- const normalized = new Float32Array(width * height * 3);
-
- for (let i = 0; i < data.length; i++) {
- normalized[i] = data[i] / 255.0;
- }
- return normalized;
- }
- export function parseYoloResults(
- tensor: Int8Array | Uint8Array | Float32Array | any,
- frameWidth: number,
- frameHeight: number
- ): BoundingBox[] {
- 'worklet';
-
- // Detection parameters from INT8 model
- const scale = 0.019916336983442307;
- const zeroPoint = -124;
- const numDetections = 300;
- const numElements = 6;
- const detections: BoundingBox[] = [];
- const data = tensor;
- if (!data || data.length === 0) return [];
- for (let i = 0; i < numDetections; i++) {
- const base = i * numElements;
- if (base + 5 >= data.length) break;
- // Handle Float32 vs Quantized Int8
- const getVal = (idx: number) => {
- const val = data[idx];
- if (data instanceof Float32Array) return val;
- return (val - zeroPoint) * scale;
- };
- const x1 = getVal(base + 0);
- const y1 = getVal(base + 1);
- const x2 = getVal(base + 2);
- const y2 = getVal(base + 3);
- const score = getVal(base + 4);
- const classId = Math.round(getVal(base + 5));
- if (score > 0.45 && classId >= 0 && classId < CLASS_NAMES.length) {
- const normalizedX1 = x1 / 640;
- const normalizedY1 = y1 / 640;
- const normalizedX2 = x2 / 640;
- const normalizedY2 = y2 / 640;
- detections.push({
- id: `det_${i}_${Math.random().toString(36).substr(2, 9)}`,
- x: Math.max(0, normalizedX1 * frameWidth),
- y: Math.max(0, normalizedY1 * frameHeight),
- width: Math.max(0, (normalizedX2 - normalizedX1) * frameWidth),
- height: Math.max(0, (normalizedY2 - normalizedY1) * frameHeight),
- relX: normalizedX1,
- relY: normalizedY1,
- relWidth: normalizedX2 - normalizedX1,
- relHeight: normalizedY2 - normalizedY1,
- label: CLASS_NAMES[classId],
- confidence: score,
- classId: classId
- });
- }
- }
- return detections;
- }
- export function calculateTally(detections: BoundingBox[]) {
- 'worklet';
- const counts: { [key: string]: number } = {};
- for (const det of detections) {
- counts[det.label] = (counts[det.label] || 0) + 1;
- }
- return counts;
- }
-
-
|