import 'dart:io'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:tflite_flutter/tflite_flutter.dart'; import 'package:image/image.dart' as img; import 'package:google_fonts/google_fonts.dart'; import '../../widgets/color.dart'; class KlasifikasiScreen extends StatefulWidget { const KlasifikasiScreen({super.key}); @override State createState() => _KlasifikasiScreenState(); } class _KlasifikasiScreenState extends State { File? _image; Interpreter? _interpreter; List? _labels; bool _isProcessing = false; String _jenisKopi = ""; String _tingkatRoasting = ""; double _akurasi = 0.0; List> _top3Predictions = []; bool _isValidCoffee = true; @override void initState() { super.initState(); _loadModel(); } Future _loadModel() async { try { _interpreter = await Interpreter.fromAsset( 'assets/model/best_model_epoch15.tflite', ); _labels = [ "Arabika Dark", "Arabika Light", "Arabika Medium", "Robusta Dark", "Robusta Light", "Robusta Medium", ]; } catch (e) { debugPrint("Gagal memuat model: $e"); } } // FUNGSI BARU: Mengecek apakah gambar terlalu gelap atau terlalu polos (hitam/putih saja) bool _isImageValid(img.Image image) { double totalBrightness = 0; List pixels = image.getBytes(); // Hitung rata-rata kecerahan (Luminance) for (int i = 0; i < pixels.length; i += 4) { int r = pixels[i]; int g = pixels[i + 1]; int b = pixels[i + 2]; totalBrightness += (0.299 * r + 0.587 * g + 0.114 * b); } double avgBrightness = totalBrightness / (image.width * image.height); // Jika rata-rata kecerahan < 30 (terlalu gelap/hitam) atau > 245 (terlalu putih) if (avgBrightness < 30 || avgBrightness > 245) { return false; } return true; } Future _pickImage(ImageSource source) async { final picker = ImagePicker(); final pickedFile = await picker.pickImage(source: source); if (pickedFile != null) { setState(() { _image = File(pickedFile.path); _isProcessing = true; _jenisKopi = ""; _isValidCoffee = true; }); _classifyImage(_image!); } } Future _classifyImage(File image) async { if (_interpreter == null) return; var imageBytes = await image.readAsBytes(); var decodedImage = img.decodeImage(imageBytes); if (decodedImage == null) return; // 1. CEK VALIDASI AWAL (Mencegah foto hitam/polos diproses model) if (!_isImageValid(decodedImage)) { setState(() { _isValidCoffee = false; _jenisKopi = "Invalid"; _isProcessing = false; }); return; } var resized = img.copyResize(decodedImage, width: 224, height: 224); var input = List.generate( 1, (i) => List.generate( 224, (j) => List.generate(224, (k) => List.generate(3, (l) => 0.0)), ), ); for (var y = 0; y < 224; y++) { for (var x = 0; x < 224; x++) { var pixel = resized.getPixel(x, y); input[0][y][x][0] = pixel.r.toDouble(); input[0][y][x][1] = pixel.g.toDouble(); input[0][y][x][2] = pixel.b.toDouble(); } } var output = List.filled(1 * 6, 0.0).reshape([1, 6]); _interpreter!.run(input, output); List results = List.from(output[0]); List> tempPredictions = []; for (int i = 0; i < _labels!.length; i++) { tempPredictions.add({"label": _labels![i], "score": results[i]}); } tempPredictions.sort((a, b) => b['score'].compareTo(a['score'])); setState(() { _top3Predictions = tempPredictions.take(3).toList(); double confidence = _top3Predictions[0]['score']; // 2. CEK THRESHOLD (Mencegah objek luar kopi yang mirip tetap terdeteksi) // Naikkan ke 0.85 agar lebih ketat if (confidence < 0.85) { _isValidCoffee = false; _jenisKopi = "Unknown"; } else { _isValidCoffee = true; String bestMatch = _top3Predictions[0]['label']; List splitLabel = bestMatch.split(" "); _jenisKopi = splitLabel[0]; _tingkatRoasting = splitLabel.length > 1 ? splitLabel[1] : ""; _akurasi = confidence * 100; } _isProcessing = false; }); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.background, appBar: AppBar( backgroundColor: AppColors.cardWhite, surfaceTintColor: Colors.transparent, elevation: 0, title: Row( children: [ Image.asset( 'assets/images/Logo_Coffee_Scan.png', width: 30, errorBuilder: (c, e, s) => const Icon(Icons.coffee, color: AppColors.brownMain), ), const SizedBox(width: 10), RichText( text: TextSpan( style: GoogleFonts.montserrat( fontSize: 20, fontWeight: FontWeight.bold, ), children: const [ TextSpan( text: 'Coffee', style: TextStyle(color: AppColors.brownMain), ), TextSpan( text: 'Scan', style: TextStyle(color: AppColors.greenMain), ), ], ), ), ], ), ), body: SafeArea( bottom: false, child: SingleChildScrollView( physics: const BouncingScrollPhysics(), padding: const EdgeInsets.only( left: 16, right: 16, top: 16, bottom: 180, ), child: Column( children: [ _buildInputCard(), const SizedBox(height: 25), if (_isProcessing) const Center( child: CircularProgressIndicator(color: AppColors.brownMain), ), if (!_isProcessing && _jenisKopi.isNotEmpty) _isValidCoffee ? _buildResultSection() : _buildInvalidObjectCard(), ], ), ), ), ); } Widget _buildInputCard() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.heroCokelat, borderRadius: BorderRadius.circular(25), ), child: Row( children: [ Container( width: 110, height: 110, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), ), child: _image == null ? const Icon(Icons.image_search, size: 40, color: Colors.grey) : ClipRRect( borderRadius: BorderRadius.circular(20), child: Image.file(_image!, fit: BoxFit.cover), ), ), const SizedBox(width: 15), Expanded( child: Column( children: [ _buildActionButton( Icons.camera_alt_rounded, "Buka Kamera", () => _pickImage(ImageSource.camera), ), const SizedBox(height: 12), _buildActionButton( Icons.photo_library_rounded, "Unggah File", () => _pickImage(ImageSource.gallery), ), ], ), ), ], ), ); } Widget _buildActionButton(IconData icon, String label, VoidCallback onTap) { return ElevatedButton.icon( onPressed: onTap, icon: Icon(icon, size: 18), label: Text( label, style: GoogleFonts.montserrat( fontSize: 12, fontWeight: FontWeight.bold, ), ), style: ElevatedButton.styleFrom( backgroundColor: AppColors.buttonCream, foregroundColor: AppColors.textDark, elevation: 0, minimumSize: const Size(double.infinity, 45), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), ); } Widget _buildInvalidObjectCard() { return Container( padding: const EdgeInsets.all(25), decoration: BoxDecoration( color: Colors.red[50], borderRadius: BorderRadius.circular(25), border: Border.all(color: Colors.red.shade200), ), child: Column( children: [ const Icon(Icons.warning_amber_rounded, color: Colors.red, size: 60), const SizedBox(height: 15), Text( "Objek Tidak Valid", style: GoogleFonts.montserrat( fontWeight: FontWeight.bold, fontSize: 18, color: Colors.red[900], ), ), const SizedBox(height: 10), Text( "Gambar terlalu gelap, polos, atau bukan biji kopi yang dikenali. Silakan gunakan foto yang lebih jelas.", textAlign: TextAlign.center, style: GoogleFonts.montserrat(fontSize: 13, color: Colors.red[700]), ), ], ), ); } Widget _buildResultSection() { return Column( children: [ const Icon(Icons.check_circle, color: AppColors.greenSuccess, size: 70), const SizedBox(height: 8), Text( "Hasil Klasifikasi", style: GoogleFonts.montserrat( fontSize: 20, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 25), Row( children: [ _buildDetailBox( "Jenis Biji Kopi", _jenisKopi, Icons.coffee_rounded, ), const SizedBox(width: 12), _buildDetailBox( "Tingkat Roasting", _tingkatRoasting, Icons.local_fire_department_rounded, ), ], ), const SizedBox(height: 20), _buildAccuracyMainCard(), const SizedBox(height: 20), _buildTop3AnalisisCard(), ], ); } Widget _buildAccuracyMainCard() { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColors.greenLight, borderRadius: BorderRadius.circular(20), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Akurasi Prediksi", style: GoogleFonts.montserrat( color: AppColors.greenSuccess, fontWeight: FontWeight.bold, ), ), Text( "${_akurasi.toStringAsFixed(0)}%", style: GoogleFonts.montserrat( fontSize: 32, fontWeight: FontWeight.bold, color: AppColors.greenSuccess, ), ), ], ), const Icon( Icons.analytics_rounded, color: AppColors.greenSuccess, size: 50, ), ], ), ); } Widget _buildTop3AnalisisCard() { List> filteredPredictions = _top3Predictions .where((pred) => (pred['score'] * 100) >= 0.1) .toList(); return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(25), boxShadow: [ BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 15), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Analisis Kemungkinan", style: GoogleFonts.montserrat( fontWeight: FontWeight.bold, fontSize: 14, ), ), const SizedBox(height: 25), Row( children: [ SizedBox( width: 120, height: 120, child: Stack( alignment: Alignment.center, children: [ SizedBox( width: 120, height: 120, child: CircularProgressIndicator( value: 1.0, strokeWidth: 12, color: Colors.grey[100], ), ), if (filteredPredictions.length > 2) SizedBox( width: 120, height: 120, child: CircularProgressIndicator( value: filteredPredictions[2]['score'], strokeWidth: 12, strokeCap: StrokeCap.round, color: AppColors.rank3, ), ), if (filteredPredictions.length > 1) SizedBox( width: 120, height: 120, child: CircularProgressIndicator( value: filteredPredictions[1]['score'], strokeWidth: 12, strokeCap: StrokeCap.round, color: AppColors.rank2, ), ), SizedBox( width: 120, height: 120, child: CircularProgressIndicator( value: filteredPredictions[0]['score'], strokeWidth: 12, strokeCap: StrokeCap.round, color: AppColors.rank1, ), ), Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( "${_akurasi.toStringAsFixed(0)}%", style: GoogleFonts.montserrat( fontWeight: FontWeight.bold, fontSize: 18, color: AppColors.rank1, ), ), Text( "Match", style: GoogleFonts.montserrat( fontSize: 9, color: AppColors.textGrey, fontWeight: FontWeight.w500, ), ), ], ), ], ), ), const SizedBox(width: 15), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: List.generate(filteredPredictions.length, (index) { Color itemColor = index == 0 ? AppColors.rank1 : (index == 1 ? AppColors.rank2 : AppColors.rank3); return _buildTop3Item( filteredPredictions[index], itemColor, index == 0, ); }), ), ), ], ), ], ), ); } Widget _buildTop3Item(Map pred, Color color, bool isTop) { return Padding( padding: const EdgeInsets.symmetric(vertical: 5), child: Row( children: [ Container( width: 8, height: 8, decoration: BoxDecoration(color: color, shape: BoxShape.circle), ), const SizedBox(width: 8), Expanded( child: Text( pred['label'], maxLines: 1, style: GoogleFonts.montserrat( fontSize: 11, color: isTop ? AppColors.textDark : AppColors.textGrey, fontWeight: isTop ? FontWeight.bold : FontWeight.w500, ), ), ), const SizedBox(width: 4), Text( "${(pred['score'] * 100).toStringAsFixed(1)}%", style: GoogleFonts.montserrat( fontSize: 11, fontWeight: FontWeight.bold, color: color, ), ), ], ), ); } Widget _buildDetailBox(String title, String value, IconData icon) { return Expanded( child: Container( padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 10), decoration: BoxDecoration( color: AppColors.buttonCream.withOpacity(0.4), borderRadius: BorderRadius.circular(20), ), child: Column( children: [ Icon(icon, color: AppColors.brownMain, size: 28), const SizedBox(height: 8), Text( title, style: GoogleFonts.montserrat( fontSize: 10, color: AppColors.textGrey, fontWeight: FontWeight.w600, ), ), Text( value, textAlign: TextAlign.center, style: GoogleFonts.montserrat( fontSize: 14, fontWeight: FontWeight.bold, color: AppColors.brownMain, ), ), ], ), ), ); } }