695 lines
22 KiB
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,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|