| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386 |
- 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.60;
- static const int _frameThrottle = 2; // Check frames more frequently
-
- int _consecutiveDetections = 0;
- static const int _requiredConsecutive = 3; // Number of frames to hold
- @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 currentFrameHasFruit = false;
- if (detections.isNotEmpty) {
- currentFrameHasFruit = detections.any((d) => d.confidence > _lockThreshold);
- }
- if (currentFrameHasFruit) {
- _consecutiveDetections++;
- } else {
- _consecutiveDetections--; // Just drop by one, don't kill the whole lock
- }
-
- _consecutiveDetections = _consecutiveDetections.clamp(0, _requiredConsecutive);
- if (mounted) {
- setState(() {
- _detections = detections;
- _isLocked = _consecutiveDetections >= _requiredConsecutive;
- });
- }
- } 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();
- }
- }
|