import 'dart:io'; 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 = []; // Variabel untuk validasi apakah gambar benar-benar kopi 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"); } } 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); 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']; // LOGIKA VALIDASI: Jika skor tertinggi < 70%, dianggap bukan kopi if (confidence < 0.70) { _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)), ), ); } // Tampilan jika yang diinput bukan biji kopi 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 Dikenali", style: GoogleFonts.montserrat(fontWeight: FontWeight.bold, fontSize: 18, color: Colors.red[900]), ), const SizedBox(height: 10), Text( "Gambar yang Anda masukkan tidak terdeteksi sebagai biji kopi Arabika atau Robusta. Silakan coba lagi.", 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)), ], ), ), ); } }