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? _labels; Future 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 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 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 results) { List> 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 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? _labels; // Future 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 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 results = List.from(output[0]); // // Mapping & Ranking // List> 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 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, // ); // } // }