| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444 |
- import 'dart:io';
- import 'dart:ui';
- import 'package:flutter/material.dart';
- import 'package:camera/camera.dart';
- import 'package:path_provider/path_provider.dart';
- import 'package:path/path.dart' as p;
- import 'package:permission_handler/permission_handler.dart';
- import '../services/tflite_service.dart';
- import '../services/database_helper.dart';
- import '../models/palm_record.dart';
- class StaticCaptureScreen extends StatefulWidget {
- const StaticCaptureScreen({super.key});
- @override
- State<StaticCaptureScreen> createState() => _StaticCaptureScreenState();
- }
- class _StaticCaptureScreenState extends State<StaticCaptureScreen> {
- CameraController? _controller;
- final TfliteService _tfliteService = TfliteService();
- final DatabaseHelper _dbHelper = DatabaseHelper();
- bool _isInitialized = false;
- bool _isAnalyzing = false;
- XFile? _capturedPhoto;
- List<DetectionResult>? _detections;
- String? _errorMessage;
- @override
- void initState() {
- super.initState();
- _initializeCamera();
- }
- Future<void> _initializeCamera() async {
- final status = await Permission.camera.request();
- if (status.isDenied) {
- if (mounted) {
- setState(() => _errorMessage = "Camera permission denied.");
- }
- return;
- }
- final cameras = await availableCameras();
- if (cameras.isEmpty) {
- if (mounted) {
- setState(() => _errorMessage = "No cameras found.");
- }
- return;
- }
- _controller = CameraController(
- cameras[0],
- ResolutionPreset.high,
- enableAudio: false,
- );
- try {
- await _controller!.initialize();
- await _tfliteService.initModel();
- if (mounted) {
- setState(() => _isInitialized = true);
- }
- } catch (e) {
- if (mounted) {
- setState(() => _errorMessage = "Camera init error: $e");
- }
- }
- }
- Future<void> _takeAndAnalyze() async {
- if (_controller == null || !_controller!.value.isInitialized || _isAnalyzing) return;
- setState(() {
- _isAnalyzing = true;
- _errorMessage = null;
- _detections = null;
- });
- try {
- // 1. Capture Photo
- final XFile photo = await _controller!.takePicture();
- if (mounted) {
- setState(() => _capturedPhoto = photo);
- }
- // 2. Run Inference
- final detections = await _tfliteService.runInference(photo.path);
- if (detections.isNotEmpty) {
- // 3. Persist Image
- final appDocDir = await getApplicationDocumentsDirectory();
- final fileName = p.basename(photo.path);
- final persistentPath = p.join(appDocDir.path, 'palm_static_${DateTime.now().millisecondsSinceEpoch}_$fileName');
- await File(photo.path).copy(persistentPath);
- // 4. Save to Database
- 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);
- if (mounted) {
- setState(() {
- _detections = detections;
- _capturedPhoto = XFile(persistentPath);
- });
- _showResultSheet(record);
- }
- } else {
- if (mounted) {
- setState(() => _errorMessage = "No palm bunches detected.");
- }
- }
- } catch (e) {
- if (mounted) {
- setState(() => _errorMessage = "Error during capture/analysis: $e");
- }
- } finally {
- if (mounted) {
- setState(() => _isAnalyzing = false);
- }
- }
- }
- Future<void> _showResultSheet(PalmRecord record) async {
- await showModalBottomSheet(
- context: context,
- isScrollControlled: true,
- backgroundColor: Colors.transparent,
- builder: (context) => _ResultSheet(record: record),
- );
-
- // Auto-reset once dismissed
- if (mounted) {
- setState(() {
- _capturedPhoto = null;
- _detections = null;
- });
- }
- }
- @override
- Widget build(BuildContext context) {
- if (_errorMessage != null) {
- return Scaffold(
- appBar: AppBar(title: const Text('Capture & Analyze')),
- body: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Text(_errorMessage!, style: const TextStyle(color: Colors.red)),
- const SizedBox(height: 16),
- ElevatedButton(onPressed: _initializeCamera, child: const Text("Retry")),
- ],
- ),
- ),
- );
- }
- if (!_isInitialized || _controller == null) {
- return const Scaffold(body: Center(child: CircularProgressIndicator()));
- }
- return Scaffold(
- backgroundColor: Colors.black,
- body: Stack(
- children: [
- // Camera Preview or Captured Image
- Center(
- child: _capturedPhoto != null && _detections != null
- ? AspectRatio(
- aspectRatio: _controller!.value.aspectRatio,
- child: Stack(
- children: [
- Image.file(File(_capturedPhoto!.path), fit: BoxFit.cover, width: double.infinity),
- Positioned.fill(
- child: LayoutBuilder(
- builder: (context, constraints) {
- return Stack(
- children: _detections!
- .map((d) => _buildBoundingBox(d, constraints))
- .toList(),
- );
- },
- ),
- ),
- ],
- ),
- )
- : CameraPreview(_controller!),
- ),
- // Top UI
- Positioned(
- top: 40,
- left: 20,
- right: 20,
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- CircleAvatar(
- backgroundColor: Colors.black54,
- child: IconButton(
- icon: const Icon(Icons.arrow_back, color: Colors.white),
- onPressed: () => Navigator.pop(context),
- ),
- ),
- if (_capturedPhoto != null)
- CircleAvatar(
- backgroundColor: Colors.black54,
- child: IconButton(
- icon: const Icon(Icons.refresh, color: Colors.white),
- onPressed: () => setState(() {
- _capturedPhoto = null;
- _detections = null;
- }),
- ),
- ),
- ],
- ),
- ),
- // Capture Button
- if (_capturedPhoto == null && !_isAnalyzing)
- Align(
- alignment: Alignment.bottomCenter,
- child: Padding(
- padding: const EdgeInsets.only(bottom: 48.0),
- child: GestureDetector(
- onTap: _takeAndAnalyze,
- child: Container(
- height: 80,
- width: 80,
- padding: const EdgeInsets.all(4),
- decoration: BoxDecoration(
- shape: BoxShape.circle,
- border: Border.all(color: Colors.white, width: 4),
- ),
- child: Container(
- decoration: const BoxDecoration(
- color: Colors.white,
- shape: BoxShape.circle,
- ),
- ),
- ),
- ),
- ),
- ),
- // Analyzing Overlay
- if (_isAnalyzing) _buildLoadingOverlay(),
- ],
- ),
- );
- }
- Widget _buildBoundingBox(DetectionResult detection, BoxConstraints constraints) {
- final rect = detection.normalizedBox;
- final color = detection.getStatusColor();
- 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: 3),
- borderRadius: BorderRadius.circular(4),
- ),
- child: Align(
- alignment: Alignment.topLeft,
- child: Container(
- padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
- color: color,
- child: Text(
- "${detection.className} ${(detection.confidence * 100).toStringAsFixed(0)}%",
- style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold),
- ),
- ),
- ),
- ),
- );
- }
- Widget _buildLoadingOverlay() {
- return Positioned.fill(
- child: BackdropFilter(
- filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
- child: Container(
- color: Colors.black.withOpacity(0.5),
- child: const Center(
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- CircularProgressIndicator(color: Colors.white, strokeWidth: 6),
- SizedBox(height: 24),
- Text(
- "AI is Analyzing...",
- style: TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold),
- ),
- ],
- ),
- ),
- ),
- ),
- );
- }
- @override
- void dispose() {
- _controller?.dispose();
- _tfliteService.dispose();
- super.dispose();
- }
- }
- class _ResultSheet extends StatelessWidget {
- final PalmRecord record;
- const _ResultSheet({required this.record});
- @override
- Widget build(BuildContext context) {
- 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);
- }
- final bool isWarning = record.ripenessClass == 'Unripe' || record.ripenessClass == 'Underripe';
- return Container(
- decoration: const BoxDecoration(
- color: Colors.white,
- borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
- ),
- padding: const EdgeInsets.all(24),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Container(
- width: 40,
- height: 4,
- decoration: BoxDecoration(color: Colors.grey[300], borderRadius: BorderRadius.circular(2)),
- ),
- const SizedBox(height: 24),
- Row(
- children: [
- Icon(Icons.analytics, color: statusColor, size: 32),
- const SizedBox(width: 12),
- const Text("Analysis Result", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
- ],
- ),
- const SizedBox(height: 24),
- _buildInfoRow("Ripeness", record.ripenessClass, statusColor),
- _buildInfoRow("Confidence", "${(record.confidence * 100).toStringAsFixed(1)}%", Colors.grey[700]!),
-
- if (record.ripenessClass == 'Empty_Bunch' || record.ripenessClass == 'Abnormal')
- _buildAlertCard("HEALTH ALERT", "Abnormal features detected. Check palm health.", Colors.red),
-
- if (isWarning)
- _buildAlertCard("YIELD WARNING", "Potential Yield Loss due to premature harvest.", Colors.orange),
- const SizedBox(height: 32),
- SizedBox(
- width: double.infinity,
- child: ElevatedButton(
- onPressed: () => Navigator.pop(context),
- style: ElevatedButton.styleFrom(
- backgroundColor: statusColor,
- foregroundColor: Colors.white,
- padding: const EdgeInsets.symmetric(vertical: 16),
- shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
- ),
- child: const Text("Acknowledge", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
- ),
- ),
- const SizedBox(height: 12),
- ],
- ),
- );
- }
- Widget _buildInfoRow(String label, String value, Color color) {
- return Padding(
- padding: const EdgeInsets.symmetric(vertical: 8.0),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(label, style: const TextStyle(fontSize: 16, color: Colors.grey)),
- Text(value, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: color)),
- ],
- ),
- );
- }
- Widget _buildAlertCard(String title, String message, Color color) {
- return Container(
- margin: const EdgeInsets.only(top: 16),
- padding: const EdgeInsets.all(16),
- decoration: BoxDecoration(
- color: color.withOpacity(0.1),
- borderRadius: BorderRadius.circular(12),
- border: Border.all(color: color.withOpacity(0.3)),
- ),
- child: Row(
- children: [
- Icon(Icons.warning_amber_rounded, color: color),
- const SizedBox(width: 12),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(title, style: TextStyle(color: color, fontWeight: FontWeight.bold, fontSize: 14)),
- Text(message, style: TextStyle(color: color.withOpacity(0.8), fontSize: 12)),
- ],
- ),
- ),
- ],
- ),
- );
- }
- }
|