CoffeeScan/lib/services/classifier_service.dart

306 lines
9.2 KiB
Dart

import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/services.dart';
import 'package:tflite_flutter/tflite_flutter.dart';
import 'package:image/image.dart' as img;
import '../models/prediction_result.dart';
class ClassifierService {
Interpreter? _interpreter;
List<String>? _labels;
Future<void> loadModel() async {
try {
_interpreter = await Interpreter.fromAsset(
'assets/model/best_model_epoch15.tflite',
);
final labelData = await rootBundle.loadString('assets/model/label.txt');
_labels = labelData
.split(RegExp(r'\r?\n'))
.where((l) => l.trim().isNotEmpty)
.toList();
print("✅ Model Load Success");
} catch (e) {
print("❌ Model Load Failed: $e");
}
}
/// REPLIKASI TOTAL LOGIKA PYTHON
img.Image syncPreprocessing(img.Image image) {
img.Image fixedImage = img.bakeOrientation(image);
// 2. Hitung Dimensi Target Zoom 80%
double zoomFactor = 0.8;
int targetW = (fixedImage.width * zoomFactor).toInt();
int targetH = (fixedImage.height * zoomFactor).toInt();
// 3. Tentukan Titik Tengah Absolut
int centerX = fixedImage.width ~/ 2;
int centerY = fixedImage.height ~/ 2;
// 4. Hitung Sisi-Sisi Potongan
int left = centerX - (targetW ~/ 2);
int top = centerY - (targetH ~/ 2);
// 5. Eksekusi Manual Crop
img.Image cropped = img.copyCrop(
fixedImage,
x: left,
y: top,
width: targetW,
height: targetH,
);
int side = (cropped.width < cropped.height)
? cropped.width
: cropped.height;
int startX = (cropped.width - side) ~/ 2;
int startY = (cropped.height - side) ~/ 2;
img.Image squareImage = img.copyCrop(
cropped,
x: startX,
y: startY,
width: side,
height: side,
);
// 7. Final Resize ke 224x224
return img.copyResize(
squareImage,
width: 224,
height: 224,
interpolation: img.Interpolation.cubic,
);
}
Future<PredictionResult> classify(File imageFile) async {
if (_interpreter == null) return _errorResult("Model belum dimuat");
final bytes = await imageFile.readAsBytes();
final decodedImage = img.decodeImage(bytes);
if (decodedImage == null) return _errorResult("Gambar rusak");
// PROSES: Samakan dengan dataset Python
img.Image finalImage = syncPreprocessing(decodedImage);
// Persiapan Tensor Input
var input = Float32List(1 * 224 * 224 * 3);
int buffer = 0;
for (int y = 0; y < 224; y++) {
for (int x = 0; x < 224; x++) {
var pixel = finalImage.getPixel(x, y);
input[buffer++] = pixel.r.toDouble();
input[buffer++] = pixel.g.toDouble();
input[buffer++] = pixel.b.toDouble();
}
}
var output = List.filled(1 * 6, 0.0).reshape([1, 6]);
_interpreter!.run(
input.buffer.asFloat32List().reshape([1, 224, 224, 3]),
output,
);
return _processResults(output[0]);
}
// --- Helper Functions ---
Future<File> preprocessImageForDisplay(File imageFile) async {
final bytes = await imageFile.readAsBytes();
final decodedImage = img.decodeImage(bytes);
if (decodedImage == null) return imageFile;
img.Image processed = syncPreprocessing(decodedImage);
final tempPath =
"${Directory.systemTemp.path}/view_${DateTime.now().millisecondsSinceEpoch}.jpg";
File(tempPath).writeAsBytesSync(img.encodeJpg(processed, quality: 95));
return File(tempPath);
}
PredictionResult _errorResult(String msg) => PredictionResult(
jenisKopi: "Error",
tingkatRoasting: msg,
akurasi: 0.0,
top3Predictions: [],
isValid: false,
);
PredictionResult _processResults(List<dynamic> results) {
List<Map<String, dynamic>> temp = [];
for (int i = 0; i < _labels!.length; i++) {
temp.add({"label": _labels![i], "score": results[i]});
}
temp.sort((a, b) => b['score'].compareTo(a['score']));
if (temp[0]['score'] < 0.5) return _errorResult("Kopi tidak dikenali");
List<String> split = temp[0]['label'].split(" ");
return PredictionResult(
jenisKopi: split[0],
tingkatRoasting: split.length > 1 ? split.sublist(1).join(" ") : "",
akurasi: temp[0]['score'] * 100,
top3Predictions: temp.take(3).toList(),
isValid: true,
);
}
}
// import 'dart:io';
// import 'dart:typed_data';
// import 'package:flutter/services.dart';
// import 'package:tflite_flutter/tflite_flutter.dart';
// import 'package:image/image.dart' as img;
// import '../models/prediction_result.dart';
// class ClassifierService {
// Interpreter? _interpreter;
// List<String>? _labels;
// Future<void> loadModel() async {
// try {
// // Pastikan file model di assets sudah sesuai dengan versi terbaru
// _interpreter = await Interpreter.fromAsset(
// 'assets/model/best_model_epoch15.tflite',
// );
// final labelData = await rootBundle.loadString('assets/model/label.txt');
// // Perbaikan: Gunakan split RegExp untuk menangani \n atau \r\n agar label bersih
// _labels = labelData
// .split(RegExp(r'\r?\n'))
// .where((label) => label.trim().isNotEmpty)
// .toList();
// print("✅ Model dan ${_labels?.length} Label berhasil dimuat!");
// } catch (e) {
// print("❌ Gagal memuat model: $e");
// }
// }
// bool _checkImageAuthenticity(img.Image image) {
// int rSum = 0, gSum = 0, bSum = 0;
// int sampleStep = 10;
// int count = 0;
// int startX = (image.width * 0.25).toInt();
// int startY = (image.height * 0.25).toInt();
// int endX = (image.width * 0.75).toInt();
// int endY = (image.height * 0.75).toInt();
// for (int y = startY; y < endY; y += sampleStep) {
// for (int x = startX; x < endX; x += sampleStep) {
// var pixel = image.getPixel(x, y);
// rSum += pixel.r.toInt();
// gSum += pixel.g.toInt();
// bSum += pixel.b.toInt();
// count++;
// }
// }
// if (count == 0) return false;
// double avgR = rSum / count;
// double avgG = gSum / count;
// double avgB = bSum / count;
// double brightness = (0.299 * avgR + 0.587 * avgG + 0.114 * avgB);
// if (brightness < 20 || brightness > 235) return false;
// if (avgG > avgR * 1.3 || avgB > avgR * 1.3) return false;
// return true;
// }
// Future<PredictionResult> classify(File imageFile) async {
// if (_interpreter == null) {
// return PredictionResult(
// jenisKopi: "Error",
// tingkatRoasting: "Model belum dimuat",
// akurasi: 0.0,
// top3Predictions: [],
// isValid: false,
// );
// }
// final bytes = await imageFile.readAsBytes();
// final decodedImage = img.decodeImage(bytes);
// if (decodedImage == null || !_checkImageAuthenticity(decodedImage)) {
// return PredictionResult(
// jenisKopi: "Invalid",
// tingkatRoasting: "",
// akurasi: 0.0,
// top3Predictions: [],
// isValid: false,
// );
// }
// // --- SINKRONISASI PREPROCESSING ---
// // Menggunakan copyResizeCropSquare agar identik dengan ImageOps.fit (Center Crop)
// // Interpolation.cubic memberikan kualitas tajam mirip LANCZOS di PIL
// var resized = img.copyResizeCropSquare(
// decodedImage,
// size: 224,
// interpolation: img.Interpolation.cubic,
// );
// // --- INPUT TENSOR ---
// // Menggunakan Float32List untuk menjamin presisi data saat masuk ke TFLite
// var input = Float32List(1 * 224 * 224 * 3);
// var buffer = 0;
// for (var y = 0; y < 224; y++) {
// for (var x = 0; x < 224; x++) {
// var pixel = resized.getPixel(x, y);
// // Nilai mentah 0-255 sesuai setingan Python (tanpa normalisasi manual)
// input[buffer++] = pixel.r.toDouble();
// input[buffer++] = pixel.g.toDouble();
// input[buffer++] = pixel.b.toDouble();
// }
// }
// // Output buffer sesuai jumlah kelas (6)
// var output = List.filled(1 * 6, 0.0).reshape([1, 6]);
// // Jalankan Inferensi
// _interpreter!.run(
// input.buffer.asFloat32List().reshape([1, 224, 224, 3]),
// output,
// );
// List<double> results = List<double>.from(output[0]);
// // Mapping & Ranking
// List<Map<String, dynamic>> temp = [];
// for (int i = 0; i < _labels!.length; i++) {
// temp.add({"label": _labels![i].trim(), "score": results[i]});
// }
// temp.sort((a, b) => b['score'].compareTo(a['score']));
// double topScore = temp[0]['score'];
// // Threshold keyakinan model (Sesuai Python CONFIDENCE_THRESHOLD = 0.5)
// if (topScore < 0.50) {
// return PredictionResult(
// jenisKopi: "Unknown",
// tingkatRoasting: "",
// akurasi: topScore * 100,
// top3Predictions: temp.take(3).toList(),
// isValid: false,
// );
// }
// // Memisahkan Jenis Kopi dan Roasting (Contoh: "Arabica Dark")
// List<String> split = temp[0]['label'].split(" ");
// return PredictionResult(
// jenisKopi: split[0],
// tingkatRoasting: split.length > 1 ? split.sublist(1).join(" ") : "",
// akurasi: topScore * 100,
// top3Predictions: temp.take(3).toList(),
// isValid: true,
// );
// }
// }