src.txt 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689
  1. ===== Folder Structure =====
  2. Folder PATH listing for volume New Volume
  3. Volume serial number is 36B1-447D
  4. E:\TASK\RESEARCH AND DEVELOPMENT\PALM-OIL-AI\MOBILE\SRC
  5. | App.tsx
  6. |
  7. +---components
  8. | DetectionOverlay.tsx
  9. | TallyDashboard.tsx
  10. |
  11. +---hooks
  12. +---navigation
  13. | AppNavigator.tsx
  14. |
  15. +---screens
  16. | DashboardScreen.tsx
  17. | GalleryAnalysisScreen.tsx
  18. | HistoryScreen.tsx
  19. | ScannerScreen.tsx
  20. |
  21. +---theme
  22. | index.ts
  23. |
  24. \---utils
  25. storage.ts
  26. yoloParser.ts
  27. ==================================================
  28. FILE: E:\Task\Research and Development\palm-oil-ai\mobile\src\App.tsx
  29. ==================================================
  30. import React from 'react';
  31. import { NavigationContainer } from '@react-navigation/native';
  32. import { AppNavigator } from './navigation/AppNavigator';
  33. export default function App() {
  34. return (
  35. <NavigationContainer>
  36. <AppNavigator />
  37. </NavigationContainer>
  38. );
  39. }
  40. ==================================================
  41. FILE: E:\Task\Research and Development\palm-oil-ai\mobile\src\components\DetectionOverlay.tsx
  42. ==================================================
  43. import React from 'react';
  44. import { View, StyleSheet, Text } from 'react-native';
  45. import Animated, { useAnimatedStyle } from 'react-native-reanimated';
  46. import { Colors } from '../theme';
  47. import { BoundingBox } from '../utils/yoloParser';
  48. interface DetectionOverlayProps {
  49. detections: BoundingBox[];
  50. containerWidth?: number;
  51. containerHeight?: number;
  52. }
  53. export const DetectionOverlay: React.FC<DetectionOverlayProps> = ({ detections, containerWidth, containerHeight }) => {
  54. return (
  55. <View style={StyleSheet.absoluteFill}>
  56. {detections.map((det) => {
  57. const x = containerWidth ? det.relX * containerWidth : det.x;
  58. const y = containerHeight ? det.relY * containerHeight : det.y;
  59. const width = containerWidth ? det.relWidth * containerWidth : det.width;
  60. const height = containerHeight ? det.relHeight * containerHeight : det.height;
  61. return (
  62. <View
  63. key={det.id}
  64. style={[
  65. styles.box,
  66. {
  67. left: x,
  68. top: y,
  69. width: width,
  70. height: height,
  71. borderColor: Colors.classes[det.classId as keyof typeof Colors.classes] || Colors.text,
  72. }
  73. ]}
  74. >
  75. <View style={[
  76. styles.labelContainer,
  77. { backgroundColor: Colors.classes[det.classId as keyof typeof Colors.classes] || Colors.text }
  78. ]}>
  79. <Text style={styles.labelText}>
  80. {det.label} ({Math.round(det.confidence * 100)}%)
  81. </Text>
  82. </View>
  83. </View>
  84. );
  85. })}
  86. </View>
  87. );
  88. };
  89. const styles = StyleSheet.create({
  90. box: {
  91. position: 'absolute',
  92. borderWidth: 2,
  93. borderRadius: 4,
  94. },
  95. labelContainer: {
  96. position: 'absolute',
  97. top: -24,
  98. left: -2,
  99. paddingHorizontal: 6,
  100. paddingVertical: 2,
  101. borderRadius: 4,
  102. },
  103. labelText: {
  104. color: '#FFF',
  105. fontSize: 12,
  106. fontWeight: 'bold',
  107. }
  108. });
  109. ==================================================
  110. FILE: E:\Task\Research and Development\palm-oil-ai\mobile\src\components\TallyDashboard.tsx
  111. ==================================================
  112. import React from 'react';
  113. import { View, StyleSheet, Text } from 'react-native';
  114. import { Colors, Typography } from '../theme';
  115. interface TallyCounts {
  116. [key: string]: number;
  117. }
  118. interface TallyDashboardProps {
  119. counts: TallyCounts;
  120. }
  121. export const TallyDashboard: React.FC<TallyDashboardProps> = ({ counts }) => {
  122. const classNames = [
  123. 'Empty_Bunch',
  124. 'Underripe',
  125. 'Abnormal',
  126. 'Ripe',
  127. 'Unripe',
  128. 'Overripe'
  129. ];
  130. return (
  131. <View style={styles.container}>
  132. {classNames.map((name, index) => (
  133. <View key={name} style={styles.item}>
  134. <Text style={[styles.count, { color: Colors.classes[index as keyof typeof Colors.classes] }]}>
  135. {counts[name] || 0}
  136. </Text>
  137. <Text style={styles.label}>{name}</Text>
  138. </View>
  139. ))}
  140. </View>
  141. );
  142. };
  143. const styles = StyleSheet.create({
  144. container: {
  145. flexDirection: 'row',
  146. flexWrap: 'wrap',
  147. backgroundColor: 'rgba(15, 23, 42, 0.8)',
  148. padding: 12,
  149. borderRadius: 12,
  150. margin: 16,
  151. position: 'absolute',
  152. bottom: 40,
  153. left: 0,
  154. right: 0,
  155. justifyContent: 'space-around',
  156. borderWidth: 1,
  157. borderColor: 'rgba(255, 255, 255, 0.1)',
  158. },
  159. item: {
  160. alignItems: 'center',
  161. minWidth: '30%',
  162. marginVertical: 4,
  163. },
  164. count: {
  165. fontSize: 18,
  166. fontWeight: 'bold',
  167. },
  168. label: {
  169. fontSize: 10,
  170. color: Colors.textSecondary,
  171. marginTop: 2,
  172. textTransform: 'uppercase',
  173. }
  174. });
  175. ==================================================
  176. FILE: E:\Task\Research and Development\palm-oil-ai\mobile\src\navigation\AppNavigator.tsx
  177. ==================================================
  178. import React from 'react';
  179. import { createNativeStackNavigator } from '@react-navigation/native-stack';
  180. import { DashboardScreen } from '../screens/DashboardScreen';
  181. import { ScannerScreen } from '../screens/ScannerScreen';
  182. import { HistoryScreen } from '../screens/HistoryScreen';
  183. import { GalleryAnalysisScreen } from '../screens/GalleryAnalysisScreen';
  184. import { Colors } from '../theme';
  185. const Stack = createNativeStackNavigator();
  186. export const AppNavigator = () => {
  187. return (
  188. <Stack.Navigator
  189. initialRouteName="Dashboard"
  190. screenOptions={{
  191. headerStyle: {
  192. backgroundColor: Colors.background,
  193. },
  194. headerTintColor: '#FFF',
  195. headerTitleStyle: {
  196. fontWeight: 'bold',
  197. },
  198. headerShadowVisible: false,
  199. }}
  200. >
  201. <Stack.Screen
  202. name="Dashboard"
  203. component={DashboardScreen}
  204. options={{ headerShown: false }}
  205. />
  206. <Stack.Screen
  207. name="Scanner"
  208. component={ScannerScreen}
  209. options={{
  210. title: 'Industrial Scanner',
  211. headerTransparent: true,
  212. headerTitleStyle: { color: '#FFF' }
  213. }}
  214. />
  215. <Stack.Screen
  216. name="History"
  217. component={HistoryScreen}
  218. options={{
  219. title: 'Field Journal',
  220. headerLargeTitle: true,
  221. }}
  222. />
  223. <Stack.Screen
  224. name="GalleryAnalysis"
  225. component={GalleryAnalysisScreen}
  226. options={{
  227. headerShown: false,
  228. }}
  229. />
  230. </Stack.Navigator>
  231. );
  232. };
  233. ==================================================
  234. FILE: E:\Task\Research and Development\palm-oil-ai\mobile\src\screens\DashboardScreen.tsx
  235. ==================================================
  236. import React from 'react';
  237. import { StyleSheet, View, Text, TouchableOpacity, SafeAreaView, StatusBar, ScrollView } from 'react-native';
  238. import { Scan, Image as ImageIcon, History, ShieldAlert } from 'lucide-react-native';
  239. import { Colors } from '../theme';
  240. export const DashboardScreen = ({ navigation }: any) => {
  241. return (
  242. <SafeAreaView style={styles.container}>
  243. <StatusBar barStyle="light-content" />
  244. <ScrollView
  245. contentContainerStyle={styles.scrollContent}
  246. showsVerticalScrollIndicator={false}
  247. >
  248. <View style={styles.header}>
  249. <Text style={styles.title}>Palm Oil AI</Text>
  250. <Text style={styles.subtitle}>Industrial Management Hub</Text>
  251. </View>
  252. <View style={styles.grid}>
  253. <TouchableOpacity
  254. style={styles.card}
  255. onPress={() => navigation.navigate('Scanner')}
  256. >
  257. <View style={[styles.iconContainer, { backgroundColor: 'rgba(52, 199, 89, 0.1)' }]}>
  258. <Scan color={Colors.success} size={32} />
  259. </View>
  260. <Text style={styles.cardTitle}>Live Field Scan</Text>
  261. <Text style={styles.cardDesc}>Real-time ripeness detection & health alerts</Text>
  262. </TouchableOpacity>
  263. <TouchableOpacity
  264. style={styles.card}
  265. onPress={() => navigation.navigate('GalleryAnalysis')}
  266. >
  267. <View style={[styles.iconContainer, { backgroundColor: 'rgba(0, 122, 255, 0.1)' }]}>
  268. <ImageIcon color={Colors.info} size={32} />
  269. </View>
  270. <Text style={styles.cardTitle}>Analyze Gallery</Text>
  271. <Text style={styles.cardDesc}>Upload & analyze harvested bunches from storage</Text>
  272. </TouchableOpacity>
  273. <TouchableOpacity
  274. style={styles.card}
  275. onPress={() => navigation.navigate('History')}
  276. >
  277. <View style={[styles.iconContainer, { backgroundColor: 'rgba(148, 163, 184, 0.1)' }]}>
  278. <History color={Colors.textSecondary} size={32} />
  279. </View>
  280. <Text style={styles.cardTitle}>Detection History</Text>
  281. <Text style={styles.cardDesc}>Review past logs and industrial field journal</Text>
  282. </TouchableOpacity>
  283. <View style={[styles.card, styles.alertCard]}>
  284. <View style={[styles.iconContainer, { backgroundColor: 'rgba(255, 59, 48, 0.1)' }]}>
  285. <ShieldAlert color={Colors.error} size={32} />
  286. </View>
  287. <Text style={styles.cardTitle}>System Health</Text>
  288. <Text style={styles.cardDesc}>AI Inference: ACTIVE | Model: V11-INT8</Text>
  289. </View>
  290. </View>
  291. <View style={styles.footer}>
  292. <Text style={styles.versionText}>Industrial Suite v4.2.0-stable</Text>
  293. </View>
  294. </ScrollView>
  295. </SafeAreaView>
  296. );
  297. };
  298. const styles = StyleSheet.create({
  299. container: {
  300. flex: 1,
  301. backgroundColor: Colors.background,
  302. },
  303. scrollContent: {
  304. paddingBottom: 32,
  305. },
  306. header: {
  307. padding: 32,
  308. paddingTop: 48,
  309. },
  310. title: {
  311. color: '#FFF',
  312. fontSize: 32,
  313. fontWeight: 'bold',
  314. },
  315. subtitle: {
  316. color: Colors.textSecondary,
  317. fontSize: 16,
  318. marginTop: 4,
  319. },
  320. grid: {
  321. flex: 1,
  322. padding: 24,
  323. gap: 16,
  324. },
  325. card: {
  326. backgroundColor: Colors.surface,
  327. padding: 20,
  328. borderRadius: 20,
  329. borderWidth: 1,
  330. borderColor: 'rgba(255,255,255,0.05)',
  331. },
  332. alertCard: {
  333. borderColor: 'rgba(255, 59, 48, 0.2)',
  334. },
  335. iconContainer: {
  336. width: 64,
  337. height: 64,
  338. borderRadius: 16,
  339. justifyContent: 'center',
  340. alignItems: 'center',
  341. marginBottom: 16,
  342. },
  343. cardTitle: {
  344. color: '#FFF',
  345. fontSize: 18,
  346. fontWeight: 'bold',
  347. },
  348. cardDesc: {
  349. color: Colors.textSecondary,
  350. fontSize: 14,
  351. marginTop: 4,
  352. },
  353. footer: {
  354. padding: 24,
  355. alignItems: 'center',
  356. },
  357. versionText: {
  358. color: 'rgba(255,255,255,0.3)',
  359. fontSize: 12,
  360. fontWeight: '500',
  361. }
  362. });
  363. ==================================================
  364. FILE: E:\Task\Research and Development\palm-oil-ai\mobile\src\screens\GalleryAnalysisScreen.tsx
  365. ==================================================
  366. import React, { useState, useEffect } from 'react';
  367. import { StyleSheet, View, Text, Image, TouchableOpacity, SafeAreaView, ActivityIndicator, Alert, Dimensions } from 'react-native';
  368. import { useNavigation, useRoute } from '@react-navigation/native';
  369. import { launchImageLibrary } from 'react-native-image-picker';
  370. import { useTensorflowModel } from 'react-native-fast-tflite';
  371. import { ArrowLeft, Upload, CheckCircle2, History as HistoryIcon } from 'lucide-react-native';
  372. import { NativeModules } from 'react-native';
  373. const { PixelModule } = NativeModules;
  374. import { Colors } from '../theme';
  375. import { parseYoloResults, calculateTally, BoundingBox } from '../utils/yoloParser';
  376. import { saveDetectionRecord } from '../utils/storage';
  377. import { DetectionOverlay } from '../components/DetectionOverlay';
  378. const { width: SCREEN_WIDTH } = Dimensions.get('window');
  379. const base64ToUint8Array = (base64: string) => {
  380. if (!base64 || typeof base64 !== 'string') return new Uint8Array(0);
  381. const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
  382. const lookup = new Uint8Array(256);
  383. for (let i = 0; i < chars.length; i++) {
  384. lookup[chars.charCodeAt(i)] = i;
  385. }
  386. const len = base64.length;
  387. // Calculate buffer length (approximate is fine for Uint8Array assignment)
  388. const buffer = new Uint8Array(Math.floor((len * 3) / 4));
  389. let p = 0;
  390. for (let i = 0; i < len; i += 4) {
  391. const encoded1 = lookup[base64.charCodeAt(i)];
  392. const encoded2 = lookup[base64.charCodeAt(i + 1)];
  393. const encoded3 = lookup[base64.charCodeAt(i + 2)] || 0;
  394. const encoded4 = lookup[base64.charCodeAt(i + 3)] || 0;
  395. buffer[p++] = (encoded1 << 2) | (encoded2 >> 4);
  396. if (p < buffer.length) buffer[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
  397. if (p < buffer.length) buffer[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
  398. }
  399. return buffer;
  400. };
  401. // Removed manual base64ToUint8Array as we now use imageToRgb
  402. export const GalleryAnalysisScreen = () => {
  403. const navigation = useNavigation<any>();
  404. const route = useRoute<any>();
  405. const [imageUri, setImageUri] = useState<string | null>(null);
  406. const [fileName, setFileName] = useState<string | null>(null);
  407. const [isAnalyzing, setIsAnalyzing] = useState(false);
  408. const [detections, setDetections] = useState<BoundingBox[]>([]);
  409. const [counts, setCounts] = useState<Record<string, number>>({});
  410. const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
  411. const model = useTensorflowModel(require('../../assets/best.tflite'));
  412. useEffect(() => {
  413. if (route.params?.imageUri) {
  414. setImageUri(route.params.imageUri);
  415. setDetections([]);
  416. setCounts({});
  417. } else {
  418. handlePickImage();
  419. }
  420. }, [route.params]);
  421. const handlePickImage = async () => {
  422. try {
  423. const result = await launchImageLibrary({
  424. mediaType: 'photo',
  425. includeBase64: true,
  426. quality: 1,
  427. });
  428. if (result.assets && result.assets[0]) {
  429. setImageUri(result.assets[0].uri || null);
  430. setFileName(result.assets[0].fileName || null);
  431. setDetections([]);
  432. setCounts({});
  433. } else {
  434. navigation.goBack();
  435. }
  436. } catch (error) {
  437. console.error('Pick Image Error:', error);
  438. Alert.alert('Error', 'Failed to pick image');
  439. navigation.goBack();
  440. }
  441. };
  442. const analyzeImage = async (uri: string | null) => {
  443. if (!uri || model.state !== 'loaded') return;
  444. setIsAnalyzing(true);
  445. try {
  446. // 1. & 2. CRITICAL FIX: Use internal native bridge to get 640x640 RGB pixels
  447. const base64Data = await PixelModule.getPixelsFromUri(uri);
  448. const uint8Array = base64ToUint8Array(base64Data);
  449. // Convert to Int8Array for the quantized model
  450. const inputTensor = new Int8Array(uint8Array.buffer);
  451. if (inputTensor.length !== 640 * 640 * 3) {
  452. console.warn(`Buffer size mismatch: ${inputTensor.length} vs 1228800.`);
  453. }
  454. const resultsRaw = model.model.runSync([inputTensor]);
  455. const results = parseYoloResults(resultsRaw[0], 640, 640);
  456. if (results.length === 0) {
  457. Alert.alert('No Detections', 'No palm oil bunches were detected in this image.');
  458. setDetections([]);
  459. setCounts({});
  460. return;
  461. }
  462. setDetections(results);
  463. const tally = calculateTally(results);
  464. setCounts(tally);
  465. // Save to history
  466. saveDetectionRecord({
  467. label: results[0].label,
  468. confidence: results[0].confidence,
  469. classId: results[0].classId,
  470. imageUri: uri,
  471. fileName: fileName || undefined,
  472. detections: results,
  473. counts: tally,
  474. });
  475. } catch (error) {
  476. console.error('Inference Error:', error);
  477. Alert.alert('Analysis Error', 'Failed to analyze the image');
  478. } finally {
  479. setIsAnalyzing(false);
  480. }
  481. };
  482. return (
  483. <SafeAreaView style={styles.container}>
  484. <View style={styles.header}>
  485. <TouchableOpacity onPress={() => navigation.goBack()} style={styles.backButton}>
  486. <ArrowLeft color="#FFF" size={24} />
  487. </TouchableOpacity>
  488. <View style={{ alignItems: 'center' }}>
  489. <Text style={styles.title}>Gallery Analysis</Text>
  490. {fileName && <Text style={styles.fileNameText}>{fileName}</Text>}
  491. </View>
  492. <View style={{ width: 40 }} />
  493. </View>
  494. <View style={styles.content}>
  495. {imageUri ? (
  496. <View
  497. style={styles.imageContainer}
  498. onLayout={(event) => {
  499. const { width, height } = event.nativeEvent.layout;
  500. setContainerSize({ width, height });
  501. }}
  502. >
  503. <Image source={{ uri: imageUri }} style={styles.image} resizeMode="contain" />
  504. {!isAnalyzing && <DetectionOverlay detections={detections} containerWidth={containerSize.width} containerHeight={containerSize.height} />}
  505. {isAnalyzing && (
  506. <View style={styles.loadingOverlay}>
  507. <ActivityIndicator size="large" color={Colors.info} />
  508. <Text style={styles.loadingText}>AI ANALYZING...</Text>
  509. </View>
  510. )}
  511. </View>
  512. ) : (
  513. <View style={styles.emptyContainer}>
  514. <ActivityIndicator size="large" color={Colors.info} />
  515. </View>
  516. )}
  517. {!isAnalyzing && detections.length > 0 && (
  518. <View style={styles.resultCard}>
  519. <View style={styles.resultHeader}>
  520. <CheckCircle2 color={Colors.success} size={24} />
  521. <Text style={styles.resultTitle}>Analysis Complete</Text>
  522. </View>
  523. <View style={styles.statsContainer}>
  524. {Object.entries(counts).map(([label, count]) => (
  525. <View key={label} style={styles.statRow}>
  526. <Text style={styles.statLabel}>{label}:</Text>
  527. <Text style={styles.statValue}>{count}</Text>
  528. </View>
  529. ))}
  530. </View>
  531. <TouchableOpacity
  532. style={styles.historyButton}
  533. onPress={() => navigation.navigate('History')}
  534. >
  535. <HistoryIcon color="#FFF" size={20} />
  536. <Text style={styles.historyButtonText}>View in Field Journal</Text>
  537. </TouchableOpacity>
  538. </View>
  539. )}
  540. </View>
  541. {imageUri && !isAnalyzing && detections.length === 0 && (
  542. <TouchableOpacity
  543. style={styles.analyzeButton}
  544. onPress={() => analyzeImage(imageUri)}
  545. >
  546. <CheckCircle2 color="#FFF" size={24} />
  547. <Text style={styles.analyzeButtonText}>Start Analysis</Text>
  548. </TouchableOpacity>
  549. )}
  550. {(detections.length > 0 || !imageUri) && (
  551. <TouchableOpacity style={styles.reUploadButton} onPress={handlePickImage} disabled={isAnalyzing}>
  552. <Upload color="#FFF" size={24} />
  553. <Text style={styles.reUploadText}>{imageUri ? 'Pick Another Image' : 'Select Image'}</Text>
  554. </TouchableOpacity>
  555. )}
  556. </SafeAreaView>
  557. );
  558. };
  559. const styles = StyleSheet.create({
  560. container: {
  561. flex: 1,
  562. backgroundColor: Colors.background,
  563. },
  564. header: {
  565. flexDirection: 'row',
  566. alignItems: 'center',
  567. justifyContent: 'space-between',
  568. padding: 16,
  569. },
  570. backButton: {
  571. padding: 8,
  572. backgroundColor: 'rgba(255,255,255,0.05)',
  573. borderRadius: 12,
  574. },
  575. title: {
  576. color: '#FFF',
  577. fontSize: 20,
  578. fontWeight: 'bold',
  579. },
  580. fileNameText: {
  581. color: Colors.textSecondary,
  582. fontSize: 12,
  583. fontWeight: '500',
  584. },
  585. content: {
  586. flex: 1,
  587. padding: 16,
  588. },
  589. imageContainer: {
  590. width: '100%',
  591. aspectRatio: 1,
  592. backgroundColor: '#000',
  593. borderRadius: 20,
  594. overflow: 'hidden',
  595. position: 'relative',
  596. borderWidth: 1,
  597. borderColor: 'rgba(255,255,255,0.1)',
  598. },
  599. image: {
  600. width: '100%',
  601. height: '100%',
  602. },
  603. loadingOverlay: {
  604. ...StyleSheet.absoluteFillObject,
  605. backgroundColor: 'rgba(15, 23, 42, 0.8)',
  606. justifyContent: 'center',
  607. alignItems: 'center',
  608. gap: 16,
  609. },
  610. loadingText: {
  611. color: '#FFF',
  612. fontSize: 14,
  613. fontWeight: '800',
  614. letterSpacing: 2,
  615. },
  616. emptyContainer: {
  617. flex: 1,
  618. justifyContent: 'center',
  619. alignItems: 'center',
  620. },
  621. resultCard: {
  622. marginTop: 24,
  623. backgroundColor: Colors.surface,
  624. padding: 24,
  625. borderRadius: 24,
  626. borderWidth: 1,
  627. borderColor: 'rgba(255,255,255,0.05)',
  628. },
  629. resultHeader: {
  630. flexDirection: 'row',
  631. alignItems: 'center',
  632. gap: 12,
  633. marginBottom: 20,
  634. },
  635. resultTitle: {
  636. color: '#FFF',
  637. fontSize: 18,
  638. fontWeight: 'bold',
  639. },
  640. statsContainer: {
  641. gap: 12,
  642. marginBottom: 24,
  643. },
  644. statRow: {
  645. flexDirection: 'row',
  646. justifyContent: 'space-between',
  647. paddingVertical: 8,
  648. borderBottomWidth: 1,
  649. borderBottomColor: 'rgba(255,255,255,0.05)',
  650. },
  651. statLabel: {
  652. color: Colors.textSecondary,
  653. fontSize: 16,
  654. },
  655. statValue: {
  656. color: '#FFF',
  657. fontSize: 16,
  658. fontWeight: 'bold',
  659. },
  660. historyButton: {
  661. flexDirection: 'row',
  662. alignItems: 'center',
  663. justifyContent: 'center',
  664. backgroundColor: 'rgba(255,255,255,0.05)',
  665. padding: 16,
  666. borderRadius: 16,
  667. gap: 10,
  668. },
  669. historyButtonText: {
  670. color: '#FFF',
  671. fontSize: 14,
  672. fontWeight: '600',
  673. },
  674. reUploadButton: {
  675. flexDirection: 'row',
  676. alignItems: 'center',
  677. justifyContent: 'center',
  678. backgroundColor: Colors.info,
  679. margin: 24,
  680. padding: 18,
  681. borderRadius: 18,
  682. gap: 12,
  683. },
  684. reUploadText: {
  685. color: '#FFF',
  686. fontSize: 16,
  687. fontWeight: 'bold',
  688. },
  689. analyzeButton: {
  690. flexDirection: 'row',
  691. alignItems: 'center',
  692. justifyContent: 'center',
  693. backgroundColor: Colors.success,
  694. margin: 24,
  695. padding: 18,
  696. borderRadius: 18,
  697. gap: 12,
  698. },
  699. analyzeButtonText: {
  700. color: '#FFF',
  701. fontSize: 16,
  702. fontWeight: 'bold',
  703. }
  704. });
  705. ==================================================
  706. FILE: E:\Task\Research and Development\palm-oil-ai\mobile\src\screens\HistoryScreen.tsx
  707. ==================================================
  708. import React, { useState, useCallback } from 'react';
  709. import { StyleSheet, View, Text, FlatList, TouchableOpacity, RefreshControl, Image, Alert } from 'react-native';
  710. import { useFocusEffect } from '@react-navigation/native';
  711. import { Trash2, Clock, CheckCircle, AlertTriangle, Square, CheckSquare, X, Trash } from 'lucide-react-native';
  712. import { getHistory, clearHistory, deleteRecords, DetectionRecord } from '../utils/storage';
  713. import { Colors } from '../theme';
  714. import { DetectionOverlay } from '../components/DetectionOverlay';
  715. const HistoryCard = ({ item, expandedId, setExpandedId, toggleSelect, isSelectMode, selectedIds, handleLongPress }: any) => {
  716. const [imgSize, setImgSize] = useState({ w: 0, h: 0 });
  717. const isExpanded = expandedId === item.id;
  718. const isSelected = selectedIds.includes(item.id);
  719. const toggleExpand = (id: string) => {
  720. if (isSelectMode) {
  721. toggleSelect(id);
  722. } else {
  723. setExpandedId(expandedId === id ? null : id);
  724. }
  725. };
  726. const date = new Date(item.timestamp);
  727. const timeStr = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
  728. const dateStr = date.toLocaleDateString();
  729. return (
  730. <TouchableOpacity
  731. activeOpacity={0.9}
  732. onPress={() => toggleExpand(item.id)}
  733. onLongPress={() => handleLongPress(item.id)}
  734. style={[
  735. styles.card,
  736. item.isHealthAlert && styles.alertCard,
  737. isSelected && styles.selectedCard
  738. ]}
  739. >
  740. <View style={styles.cardHeader}>
  741. <View style={styles.labelContainer}>
  742. {isSelectMode ? (
  743. isSelected ? (
  744. <CheckSquare color={Colors.info} size={20} />
  745. ) : (
  746. <Square color={Colors.textSecondary} size={20} />
  747. )
  748. ) : item.isHealthAlert ? (
  749. <AlertTriangle color={Colors.error} size={18} />
  750. ) : (
  751. <CheckCircle color={Colors.success} size={18} />
  752. )}
  753. <Text style={[styles.label, { color: isSelected ? Colors.info : item.isHealthAlert ? Colors.error : Colors.success }]}>
  754. {item.label}
  755. </Text>
  756. </View>
  757. <Text style={styles.confidence}>{(item.confidence * 100).toFixed(1)}% Conf.</Text>
  758. </View>
  759. {isExpanded && item.imageUri && (
  760. <View style={styles.expandedContent}>
  761. <View
  762. style={styles.imageWrapper}
  763. onLayout={(e) => setImgSize({ w: e.nativeEvent.layout.width, h: e.nativeEvent.layout.height })}
  764. >
  765. <Image
  766. source={{ uri: item.imageUri }}
  767. style={styles.detailImage}
  768. resizeMode="contain"
  769. />
  770. {imgSize.w > 0 && (
  771. <DetectionOverlay
  772. detections={item.detections}
  773. containerWidth={imgSize.w}
  774. containerHeight={imgSize.h}
  775. />
  776. )}
  777. </View>
  778. </View>
  779. )}
  780. <View style={styles.cardBody}>
  781. <View style={styles.tallyContainer}>
  782. {Object.entries(item.counts).map(([label, count]: [string, any]) => (
  783. <View key={label} style={styles.tallyItem}>
  784. <Text style={styles.tallyLabel}>{label}:</Text>
  785. <Text style={styles.tallyCount}>{count}</Text>
  786. </View>
  787. ))}
  788. </View>
  789. </View>
  790. <View style={styles.cardFooter}>
  791. <Clock color={Colors.textSecondary} size={14} />
  792. <Text style={styles.footerText}>{dateStr} at {timeStr}</Text>
  793. {item.fileName && (
  794. <Text style={[styles.footerText, { marginLeft: 'auto' }]}>{item.fileName}</Text>
  795. )}
  796. </View>
  797. </TouchableOpacity>
  798. );
  799. };
  800. export const HistoryScreen = () => {
  801. const [history, setHistory] = useState<DetectionRecord[]>([]);
  802. const [refreshing, setRefreshing] = useState(false);
  803. const [expandedId, setExpandedId] = useState<string | null>(null);
  804. const [isSelectMode, setIsSelectMode] = useState(false);
  805. const [selectedIds, setSelectedIds] = useState<string[]>([]);
  806. const fetchHistory = async () => {
  807. const data = await getHistory();
  808. setHistory(data);
  809. };
  810. useFocusEffect(
  811. useCallback(() => {
  812. fetchHistory();
  813. }, [])
  814. );
  815. const onRefresh = async () => {
  816. setRefreshing(true);
  817. await fetchHistory();
  818. setRefreshing(false);
  819. };
  820. const handleClearAll = () => {
  821. Alert.alert(
  822. "Delete All Logs",
  823. "This action will permanently wipe your entire industrial field journal. Are you sure?",
  824. [
  825. { text: "Cancel", style: "cancel" },
  826. {
  827. text: "Delete All",
  828. style: "destructive",
  829. onPress: async () => {
  830. await clearHistory();
  831. setHistory([]);
  832. setIsSelectMode(false);
  833. setSelectedIds([]);
  834. }
  835. }
  836. ]
  837. );
  838. };
  839. const handleDeleteSelected = () => {
  840. Alert.alert(
  841. "Delete Selected",
  842. `Are you sure you want to delete ${selectedIds.length} records?`,
  843. [
  844. { text: "Cancel", style: "cancel" },
  845. {
  846. text: "Delete",
  847. style: "destructive",
  848. onPress: async () => {
  849. await deleteRecords(selectedIds);
  850. setSelectedIds([]);
  851. setIsSelectMode(false);
  852. fetchHistory();
  853. }
  854. }
  855. ]
  856. );
  857. };
  858. const toggleSelect = (id: string) => {
  859. if (selectedIds.includes(id)) {
  860. setSelectedIds(selectedIds.filter((idx: string) => idx !== id));
  861. } else {
  862. setSelectedIds([...selectedIds, id]);
  863. }
  864. };
  865. const toggleExpand = (id: string) => {
  866. if (isSelectMode) {
  867. toggleSelect(id);
  868. } else {
  869. setExpandedId(expandedId === id ? null : id);
  870. }
  871. };
  872. const handleLongPress = (id: string) => {
  873. if (!isSelectMode) {
  874. setIsSelectMode(true);
  875. setSelectedIds([id]);
  876. }
  877. };
  878. const exitSelectionMode = () => {
  879. setIsSelectMode(false);
  880. setSelectedIds([]);
  881. };
  882. const renderItem = ({ item }: { item: DetectionRecord }) => (
  883. <HistoryCard
  884. item={item}
  885. expandedId={expandedId}
  886. setExpandedId={setExpandedId}
  887. toggleSelect={toggleSelect}
  888. isSelectMode={isSelectMode}
  889. selectedIds={selectedIds}
  890. handleLongPress={handleLongPress}
  891. />
  892. );
  893. return (
  894. <View style={styles.container}>
  895. <View style={styles.header}>
  896. <View>
  897. <Text style={styles.title}>Field Journal</Text>
  898. {isSelectMode && (
  899. <Text style={styles.selectionCount}>{selectedIds.length} Selected</Text>
  900. )}
  901. </View>
  902. <View style={styles.headerActions}>
  903. {history.length > 0 && (
  904. isSelectMode ? (
  905. <TouchableOpacity onPress={exitSelectionMode} style={styles.iconButton}>
  906. <X color={Colors.textSecondary} size={24} />
  907. </TouchableOpacity>
  908. ) : (
  909. <>
  910. <TouchableOpacity onPress={handleClearAll} style={styles.clearHeaderButton}>
  911. <Trash2 color={Colors.error} size={20} />
  912. <Text style={styles.clearHeaderText}>Delete All</Text>
  913. </TouchableOpacity>
  914. <TouchableOpacity onPress={() => setIsSelectMode(true)} style={styles.iconButton}>
  915. <CheckSquare color={Colors.textSecondary} size={22} />
  916. </TouchableOpacity>
  917. </>
  918. )
  919. )}
  920. </View>
  921. </View>
  922. {history.length === 0 ? (
  923. <View style={styles.emptyState}>
  924. <Clock color={Colors.textSecondary} size={48} strokeWidth={1} />
  925. <Text style={styles.emptyText}>No detections recorded yet.</Text>
  926. <Text style={styles.emptySubtext}>Perform detections in the Scanner tab to see them here.</Text>
  927. </View>
  928. ) : (
  929. <View style={{ flex: 1 }}>
  930. <FlatList
  931. data={history}
  932. keyExtractor={(item) => item.id}
  933. renderItem={renderItem}
  934. contentContainerStyle={styles.listContent}
  935. refreshControl={
  936. <RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={Colors.success} />
  937. }
  938. />
  939. {isSelectMode && selectedIds.length > 0 && (
  940. <View style={styles.bottomActions}>
  941. <TouchableOpacity
  942. style={styles.deleteSelectionButton}
  943. onPress={handleDeleteSelected}
  944. >
  945. <Trash color="#FFF" size={20} />
  946. <Text style={styles.deleteButtonText}>Delete Selected ({selectedIds.length})</Text>
  947. </TouchableOpacity>
  948. <TouchableOpacity
  949. style={styles.clearAllButton}
  950. onPress={handleClearAll}
  951. >
  952. <Trash2 color="#FFF" size={20} />
  953. <Text style={styles.deleteButtonText}>Delete All</Text>
  954. </TouchableOpacity>
  955. </View>
  956. )}
  957. </View>
  958. )}
  959. </View>
  960. );
  961. };
  962. const styles = StyleSheet.create({
  963. container: {
  964. flex: 1,
  965. backgroundColor: Colors.background,
  966. },
  967. header: {
  968. padding: 24,
  969. paddingBottom: 16,
  970. flexDirection: 'row',
  971. justifyContent: 'space-between',
  972. alignItems: 'center',
  973. },
  974. title: {
  975. color: '#FFF',
  976. fontSize: 28,
  977. fontWeight: 'bold',
  978. },
  979. selectionCount: {
  980. color: Colors.info,
  981. fontSize: 14,
  982. fontWeight: '500',
  983. marginTop: 2,
  984. },
  985. headerActions: {
  986. flexDirection: 'row',
  987. gap: 8,
  988. },
  989. iconButton: {
  990. padding: 8,
  991. backgroundColor: 'rgba(255,255,255,0.05)',
  992. borderRadius: 12,
  993. },
  994. clearButton: {
  995. padding: 8,
  996. },
  997. listContent: {
  998. padding: 16,
  999. paddingTop: 0,
  1000. },
  1001. card: {
  1002. backgroundColor: Colors.surface,
  1003. borderRadius: 16,
  1004. padding: 16,
  1005. marginBottom: 16,
  1006. borderWidth: 1,
  1007. borderColor: 'rgba(255,255,255,0.05)',
  1008. },
  1009. selectedCard: {
  1010. borderColor: Colors.info,
  1011. borderWidth: 2,
  1012. backgroundColor: 'rgba(0, 122, 255, 0.05)',
  1013. },
  1014. alertCard: {
  1015. borderColor: 'rgba(255, 59, 48, 0.3)',
  1016. borderLeftWidth: 4,
  1017. borderLeftColor: Colors.error,
  1018. },
  1019. expandedContent: {
  1020. marginVertical: 12,
  1021. borderRadius: 12,
  1022. overflow: 'hidden',
  1023. backgroundColor: '#000',
  1024. },
  1025. imageWrapper: {
  1026. width: '100%',
  1027. aspectRatio: 1,
  1028. position: 'relative',
  1029. },
  1030. detailImage: {
  1031. width: '100%',
  1032. height: '100%',
  1033. },
  1034. cardHeader: {
  1035. flexDirection: 'row',
  1036. justifyContent: 'space-between',
  1037. alignItems: 'center',
  1038. marginBottom: 12,
  1039. },
  1040. labelContainer: {
  1041. flexDirection: 'row',
  1042. alignItems: 'center',
  1043. gap: 8,
  1044. },
  1045. label: {
  1046. fontSize: 18,
  1047. fontWeight: 'bold',
  1048. },
  1049. confidence: {
  1050. color: Colors.textSecondary,
  1051. fontSize: 14,
  1052. },
  1053. cardBody: {
  1054. paddingVertical: 12,
  1055. borderTopWidth: 1,
  1056. borderBottomWidth: 1,
  1057. borderColor: 'rgba(255,255,255,0.05)',
  1058. },
  1059. tallyContainer: {
  1060. flexDirection: 'row',
  1061. flexWrap: 'wrap',
  1062. gap: 12,
  1063. },
  1064. tallyItem: {
  1065. flexDirection: 'row',
  1066. gap: 4,
  1067. },
  1068. tallyLabel: {
  1069. color: Colors.textSecondary,
  1070. fontSize: 12,
  1071. },
  1072. tallyCount: {
  1073. color: '#FFF',
  1074. fontSize: 12,
  1075. fontWeight: 'bold',
  1076. },
  1077. cardFooter: {
  1078. flexDirection: 'row',
  1079. alignItems: 'center',
  1080. gap: 6,
  1081. marginTop: 12,
  1082. },
  1083. footerText: {
  1084. color: Colors.textSecondary,
  1085. fontSize: 12,
  1086. },
  1087. emptyState: {
  1088. flex: 1,
  1089. justifyContent: 'center',
  1090. alignItems: 'center',
  1091. padding: 32,
  1092. },
  1093. emptyText: {
  1094. color: '#FFF',
  1095. fontSize: 18,
  1096. fontWeight: 'bold',
  1097. marginTop: 16,
  1098. },
  1099. emptySubtext: {
  1100. color: Colors.textSecondary,
  1101. textAlign: 'center',
  1102. marginTop: 8,
  1103. },
  1104. bottomActions: {
  1105. position: 'absolute',
  1106. bottom: 24,
  1107. left: 24,
  1108. right: 24,
  1109. backgroundColor: Colors.error,
  1110. borderRadius: 16,
  1111. elevation: 8,
  1112. shadowColor: '#000',
  1113. shadowOffset: { width: 0, height: 4 },
  1114. shadowOpacity: 0.3,
  1115. shadowRadius: 8,
  1116. flexDirection: 'row',
  1117. overflow: 'hidden',
  1118. },
  1119. deleteSelectionButton: {
  1120. flexDirection: 'row',
  1121. alignItems: 'center',
  1122. justifyContent: 'center',
  1123. padding: 16,
  1124. gap: 12,
  1125. flex: 1.5,
  1126. },
  1127. deleteButtonText: {
  1128. color: '#FFF',
  1129. fontSize: 14,
  1130. fontWeight: 'bold',
  1131. },
  1132. clearHeaderButton: {
  1133. flexDirection: 'row',
  1134. alignItems: 'center',
  1135. gap: 6,
  1136. paddingVertical: 8,
  1137. paddingHorizontal: 12,
  1138. backgroundColor: 'rgba(255, 59, 48, 0.1)',
  1139. borderRadius: 12,
  1140. },
  1141. clearHeaderText: {
  1142. color: Colors.error,
  1143. fontSize: 12,
  1144. fontWeight: 'bold',
  1145. },
  1146. clearAllButton: {
  1147. flexDirection: 'row',
  1148. alignItems: 'center',
  1149. justifyContent: 'center',
  1150. padding: 16,
  1151. gap: 12,
  1152. borderLeftWidth: 1,
  1153. borderLeftColor: 'rgba(255,255,255,0.2)',
  1154. flex: 1,
  1155. },
  1156. });
  1157. ==================================================
  1158. FILE: E:\Task\Research and Development\palm-oil-ai\mobile\src\screens\ScannerScreen.tsx
  1159. ==================================================
  1160. import React, { useState, useEffect } from 'react';
  1161. import { StyleSheet, View, Text, StatusBar, SafeAreaView, TouchableOpacity, Image } from 'react-native';
  1162. import { useIsFocused } from '@react-navigation/native';
  1163. import { Camera, useCameraDevice, useCameraPermission, useFrameProcessor, useCameraFormat } from 'react-native-vision-camera';
  1164. import { useTensorflowModel } from 'react-native-fast-tflite';
  1165. import { runOnJS } from 'react-native-reanimated';
  1166. import { launchImageLibrary } from 'react-native-image-picker';
  1167. import { parseYoloResults, calculateTally, BoundingBox } from '../utils/yoloParser';
  1168. import { saveDetectionRecord } from '../utils/storage';
  1169. import { DetectionOverlay } from '../components/DetectionOverlay';
  1170. import { TallyDashboard } from '../components/TallyDashboard';
  1171. import { Colors } from '../theme';
  1172. import { Image as ImageIcon, Upload } from 'lucide-react-native';
  1173. export const ScannerScreen = ({ route }: any) => {
  1174. const isFocused = useIsFocused();
  1175. const { hasPermission, requestPermission } = useCameraPermission();
  1176. const device = useCameraDevice('back');
  1177. const [detections, setDetections] = useState<BoundingBox[]>([]);
  1178. const [counts, setCounts] = useState<Record<string, number>>({});
  1179. const [cameraInitialized, setCameraInitialized] = useState(false);
  1180. const [lastSavedTime, setLastSavedTime] = useState(0);
  1181. // Load the model
  1182. const model = useTensorflowModel(require('../../assets/best.tflite'));
  1183. // Find a format that matches 640x640 or closest small resolution
  1184. const format = useCameraFormat(device, [
  1185. { videoResolution: { width: 640, height: 480 } },
  1186. { fps: 30 }
  1187. ]);
  1188. useEffect(() => {
  1189. if (!hasPermission) {
  1190. requestPermission();
  1191. }
  1192. }, [hasPermission]);
  1193. const frameProcessor = useFrameProcessor((frame) => {
  1194. 'worklet';
  1195. if (model.state === 'loaded') {
  1196. try {
  1197. // FALLBACK: Without the resize plugin, we pass the raw buffer.
  1198. // Fast-TFLite might handle resizing if we are lucky with the input.
  1199. // In the next step, we will select a 640x480 format to get closer to 640x640.
  1200. const buffer = frame.toArrayBuffer();
  1201. const result = model.model.runSync([new Int8Array(buffer)]);
  1202. const boxes = parseYoloResults(result[0], frame.width, frame.height);
  1203. runOnJS(setDetections)(boxes);
  1204. const currentCounts = calculateTally(boxes);
  1205. runOnJS(setCounts)(currentCounts);
  1206. if (boxes.length > 0) {
  1207. runOnJS(handleAutoSave)(boxes, currentCounts);
  1208. }
  1209. } catch (e) {
  1210. console.error('AI Inference Detail:', e);
  1211. }
  1212. }
  1213. }, [model]);
  1214. const handleAutoSave = (boxes: BoundingBox[], currentCounts: Record<string, number>) => {
  1215. const now = Date.now();
  1216. if (now - lastSavedTime > 5000) {
  1217. const topDet = boxes.reduce((prev, current) => (prev.confidence > current.confidence) ? prev : current);
  1218. saveDetectionRecord({
  1219. label: topDet.label,
  1220. confidence: topDet.confidence,
  1221. classId: topDet.classId,
  1222. detections: boxes,
  1223. counts: currentCounts
  1224. });
  1225. setLastSavedTime(now);
  1226. }
  1227. };
  1228. if (!hasPermission) return (
  1229. <View style={[styles.container, { backgroundColor: Colors.error, justifyContent: 'center' }]}>
  1230. <Text style={styles.text}>ERROR: No Camera Permission</Text>
  1231. </View>
  1232. );
  1233. if (!device) return (
  1234. <View style={[styles.container, { backgroundColor: Colors.info, justifyContent: 'center' }]}>
  1235. <Text style={styles.text}>ERROR: No Camera Device Found</Text>
  1236. </View>
  1237. );
  1238. return (
  1239. <View style={styles.container}>
  1240. <StatusBar barStyle="light-content" />
  1241. {isFocused && (
  1242. <Camera
  1243. style={StyleSheet.absoluteFill}
  1244. device={device}
  1245. isActive={isFocused}
  1246. frameProcessor={frameProcessor}
  1247. format={format}
  1248. pixelFormat="rgb"
  1249. onInitialized={() => {
  1250. console.log('Camera: Initialized');
  1251. setCameraInitialized(true);
  1252. }}
  1253. onError={(error) => console.error('Camera: Error', error)}
  1254. />
  1255. )}
  1256. <SafeAreaView style={styles.overlay} pointerEvents="none">
  1257. <View style={[styles.header, { backgroundColor: 'rgba(15, 23, 42, 0.6)' }]}>
  1258. <Text style={styles.title}>Live Scanner</Text>
  1259. <Text style={styles.status}>
  1260. {model.state === 'loaded' ? '● AI ACTIVE' : `○ ${model.state.toUpperCase()}`}
  1261. </Text>
  1262. </View>
  1263. <DetectionOverlay detections={detections} />
  1264. <TallyDashboard counts={counts} />
  1265. </SafeAreaView>
  1266. <View style={styles.debugBox}>
  1267. <Text style={styles.debugText}>
  1268. Cam: {cameraInitialized ? 'READY' : 'STARTING...'} |
  1269. Model: {model.state.toUpperCase()} |
  1270. Dets: {detections.length}
  1271. </Text>
  1272. </View>
  1273. </View>
  1274. );
  1275. };
  1276. const styles = StyleSheet.create({
  1277. container: {
  1278. flex: 1,
  1279. backgroundColor: Colors.background,
  1280. },
  1281. overlay: {
  1282. flex: 1,
  1283. },
  1284. header: {
  1285. padding: 16,
  1286. flexDirection: 'row',
  1287. justifyContent: 'space-between',
  1288. alignItems: 'center',
  1289. },
  1290. title: {
  1291. color: '#FFF',
  1292. fontSize: 16,
  1293. fontWeight: 'bold',
  1294. letterSpacing: 0.5,
  1295. },
  1296. status: {
  1297. color: Colors.success,
  1298. fontSize: 11,
  1299. fontWeight: '800',
  1300. },
  1301. text: {
  1302. color: '#FFF',
  1303. textAlign: 'center',
  1304. fontSize: 18,
  1305. fontWeight: 'bold',
  1306. },
  1307. galleryButton: {
  1308. position: 'absolute',
  1309. bottom: 100,
  1310. right: 20,
  1311. backgroundColor: 'rgba(30, 41, 59, 0.8)',
  1312. padding: 16,
  1313. borderRadius: 30,
  1314. borderWidth: 1,
  1315. borderColor: 'rgba(255,255,255,0.2)',
  1316. },
  1317. debugBox: {
  1318. position: 'absolute',
  1319. top: 60,
  1320. left: 20,
  1321. right: 20,
  1322. backgroundColor: 'rgba(255,255,255,0.9)',
  1323. padding: 8,
  1324. borderRadius: 8,
  1325. },
  1326. debugText: {
  1327. color: '#000',
  1328. fontSize: 12,
  1329. fontWeight: '600',
  1330. textAlign: 'center',
  1331. }
  1332. });
  1333. ==================================================
  1334. FILE: E:\Task\Research and Development\palm-oil-ai\mobile\src\theme\index.ts
  1335. ==================================================
  1336. export const Colors = {
  1337. // Industrial Alert Colors
  1338. error: '#FF3B30', // High-visibility Red for Abnormal/Empty_Bunch
  1339. warning: '#FFCC00', // Yellow for Penalty/Underripe
  1340. success: '#34C759', // Green for Ripe
  1341. info: '#007AFF', // Blue for Overripe (processing focus)
  1342. // Base Palette
  1343. background: '#0F172A', // Deep Slate
  1344. surface: '#1E293B',
  1345. text: '#F8FAFC',
  1346. textSecondary: '#94A3B8',
  1347. // Class Mapping Colors
  1348. classes: {
  1349. 0: '#FF3B30', // Empty_Bunch (Alert)
  1350. 1: '#FFCC00', // Underripe (Warning)
  1351. 2: '#FF3B30', // Abnormal (Health Alert)
  1352. 3: '#34C759', // Ripe (Success)
  1353. 4: '#FF9500', // Unripe (Penalty)
  1354. 5: '#AF52DE', // Overripe (FFA Prevention)
  1355. }
  1356. };
  1357. export const Typography = {
  1358. header: {
  1359. fontSize: 24,
  1360. fontWeight: 'bold',
  1361. color: Colors.text,
  1362. },
  1363. body: {
  1364. fontSize: 16,
  1365. color: Colors.textSecondary,
  1366. },
  1367. label: {
  1368. fontSize: 12,
  1369. fontWeight: '600',
  1370. textTransform: 'uppercase',
  1371. }
  1372. };
  1373. ==================================================
  1374. FILE: E:\Task\Research and Development\palm-oil-ai\mobile\src\utils\storage.ts
  1375. ==================================================
  1376. import AsyncStorage from '@react-native-async-storage/async-storage';
  1377. import { BoundingBox } from './yoloParser';
  1378. export interface DetectionRecord {
  1379. id: string;
  1380. timestamp: string;
  1381. label: string;
  1382. confidence: number;
  1383. classId: number;
  1384. isHealthAlert: boolean;
  1385. imageUri?: string;
  1386. fileName?: string;
  1387. detections: BoundingBox[];
  1388. counts: Record<string, number>;
  1389. }
  1390. const STORAGE_KEY = 'palm_history';
  1391. /**
  1392. * Saves a new detection record to local storage.
  1393. */
  1394. export const saveDetectionRecord = async (record: Omit<DetectionRecord, 'id' | 'timestamp' | 'isHealthAlert'>) => {
  1395. try {
  1396. const existing = await AsyncStorage.getItem(STORAGE_KEY);
  1397. const history: DetectionRecord[] = existing ? JSON.parse(existing) : [];
  1398. const newRecord: DetectionRecord = {
  1399. ...record,
  1400. id: Date.now().toString(),
  1401. timestamp: new Date().toISOString(),
  1402. isHealthAlert: record.detections.some(d => d.classId === 0 || d.classId === 2)
  1403. };
  1404. await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify([newRecord, ...history]));
  1405. console.log('Storage: Record saved successfully');
  1406. } catch (error) {
  1407. console.error('Storage: Error saving record', error);
  1408. }
  1409. };
  1410. /**
  1411. * Retrieves all detection records from local storage.
  1412. */
  1413. export const getHistory = async (): Promise<DetectionRecord[]> => {
  1414. try {
  1415. const existing = await AsyncStorage.getItem(STORAGE_KEY);
  1416. return existing ? JSON.parse(existing) : [];
  1417. } catch (error) {
  1418. console.error('Storage: Error fetching history', error);
  1419. return [];
  1420. }
  1421. };
  1422. /**
  1423. * Clears all detection records from local storage.
  1424. */
  1425. export const clearHistory = async () => {
  1426. try {
  1427. await AsyncStorage.removeItem(STORAGE_KEY);
  1428. console.log('Storage: History cleared');
  1429. } catch (error) {
  1430. console.error('Storage: Error clearing history', error);
  1431. }
  1432. };
  1433. /**
  1434. * Deletes specific records from local storage.
  1435. */
  1436. export const deleteRecords = async (ids: string[]) => {
  1437. try {
  1438. const existing = await AsyncStorage.getItem(STORAGE_KEY);
  1439. if (!existing) return;
  1440. const history: DetectionRecord[] = JSON.parse(existing);
  1441. const updated = history.filter(record => !ids.includes(record.id));
  1442. await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
  1443. console.log(`Storage: ${ids.length} records deleted`);
  1444. } catch (error) {
  1445. console.error('Storage: Error deleting records', error);
  1446. }
  1447. };
  1448. ==================================================
  1449. FILE: E:\Task\Research and Development\palm-oil-ai\mobile\src\utils\yoloParser.ts
  1450. ==================================================
  1451. export interface BoundingBox {
  1452. id: string;
  1453. x: number;
  1454. y: number;
  1455. width: number;
  1456. height: number;
  1457. relX: number;
  1458. relY: number;
  1459. relWidth: number;
  1460. relHeight: number;
  1461. label: string;
  1462. confidence: number;
  1463. classId: number;
  1464. }
  1465. const CLASS_NAMES = [
  1466. 'Empty_Bunch',
  1467. 'Underripe',
  1468. 'Abnormal',
  1469. 'Ripe',
  1470. 'Unripe',
  1471. 'Overripe'
  1472. ];
  1473. /**
  1474. * Parses YOLOv8/v11 output tensor into BoundingBox objects.
  1475. * Format: [x1, y1, x2, y2, score, classId]
  1476. * Quantization: scale=0.019916336983442307, zeroPoint=-124
  1477. */
  1478. /**
  1479. * Normalizes a raw pixel buffer to 0.0-1.0 range for Float32 models.
  1480. */
  1481. export function normalizeTensor(buffer: ArrayBuffer, width: number, height: number): Float32Array {
  1482. 'worklet';
  1483. const data = new Uint8Array(buffer);
  1484. const normalized = new Float32Array(width * height * 3);
  1485. for (let i = 0; i < data.length; i++) {
  1486. normalized[i] = data[i] / 255.0;
  1487. }
  1488. return normalized;
  1489. }
  1490. export function parseYoloResults(
  1491. tensor: Int8Array | Uint8Array | Float32Array | any,
  1492. frameWidth: number,
  1493. frameHeight: number
  1494. ): BoundingBox[] {
  1495. 'worklet';
  1496. // Detection parameters from INT8 model
  1497. const scale = 0.019916336983442307;
  1498. const zeroPoint = -124;
  1499. const numDetections = 300;
  1500. const numElements = 6;
  1501. const detections: BoundingBox[] = [];
  1502. const data = tensor;
  1503. if (!data || data.length === 0) return [];
  1504. for (let i = 0; i < numDetections; i++) {
  1505. const base = i * numElements;
  1506. if (base + 5 >= data.length) break;
  1507. // Handle Float32 vs Quantized Int8
  1508. const getVal = (idx: number) => {
  1509. const val = data[idx];
  1510. if (data instanceof Float32Array) return val;
  1511. return (val - zeroPoint) * scale;
  1512. };
  1513. const x1 = getVal(base + 0);
  1514. const y1 = getVal(base + 1);
  1515. const x2 = getVal(base + 2);
  1516. const y2 = getVal(base + 3);
  1517. const score = getVal(base + 4);
  1518. const classId = Math.round(getVal(base + 5));
  1519. if (score > 0.45 && classId >= 0 && classId < CLASS_NAMES.length) {
  1520. const normalizedX1 = x1 / 640;
  1521. const normalizedY1 = y1 / 640;
  1522. const normalizedX2 = x2 / 640;
  1523. const normalizedY2 = y2 / 640;
  1524. detections.push({
  1525. id: `det_${i}_${Math.random().toString(36).substr(2, 9)}`,
  1526. x: Math.max(0, normalizedX1 * frameWidth),
  1527. y: Math.max(0, normalizedY1 * frameHeight),
  1528. width: Math.max(0, (normalizedX2 - normalizedX1) * frameWidth),
  1529. height: Math.max(0, (normalizedY2 - normalizedY1) * frameHeight),
  1530. relX: normalizedX1,
  1531. relY: normalizedY1,
  1532. relWidth: normalizedX2 - normalizedX1,
  1533. relHeight: normalizedY2 - normalizedY1,
  1534. label: CLASS_NAMES[classId],
  1535. confidence: score,
  1536. classId: classId
  1537. });
  1538. }
  1539. }
  1540. return detections;
  1541. }
  1542. export function calculateTally(detections: BoundingBox[]) {
  1543. 'worklet';
  1544. const counts: { [key: string]: number } = {};
  1545. for (const det of detections) {
  1546. counts[det.label] = (counts[det.label] || 0) + 1;
  1547. }
  1548. return counts;
  1549. }