Forráskód Böngészése

live analysis screen

Dr-Swopt 1 hete
szülő
commit
e5fa9b926e

+ 3 - 7
palm_oil_mobile/lib/screens/analysis_screen.dart

@@ -206,7 +206,7 @@ class _AnalysisScreenState extends State<AnalysisScreen> with SingleTickerProvid
 
   Widget _buildBoundingBox(DetectionResult detection, BoxConstraints constraints) {
     final rect = detection.normalizedBox;
-    final color = _getStatusColor(detection.className);
+    final color = detection.getStatusColor();
 
     return Positioned(
       left: rect.left * constraints.maxWidth,
@@ -233,11 +233,7 @@ class _AnalysisScreenState extends State<AnalysisScreen> with SingleTickerProvid
     );
   }
 
-  Color _getStatusColor(String label) {
-    if (label == 'Empty_Bunch' || label == 'Abnormal') return Colors.red;
-    if (label == 'Ripe' || label == 'Overripe') return Colors.green;
-    return Colors.orange;
-  }
+  // Removed _getStatusColor local method in favor of DetectionResult.getStatusColor()
 
   Widget _buildResultSummary() {
     final best = _detections!.first;
@@ -254,7 +250,7 @@ class _AnalysisScreenState extends State<AnalysisScreen> with SingleTickerProvid
           Row(
             mainAxisAlignment: MainAxisAlignment.center,
             children: [
-              Icon(Icons.check_circle, color: _getStatusColor(best.className), size: 32),
+              Icon(Icons.check_circle, color: best.getStatusColor(), size: 32),
               const SizedBox(width: 12),
               Text(
                 best.className,

+ 13 - 0
palm_oil_mobile/lib/screens/home_screen.dart

@@ -1,5 +1,6 @@
 import 'package:flutter/material.dart';
 import 'analysis_screen.dart';
+import 'live_analysis_screen.dart';
 import 'history_screen.dart';
 
 class HomeScreen extends StatelessWidget {
@@ -37,6 +38,18 @@ class HomeScreen extends StatelessWidget {
                 ),
               ),
               const SizedBox(height: 24),
+              _buildNavCard(
+                context,
+                title: 'Live Inference',
+                subtitle: 'Real-time "Point-and-Scan"',
+                icon: Icons.camera,
+                color: Colors.orange,
+                onTap: () => Navigator.push(
+                  context,
+                  MaterialPageRoute(builder: (context) => const LiveAnalysisScreen()),
+                ),
+              ),
+              const SizedBox(height: 24),
               _buildNavCard(
                 context,
                 title: 'History Vault',

+ 375 - 0
palm_oil_mobile/lib/screens/live_analysis_screen.dart

@@ -0,0 +1,375 @@
+import 'dart:io';
+import 'dart:ui';
+import 'package:flutter/material.dart';
+import 'package:camera/camera.dart';
+import 'package:permission_handler/permission_handler.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:path/path.dart' as p;
+import '../services/tflite_service.dart';
+import '../services/database_helper.dart';
+import '../models/palm_record.dart';
+
+class LiveAnalysisScreen extends StatefulWidget {
+  const LiveAnalysisScreen({super.key});
+
+  @override
+  State<LiveAnalysisScreen> createState() => _LiveAnalysisScreenState();
+}
+
+class _LiveAnalysisScreenState extends State<LiveAnalysisScreen> {
+  CameraController? _controller;
+  final TfliteService _tfliteService = TfliteService();
+  final DatabaseHelper _dbHelper = DatabaseHelper();
+
+  bool _isInitialized = false;
+  bool _isProcessing = false;
+  int _frameCount = 0;
+  List<DetectionResult>? _detections;
+  
+  // Detection Lock Logic
+  bool _isLocked = false;
+  static const double _lockThreshold = 0.75;
+  static const int _frameThrottle = 10; // Process every 10th frame
+
+  @override
+  void initState() {
+    super.initState();
+    _initializeCamera();
+  }
+
+  Future<void> _initializeCamera() async {
+    final status = await Permission.camera.request();
+    if (status.isDenied) return;
+
+    final cameras = await availableCameras();
+    if (cameras.isEmpty) return;
+
+    _controller = CameraController(
+      cameras[0],
+      ResolutionPreset.medium,
+      enableAudio: false,
+      imageFormatGroup: Platform.isAndroid ? ImageFormatGroup.yuv420 : ImageFormatGroup.bgra8888,
+    );
+
+    try {
+      await _controller!.initialize();
+      await _tfliteService.initModel();
+
+      _controller!.startImageStream((image) {
+        if (_isProcessing) return;
+        _frameCount++;
+        if (_frameCount % _frameThrottle != 0) return;
+
+        _processStreamFrame(image);
+      });
+
+      if (mounted) {
+        setState(() {
+          _isInitialized = true;
+        });
+      }
+    } catch (e) {
+      print("Camera init error: $e");
+    }
+  }
+
+  Future<void> _processStreamFrame(CameraImage image) async {
+    setState(() => _isProcessing = true);
+    try {
+      final detections = await _tfliteService.runInferenceOnStream(image);
+      
+      bool locked = false;
+      if (detections.isNotEmpty) {
+        locked = detections.any((d) => d.confidence > _lockThreshold);
+      }
+
+      if (mounted) {
+        setState(() {
+          _detections = detections;
+          _isLocked = locked;
+        });
+      }
+    } catch (e) {
+      print("Stream processing error: $e");
+    } finally {
+      _isProcessing = false;
+    }
+  }
+
+  Future<void> _captureAndAnalyze() async {
+    if (_controller == null || !_controller!.value.isInitialized) return;
+
+    // 1. Stop stream to avoid resource conflict
+    await _controller!.stopImageStream();
+    
+    // Show loading dialog
+    if (!mounted) return;
+    _showLoadingDialog();
+
+    try {
+      // 2. Take high-res picture
+      final XFile photo = await _controller!.takePicture();
+      
+      // 3. Run final inference on high-res
+      final detections = await _tfliteService.runInference(photo.path);
+
+      if (detections.isNotEmpty) {
+        // 4. Archive
+        final appDocDir = await getApplicationDocumentsDirectory();
+        final fileName = p.basename(photo.path);
+        final persistentPath = p.join(appDocDir.path, 'palm_live_${DateTime.now().millisecondsSinceEpoch}_$fileName');
+        await File(photo.path).copy(persistentPath);
+
+        final best = detections.first;
+        final record = PalmRecord(
+          imagePath: persistentPath,
+          ripenessClass: best.className,
+          confidence: best.confidence,
+          timestamp: DateTime.now(),
+          x1: best.normalizedBox.left,
+          y1: best.normalizedBox.top,
+          x2: best.normalizedBox.right,
+          y2: best.normalizedBox.bottom,
+          detections: detections.map((d) => {
+            'className': d.className,
+            'classIndex': d.classIndex,
+            'confidence': d.confidence,
+            'x1': d.normalizedBox.left,
+            'y1': d.normalizedBox.top,
+            'x2': d.normalizedBox.right,
+            'y2': d.normalizedBox.bottom,
+          }).toList(),
+        );
+
+        await _dbHelper.insertRecord(record);
+
+        // 5. Show result and resume camera
+        if (mounted) {
+          Navigator.of(context).pop(); // Close loading
+          _showResultSheet(record);
+        }
+      } else {
+         if (mounted) {
+          Navigator.of(context).pop();
+          ScaffoldMessenger.of(context).showSnackBar(
+            const SnackBar(content: Text("No palm bunches detected in final snap."))
+          );
+        }
+      }
+    } catch (e) {
+      if (mounted) Navigator.of(context).pop();
+      print("Capture error: $e");
+    } finally {
+      // Restart stream
+      _controller!.startImageStream((image) {
+        if (_isProcessing) return;
+        _frameCount++;
+        if (_frameCount % _frameThrottle != 0) return;
+        _processStreamFrame(image);
+      });
+    }
+  }
+
+  void _showLoadingDialog() {
+    showDialog(
+      context: context,
+      barrierDismissible: false,
+      builder: (context) => const Center(
+        child: Column(
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            CircularProgressIndicator(color: Colors.white),
+            SizedBox(height: 16),
+            Text("Final Grading...", style: TextStyle(color: Colors.white)),
+          ],
+        ),
+      ),
+    );
+  }
+
+  void _showResultSheet(PalmRecord record) {
+    // Determine color based on ripeness class
+    Color statusColor = const Color(0xFFFF9800); // Default orange
+    if (record.ripenessClass == 'Empty_Bunch' || record.ripenessClass == 'Abnormal') {
+      statusColor = const Color(0xFFF44336);
+    } else if (record.ripenessClass == 'Ripe' || record.ripenessClass == 'Overripe') {
+      statusColor = const Color(0xFF4CAF50);
+    }
+
+    showModalBottomSheet(
+      context: context,
+      isScrollControlled: true,
+      shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20))),
+      builder: (context) => Container(
+        padding: const EdgeInsets.all(24),
+        child: Column(
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            Icon(Icons.check_circle, color: statusColor, size: 64),
+            const SizedBox(height: 16),
+            Text(record.ripenessClass, style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
+            Text("Confidence: ${(record.confidence * 100).toStringAsFixed(1)}%", style: const TextStyle(color: Colors.grey)),
+            const SizedBox(height: 24),
+            if (record.ripenessClass == 'Empty_Bunch' || record.ripenessClass == 'Abnormal')
+              Container(
+                padding: const EdgeInsets.all(12),
+                decoration: BoxDecoration(color: Colors.red.shade50, borderRadius: BorderRadius.circular(8)),
+                child: const Text("HEALTH ALERT: Abnormal detected!", style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold)),
+              ),
+            const SizedBox(height: 24),
+            SizedBox(
+              width: double.infinity,
+              child: ElevatedButton(
+                onPressed: () => Navigator.pop(context),
+                child: const Text("Done"),
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    if (!_isInitialized || _controller == null) {
+      return const Scaffold(body: Center(child: CircularProgressIndicator()));
+    }
+
+    return Scaffold(
+      backgroundColor: Colors.black,
+      body: Stack(
+        children: [
+          // Camera Preview
+          Center(
+            child: CameraPreview(_controller!),
+          ),
+          
+          // Bounding Box Overlays
+          if (_detections != null)
+            Positioned.fill(
+              child: LayoutBuilder(
+                builder: (context, constraints) {
+                  return Stack(
+                    children: _detections!
+                        .map((d) => _buildOverlayBox(d, constraints))
+                        .toList(),
+                  );
+                },
+              ),
+            ),
+
+          // Top Info Bar
+          Positioned(
+            top: 40,
+            left: 20,
+            right: 20,
+            child: Container(
+              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+              decoration: BoxDecoration(
+                color: Colors.black54,
+                borderRadius: BorderRadius.circular(20),
+              ),
+              child: Row(
+                children: [
+                  Icon(
+                    _isLocked ? Icons.lock : Icons.center_focus_weak,
+                    color: _isLocked ? Colors.green : Colors.yellow,
+                  ),
+                  const SizedBox(width: 8),
+                  Text(
+                    _isLocked ? "LOCKED" : "TARGETING...",
+                    style: TextStyle(
+                      color: _isLocked ? Colors.green : Colors.yellow,
+                      fontWeight: FontWeight.bold,
+                    ),
+                  ),
+                  const Spacer(),
+                  IconButton(
+                    icon: const Icon(Icons.close, color: Colors.white),
+                    onPressed: () => Navigator.pop(context),
+                  ),
+                ],
+              ),
+            ),
+          ),
+
+          // Bottom Controls
+          Positioned(
+            bottom: 40,
+            left: 0,
+            right: 0,
+            child: Center(
+              child: GestureDetector(
+                onTap: _isLocked ? _captureAndAnalyze : null,
+                child: Container(
+                  width: 80,
+                  height: 80,
+                  decoration: BoxDecoration(
+                    shape: BoxShape.circle,
+                    border: Border.all(color: Colors.white, width: 4),
+                    color: _isLocked ? Colors.green.withValues(alpha: 0.8) : Colors.white24,
+                  ),
+                  child: Icon(
+                    _isLocked ? Icons.camera_alt : Icons.hourglass_empty,
+                    color: Colors.white,
+                    size: 32,
+                  ),
+                ),
+              ),
+            ),
+          ),
+          
+          if (!_isLocked)
+            const Positioned(
+              bottom: 130,
+              left: 0,
+              right: 0,
+              child: Center(
+                child: Text(
+                  "Hold steady to lock target",
+                  style: TextStyle(color: Colors.white, fontWeight: FontWeight.w500),
+                ),
+              ),
+            ),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildOverlayBox(DetectionResult detection, BoxConstraints constraints) {
+    final rect = detection.normalizedBox;
+    final color = detection.confidence > _lockThreshold ? Colors.green : Colors.yellow;
+
+    return Positioned(
+      left: rect.left * constraints.maxWidth,
+      top: rect.top * constraints.maxHeight,
+      width: rect.width * constraints.maxWidth,
+      height: rect.height * constraints.maxHeight,
+      child: Container(
+        decoration: BoxDecoration(
+          border: Border.all(color: color, width: 2),
+          borderRadius: BorderRadius.circular(4),
+        ),
+        child: Align(
+          alignment: Alignment.topLeft,
+          child: Container(
+            padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
+            color: color,
+            child: Text(
+              "${(detection.confidence * 100).toStringAsFixed(0)}%",
+              style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+
+  @override
+  void dispose() {
+    _controller?.dispose();
+    _tfliteService.dispose();
+    super.dispose();
+  }
+}

+ 150 - 29
palm_oil_mobile/lib/services/tflite_service.dart

@@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
 import 'package:image/image.dart' as img;
 import 'package:image_picker/image_picker.dart';
 import 'package:tflite_flutter/tflite_flutter.dart';
+import 'package:camera/camera.dart';
 
 /// A detection result parsed from the model's end-to-end output.
 class DetectionResult {
@@ -21,6 +22,12 @@ class DetectionResult {
     required this.confidence,
     required this.normalizedBox,
   });
+
+  Color getStatusColor() {
+    if (className == 'Empty_Bunch' || className == 'Abnormal') return const Color(0xFFF44336); // Colors.red
+    if (className == 'Ripe' || className == 'Overripe') return const Color(0xFF4CAF50); // Colors.green
+    return const Color(0xFFFF9800); // Colors.orange
+  }
 }
 
 /// Custom TFLite inference service that correctly decodes the end-to-end
@@ -82,6 +89,148 @@ class TfliteService {
     return await _runInferenceInIsolate(imageBytes);
   }
 
+  /// Run inference on a [CameraImage] from the stream.
+  /// Throttled by the caller.
+  Future<List<DetectionResult>> runInferenceOnStream(CameraImage image) async {
+    if (!_isInitialized) await initModel();
+
+    // We pass the CameraImage planes to the isolate for conversion and inference.
+    return await compute(_inferenceStreamTaskWrapper, {
+      'planes': image.planes.map((p) => {
+        'bytes': p.bytes,
+        'bytesPerRow': p.bytesPerRow,
+        'bytesPerPixel': p.bytesPerPixel,
+      }).toList(),
+      'width': image.width,
+      'height': image.height,
+      'format': image.format.group,
+      'modelBytes': (await rootBundle.load('assets/$_modelAsset')).buffer.asUint8List(),
+      'labelData': await rootBundle.loadString('assets/$_labelsAsset'),
+    });
+  }
+
+  static List<DetectionResult> _inferenceStreamTaskWrapper(Map<String, dynamic> args) {
+    // This is a simplified wrapper for stream inference in isolate
+    final modelBytes = args['modelBytes'] as Uint8List;
+    final labelData = args['labelData'] as String;
+    final planes = args['planes'] as List<dynamic>;
+    final width = args['width'] as int;
+    final height = args['height'] as int;
+    
+    final interpreter = Interpreter.fromBuffer(modelBytes);
+    final labels = labelData.split('\n').where((l) => l.trim().isNotEmpty).map((l) => l.trim()).toList();
+
+    try {
+      // Manual YUV to RGB conversion if needed, or use image package if possible
+      // For speed in stream, we might want a more optimized conversion.
+      // But for now, let's use a basic one or the image package.
+      
+      img.Image? image;
+      if (args['format'] == ImageFormatGroup.yuv420) {
+        // Simple YUV420 to RGB (this is slow in Dart, but better in isolate)
+        image = _convertYUV420ToImage(planes, width, height);
+      } else if (args['format'] == ImageFormatGroup.bgra8888) {
+        image = img.Image.fromBytes(
+          width: width,
+          height: height,
+          bytes: planes[0]['bytes'].buffer,
+          format: img.Format.uint8,
+          numChannels: 4,
+          order: img.ChannelOrder.bgra,
+        );
+      }
+
+      if (image == null) return [];
+
+      // Resize and Run
+      final resized = img.copyResize(image, width: _inputSize, height: _inputSize);
+      
+      final inputTensor = List.generate(1, (_) =>
+        List.generate(_inputSize, (y) =>
+          List.generate(_inputSize, (x) {
+            final pixel = resized.getPixel(x, y);
+            return [pixel.r / 255.0, pixel.g / 255.0, pixel.b / 255.0];
+          })
+        )
+      );
+
+      final outputShape = interpreter.getOutputTensors()[0].shape;
+      final outputTensor = List.generate(1, (_) =>
+        List.generate(outputShape[1], (_) =>
+          List<double>.filled(outputShape[2], 0.0)
+        )
+      );
+
+      interpreter.run(inputTensor, outputTensor);
+      return _decodeDetections(outputTensor[0], labels);
+    } finally {
+      interpreter.close();
+    }
+  }
+
+  static img.Image _convertYUV420ToImage(List<dynamic> planes, int width, int height) {
+    final yPlane = planes[0];
+    final uPlane = planes[1];
+    final vPlane = planes[2];
+
+    final yBytes = yPlane['bytes'] as Uint8List;
+    final uBytes = uPlane['bytes'] as Uint8List;
+    final vBytes = vPlane['bytes'] as Uint8List;
+
+    final yRowStride = yPlane['bytesPerRow'] as int;
+    final uvRowStride = uPlane['bytesPerRow'] as int;
+    final uvPixelStride = uPlane['bytesPerPixel'] as int;
+
+    final image = img.Image(width: width, height: height);
+
+    for (int y = 0; y < height; y++) {
+      for (int x = 0; x < width; x++) {
+        final int uvIndex = (uvRowStride * (y / 2).floor()) + (uvPixelStride * (x / 2).floor());
+        final int yIndex = (y * yRowStride) + x;
+
+        final int yp = yBytes[yIndex];
+        final int up = uBytes[uvIndex];
+        final int vp = vBytes[uvIndex];
+
+        // Standard YUV to RGB conversion
+        int r = (yp + (1.370705 * (vp - 128))).toInt().clamp(0, 255);
+        int g = (yp - (0.337633 * (up - 128)) - (0.698001 * (vp - 128))).toInt().clamp(0, 255);
+        int b = (yp + (1.732446 * (up - 128))).toInt().clamp(0, 255);
+
+        image.setPixelRgb(x, y, r, g, b);
+      }
+    }
+    return image;
+  }
+
+  static List<DetectionResult> _decodeDetections(List<List<double>> rawDetections, List<String> labels) {
+    final detections = <DetectionResult>[];
+    for (final det in rawDetections) {
+      if (det.length < 6) continue;
+      final conf = det[4];
+      if (conf < _confidenceThreshold) continue;
+
+      final x1 = det[0].clamp(0.0, 1.0);
+      final y1 = det[1].clamp(0.0, 1.0);
+      final x2 = det[2].clamp(0.0, 1.0);
+      final y2 = det[3].clamp(0.0, 1.0);
+      final classId = det[5].round();
+
+      if (x2 <= x1 || y2 <= y1) continue;
+
+      final label = (classId >= 0 && classId < labels.length) ? labels[classId] : 'Unknown';
+
+      detections.add(DetectionResult(
+        className: label,
+        classIndex: classId,
+        confidence: conf,
+        normalizedBox: Rect.fromLTRB(x1, y1, x2, y2),
+      ));
+    }
+    detections.sort((a, b) => b.confidence.compareTo(a.confidence));
+    return detections;
+  }
+
   Future<List<DetectionResult>> _runInferenceInIsolate(Uint8List imageBytes) async {
     // We need the model and labels passed as data
     final modelData = await rootBundle.load('assets/$_modelAsset');
@@ -138,35 +287,7 @@ class TfliteService {
       // 4. Run
       interpreter.run(inputTensor, outputTensor);
 
-      // 5. Decode
-      final detections = <DetectionResult>[];
-      final rawDetections = outputTensor[0];
-
-      for (final det in rawDetections) {
-        if (det.length < 6) continue;
-        final conf = det[4];
-        if (conf < _confidenceThreshold) continue;
-
-        final x1 = det[0].clamp(0.0, 1.0);
-        final y1 = det[1].clamp(0.0, 1.0);
-        final x2 = det[2].clamp(0.0, 1.0);
-        final y2 = det[3].clamp(0.0, 1.0);
-        final classId = det[5].round();
-
-        if (x2 <= x1 || y2 <= y1) continue;
-
-        final label = (classId >= 0 && classId < labels.length) ? labels[classId] : 'Unknown';
-
-        detections.add(DetectionResult(
-          className: label,
-          classIndex: classId,
-          confidence: conf,
-          normalizedBox: Rect.fromLTRB(x1, y1, x2, y2),
-        ));
-      }
-
-      detections.sort((a, b) => b.confidence.compareTo(a.confidence));
-      return detections;
+      return _decodeDetections(outputTensor[0], labels);
     } finally {
       interpreter.close();
     }

+ 96 - 0
palm_oil_mobile/pubspec.lock

@@ -25,6 +25,46 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.1.2"
+  camera:
+    dependency: "direct main"
+    description:
+      name: camera
+      sha256: "4142a19a38e388d3bab444227636610ba88982e36dff4552d5191a86f65dc437"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.11.4"
+  camera_android_camerax:
+    dependency: transitive
+    description:
+      name: camera_android_camerax
+      sha256: "8516fe308bc341a5067fb1a48edff0ddfa57c0d3cdcc9dbe7ceca3ba119e2577"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.6.30"
+  camera_avfoundation:
+    dependency: transitive
+    description:
+      name: camera_avfoundation
+      sha256: "11b4aee2f5e5e038982e152b4a342c749b414aa27857899d20f4323e94cb5f0b"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.9.23+2"
+  camera_platform_interface:
+    dependency: transitive
+    description:
+      name: camera_platform_interface
+      sha256: "98cfc9357e04bad617671b4c1f78a597f25f08003089dd94050709ae54effc63"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.12.0"
+  camera_web:
+    dependency: transitive
+    description:
+      name: camera_web
+      sha256: "57f49a635c8bf249d07fb95eb693d7e4dda6796dedb3777f9127fb54847beba7"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.3.5+3"
   characters:
     dependency: transitive
     description:
@@ -416,6 +456,54 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.3.0"
+  permission_handler:
+    dependency: "direct main"
+    description:
+      name: permission_handler
+      sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849"
+      url: "https://pub.dev"
+    source: hosted
+    version: "11.4.0"
+  permission_handler_android:
+    dependency: transitive
+    description:
+      name: permission_handler_android
+      sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc
+      url: "https://pub.dev"
+    source: hosted
+    version: "12.1.0"
+  permission_handler_apple:
+    dependency: transitive
+    description:
+      name: permission_handler_apple
+      sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
+      url: "https://pub.dev"
+    source: hosted
+    version: "9.4.7"
+  permission_handler_html:
+    dependency: transitive
+    description:
+      name: permission_handler_html
+      sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.1.3+5"
+  permission_handler_platform_interface:
+    dependency: transitive
+    description:
+      name: permission_handler_platform_interface
+      sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.3.0"
+  permission_handler_windows:
+    dependency: transitive
+    description:
+      name: permission_handler_windows
+      sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.2.1"
   petitparser:
     dependency: transitive
     description:
@@ -533,6 +621,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.1.4"
+  stream_transform:
+    dependency: transitive
+    description:
+      name: stream_transform
+      sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.1"
   string_scanner:
     dependency: transitive
     description:

+ 2 - 0
palm_oil_mobile/pubspec.yaml

@@ -34,6 +34,8 @@ dependencies:
   # The following adds the Cupertino Icons font to your application.
   # Use with the CupertinoIcons class for iOS style icons.
   cupertino_icons: ^1.0.8
+  camera: ^0.11.0+2
+  permission_handler: ^11.3.1
   image_picker: ^1.0.7
   sqflite: ^2.3.0
   path_provider: ^2.1.2

+ 3 - 0
palm_oil_mobile/windows/flutter/generated_plugin_registrant.cc

@@ -7,8 +7,11 @@
 #include "generated_plugin_registrant.h"
 
 #include <file_selector_windows/file_selector_windows.h>
+#include <permission_handler_windows/permission_handler_windows_plugin.h>
 
 void RegisterPlugins(flutter::PluginRegistry* registry) {
   FileSelectorWindowsRegisterWithRegistrar(
       registry->GetRegistrarForPlugin("FileSelectorWindows"));
+  PermissionHandlerWindowsPluginRegisterWithRegistrar(
+      registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
 }

+ 1 - 0
palm_oil_mobile/windows/flutter/generated_plugins.cmake

@@ -4,6 +4,7 @@
 
 list(APPEND FLUTTER_PLUGIN_LIST
   file_selector_windows
+  permission_handler_windows
 )
 
 list(APPEND FLUTTER_FFI_PLUGIN_LIST

+ 0 - 1689
src.txt

@@ -1,1689 +0,0 @@
-===== 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;
-}
- 
-