CoffeeScan/lib/screen/klasifikasi/klasifikasi.dart

695 lines
22 KiB
Dart

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import '../../widgets/color.dart';
import '../../services/classifier_service.dart';
import '../../models/prediction_result.dart';
class KlasifikasiScreen extends StatefulWidget {
const KlasifikasiScreen({super.key});
@override
State<KlasifikasiScreen> createState() => _KlasifikasiScreenState();
}
class _KlasifikasiScreenState extends State<KlasifikasiScreen> {
File? _image;
bool _isProcessing = false;
PredictionResult? _result;
final ClassifierService _classifierService = ClassifierService();
@override
void initState() {
super.initState();
_initModel();
}
Future<void> _initModel() async {
await _classifierService.loadModel();
}
// ini hasil foto asli tanpa preprocessing
Future<void> _pickImage(ImageSource source) async {
final picker = ImagePicker();
final pickedFile = await picker.pickImage(
source: source,
imageQuality: 100,
);
if (pickedFile != null) {
setState(() {
_isProcessing = true;
_result = null;
});
try {
File original = File(pickedFile.path);
// 1. Preprocessing untuk UI (Zoom 80% & Square Crop)
// Gambar ini yang akan disimpan di variabel _image untuk ditampilkan di widget
File processed = await _classifierService.preprocessImageForDisplay(
original,
);
setState(() {
_image = processed;
});
// 2. Klasifikasi menggunakan file yang sudah di-crop/resize
final result = await _classifierService.classify(processed);
setState(() {
_result = result;
_isProcessing = false;
});
} catch (e) {
print("Error during image processing: $e");
setState(() => _isProcessing = false);
}
}
}
// ini kalau mau preprocessing gambar untuk ditampilkan di UI, biar sama dengan yang dianalisis model
// Future<void> _pickImage(ImageSource source) async {
// final picker = ImagePicker();
// final pickedFile = await picker.pickImage(
// source: source,
// imageQuality: 100,
// );
// if (pickedFile != null) {
// File original = File(pickedFile.path);
// /// preprocessing supaya gambar yang tampil = gambar yang dianalisis model
// File processed = await _classifierService.preprocessImageForDisplay(
// original,
// );
// setState(() {
// _image = processed;
// _isProcessing = true;
// _result = null;
// });
// final result = await _classifierService.classify(processed);
// setState(() {
// _result = result;
// _isProcessing = false;
// });
// }
// }
// fungsi untuk menampilkan dialog instruksi sebelum membuka kamera
// void _showCameraInstructions(BuildContext context) {
// showDialog(
// context: context,
// builder: (BuildContext context) {
// return Dialog(
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(25),
// ),
// child: Container(
// padding: const EdgeInsets.all(20),
// decoration: BoxDecoration(
// color: Colors.white,
// borderRadius: BorderRadius.circular(25),
// ),
// child: Column(
// mainAxisSize: MainAxisSize.min,
// children: [
// // Icon Header yang menarik
// Container(
// padding: const EdgeInsets.all(15),
// decoration: BoxDecoration(
// color: AppColors.heroCokelat.withOpacity(0.2),
// shape: BoxShape.circle,
// ),
// child: const Icon(
// Icons.center_focus_strong_rounded,
// color: AppColors.brownMain,
// size: 40,
// ),
// ),
// const SizedBox(height: 20),
// Text(
// "Instruksi Pengambilan",
// style: GoogleFonts.montserrat(
// fontSize: 18,
// fontWeight: FontWeight.bold,
// color: AppColors.brownMain,
// ),
// ),
// const SizedBox(height: 15),
// // Point-point instruksi
// _buildInstructionRow(
// Icons.center_focus_weak,
// "Pastikan biji kopi berada tepat di tengah bingkai.",
// ),
// const SizedBox(height: 10),
// _buildInstructionRow(
// Icons.lightbulb_outline,
// "Gunakan pencahayaan yang cukup agar tekstur terlihat jelas.",
// ),
// const SizedBox(height: 10),
// _buildInstructionRow(
// Icons.stay_primary_portrait,
// "Pegang ponsel dengan stabil agar foto tidak buram (blur).",
// ),
// const SizedBox(height: 25),
// // Tombol Mengerti
// SizedBox(
// width: double.infinity,
// child: ElevatedButton(
// onPressed: () {
// Navigator.pop(context); // Tutup dialog
// _pickImage(ImageSource.camera); // Lanjut buka kamera
// },
// style: ElevatedButton.styleFrom(
// backgroundColor: AppColors.brownMain,
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(15),
// ),
// padding: const EdgeInsets.symmetric(vertical: 12),
// ),
// child: Text(
// "Saya Mengerti",
// style: GoogleFonts.montserrat(
// color: Colors.white,
// fontWeight: FontWeight.bold,
// ),
// ),
// ),
// ),
// ],
// ),
// ),
// );
// },
// );
// }
// // Widget pendukung untuk baris teks instruksi
// Widget _buildInstructionRow(IconData icon, String text) {
// return Row(
// children: [
// Icon(icon, size: 20, color: AppColors.greenMain),
// const SizedBox(width: 12),
// Expanded(
// child: Text(
// text,
// style: GoogleFonts.montserrat(
// fontSize: 12,
// color: AppColors.textDark,
// height: 1.4,
// ),
// ),
// ),
// ],
// );
// }
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.background,
appBar: _buildAppBar(),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: 50,
),
child: Column(
children: [
_buildInputCard(),
const SizedBox(height: 25),
if (_isProcessing)
const Center(
child: CircularProgressIndicator(color: AppColors.brownMain),
),
if (!_isProcessing && _result != null)
_result!.isValid ? _buildResultSection() : _buildInvalidCard(),
],
),
),
),
);
}
PreferredSizeWidget _buildAppBar() {
return AppBar(
backgroundColor: AppColors.cardWhite,
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: const TextStyle(
fontFamily: 'Montserrat',
fontSize: 20,
fontWeight: FontWeight.w900,
),
children: const [
TextSpan(
text: 'Coffee',
style: TextStyle(color: AppColors.brownMain),
),
TextSpan(
text: 'Scan',
style: TextStyle(color: AppColors.greenMain),
),
],
),
),
],
),
);
}
Widget _buildInputCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.heroCokelat,
borderRadius: BorderRadius.circular(25),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Stack(
alignment: Alignment.center,
children: [
Container(
width: 135,
height: 135,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
child: _image == null
? const Icon(
Icons.image_search,
size: 50,
color: Colors.grey,
)
: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Image.file(_image!, fit: BoxFit.cover),
),
),
if (_image == null)
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
border: Border.all(
color: Colors.white.withOpacity(0.5),
width: 2,
),
borderRadius: BorderRadius.circular(15),
),
),
],
),
const SizedBox(width: 16),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Posisikan biji kopi di tengah bingkai kotak agar hasil lebih akurat",
style: const TextStyle(
fontFamily: 'Montserrat',
fontSize: 8,
fontWeight: FontWeight.w500,
color: Colors.white,
height: 1.2,
),
),
const SizedBox(height: 10),
_buildActionButton(
Icons.camera_alt_rounded,
"Buka Kamera",
() => _pickImage(ImageSource.camera),
),
// _buildActionButton(
// Icons.camera_alt_rounded,
// "Buka Kamera",
// () => _showCameraInstructions(
// context,
// ),
// ),
const SizedBox(height: 8),
_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, color: Colors.black),
label: Text(
label,
style: const TextStyle(
fontFamily: 'Montserrat',
fontSize: 12,
fontWeight: FontWeight.w700,
color: Colors.black,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFF5E6CA),
elevation: 0,
minimumSize: const Size(double.infinity, 38),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
);
}
Widget _buildInvalidCard() {
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.block_flipped, color: Colors.red, size: 60),
const SizedBox(height: 15),
Text(
"Objek Tidak Dikenali",
style: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w700,
fontSize: 18,
color: Colors.red[900],
),
),
const SizedBox(height: 10),
Text(
"Pastikan objek berada di tengah kotak dan memiliki pencahayaan yang cukup.",
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: '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: const TextStyle(
fontFamily: 'Montserrat',
fontSize: 20,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 25),
Row(
children: [
_buildDetailBox(
"Jenis Biji Kopi",
_result!.jenisKopi,
Icons.coffee_rounded,
),
const SizedBox(width: 12),
_buildDetailBox(
"Tingkat Roasting",
_result!.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: const TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w700,
color: AppColors.greenSuccess,
),
),
Text(
"${_result!.akurasi.toStringAsFixed(2)}%",
style: const TextStyle(
fontFamily: 'Montserrat',
fontSize: 32,
fontWeight: FontWeight.w700,
color: AppColors.greenSuccess,
),
),
],
),
const Icon(
Icons.analytics_rounded,
color: AppColors.greenSuccess,
size: 50,
),
],
),
);
}
Widget _buildTop3AnalisisCard() {
List<Map<String, dynamic>> filtered = _result!.top3Predictions
.where((pred) => (pred['score'] * 100) >= 0.1)
.toList();
double s1 = filtered.isNotEmpty ? filtered[0]['score'] : 0.0;
double s2 = filtered.length > 1 ? filtered[1]['score'] : 0.0;
double s3 = filtered.length > 2 ? filtered[2]['score'] : 0.0;
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: const TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w700,
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 (filtered.length > 2)
SizedBox(
width: 120,
height: 120,
child: CircularProgressIndicator(
value: s1 + s2 + s3,
strokeWidth: 12,
strokeCap: StrokeCap.round,
color: AppColors.rank3,
),
),
if (filtered.length > 1)
SizedBox(
width: 120,
height: 120,
child: CircularProgressIndicator(
value: s1 + s2,
strokeWidth: 12,
strokeCap: StrokeCap.round,
color: AppColors.rank2,
),
),
SizedBox(
width: 120,
height: 120,
child: CircularProgressIndicator(
value: s1,
strokeWidth: 12,
strokeCap: StrokeCap.round,
color: AppColors.rank1,
),
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"${(s1 * 100).toStringAsFixed(2)}%",
style: const TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w700,
fontSize: 18,
color: AppColors.rank1,
),
),
],
),
],
),
),
const SizedBox(width: 15),
Expanded(
child: Column(
children: List.generate(filtered.length, (index) {
Color c = index == 0
? AppColors.rank1
: (index == 1 ? AppColors.rank2 : AppColors.rank3);
return _buildTop3Item(filtered[index], c, 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: TextStyle(
fontFamily: 'Montserrat',
fontSize: 11,
color: isTop ? AppColors.textDark : AppColors.textGrey,
fontWeight: isTop ? FontWeight.w700 : FontWeight.w500,
),
),
),
const SizedBox(width: 4),
Text(
"${(pred['score'] * 100).toStringAsFixed(2)}%",
style: TextStyle(
fontFamily: 'Montserrat',
fontSize: 11,
fontWeight: FontWeight.w700,
color: color,
),
),
],
),
);
}
Widget _buildDetailBox(String t, String v, IconData i) {
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(i, color: AppColors.brownMain, size: 28),
const SizedBox(height: 8),
Text(
t,
style: const TextStyle(
fontFamily: 'Montserrat',
fontSize: 10,
color: AppColors.textGrey,
fontWeight: FontWeight.w600,
),
),
Text(
v,
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: 'Montserrat',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.brownMain,
),
),
],
),
),
);
}
}