CoffeeScan/lib/screen/klasifikasi/klas-2

373 lines
14 KiB
Plaintext

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<KlasifikasiScreen> createState() => _KlasifikasiScreenState();
}
class _KlasifikasiScreenState extends State<KlasifikasiScreen> {
File? _image;
Interpreter? _interpreter;
List<String>? _labels;
bool _isProcessing = false;
String _jenisKopi = "";
String _tingkatRoasting = "";
double _akurasi = 0.0;
List<Map<String, dynamic>> _top3Predictions = [];
// Variabel untuk validasi apakah gambar benar-benar kopi
bool _isValidCoffee = true;
@override
void initState() {
super.initState();
_loadModel();
}
Future<void> _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<void> _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<void> _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<double> results = List<double>.from(output[0]);
List<Map<String, dynamic>> 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<String> 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<Map<String, dynamic>> 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<String, dynamic> 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)),
],
),
),
);
}
}