diff --git a/backend-ai/Satpam_IsolationForest.pkl b/backend-ai/Satpam_IsolationForest.pkl new file mode 100644 index 0000000..b1c61b0 Binary files /dev/null and b/backend-ai/Satpam_IsolationForest.pkl differ diff --git a/backend-ai/app.py b/backend-ai/app.py index 166ae29..4d1343a 100644 --- a/backend-ai/app.py +++ b/backend-ai/app.py @@ -1,129 +1,131 @@ import os import numpy as np +import joblib from flask import Flask, request, jsonify -from tensorflow.keras.models import load_model +from tensorflow.keras.models import load_model, Sequential from tensorflow.keras.preprocessing.image import img_to_array -from tensorflow.keras.applications.mobilenet_v2 import preprocess_input # WAJIB UNTUK MOBILENETV2 +from tensorflow.keras.applications.mobilenet_v2 import preprocess_input from PIL import Image from rembg import remove from flask_cors import CORS app = Flask(__name__) -CORS(app) # Agar bisa diakses dari Frontend Vue.js atau Laravel +CORS(app) # ============================================================================== -# 1. KONFIGURASI MODEL & KELAS +# 1. LOAD SEMUA MODEL (KLASIFIKATOR + SATPAM 1 & 2) # ============================================================================== -# Pastikan file model .keras hasil training sudah dipindahkan ke folder ini -MODEL_PATH = 'Arsitektur_MobileNetV2.keras' +MODEL_CNN_PATH = 'Arsitektur_MobileNetV2.keras' +MODEL_CAE_PATH = 'satpam_kopi_cae.keras' +SATPAM_IF_PATH = 'Satpam_IsolationForest.pkl' -if os.path.exists(MODEL_PATH): - model = load_model(MODEL_PATH) - print(f"✅ Model {MODEL_PATH} berhasil dimuat.") -else: - print(f"❌ ERROR: File {MODEL_PATH} tidak ditemukan!") +print("⏳ Memuat seluruh infrastruktur model...") +full_model = load_model(MODEL_CNN_PATH, compile=False) +satpam_cae = load_model(MODEL_CAE_PATH, compile=False) +iso_forest = joblib.load(SATPAM_IF_PATH) + +# Buat Feature Extractor untuk Satpam IF +feature_extractor = Sequential([ + full_model.layers[0], + full_model.layers[1] +]) +feature_extractor.build((None, 224, 224, 3)) -# Urutan kelas sesuai dengan training di Colab classes = ['honey', 'natural', 'wash'] +THRESHOLD_GOSONG = 51 +THRESHOLD_MSE = 0.0021 + +print("✅ Sistem Keamanan Berlapis Berhasil Diaktifkan.") # ============================================================================== -# 2. FUNGSI PREPROCESSING (Sesuai Standar MobileNetV2 + Rembg) +# 2. PIPELINE PENYARINGAN BERLAPIS # ============================================================================== -def preprocess_robust_mobilenet(input_img): - # A. Hapus Background (Mengubah objek acak menjadi transparan) +def proses_gambar_strict(input_img): + # A. Rembg & Crop output_rgba = remove(input_img) - - # B. Auto-Crop ke Bounding Box (Menghilangkan sisa ruang kosong) bbox = output_rgba.getbbox() - if bbox: - output_rgba = output_rgba.crop(bbox) + if bbox: output_rgba = output_rgba.crop(bbox) - # C. Center Padding (Membuat kanvas hitam persegi 1:1) + # -------------------------------------------------------------------------- + # LAPIS 1: Filter Kecerahan + # -------------------------------------------------------------------------- + img_rgba_np = np.array(output_rgba) + mask_biji = img_rgba_np[:, :, 3] > 0 + kecerahan = np.median(img_rgba_np[mask_biji][:, :3]) if np.any(mask_biji) else 0 + if kecerahan < THRESHOLD_GOSONG: + return None, "DITOLAK_GELAP", f"Kecerahan terlalu rendah ({kecerahan:.1f})" + + # Siapkan Kanvas Hitam Dasar max_dim = max(output_rgba.size) black_bg = Image.new("RGB", (max_dim, max_dim), (0, 0, 0)) + black_bg.paste(output_rgba, ((max_dim - output_rgba.size[0]) // 2, (max_dim - output_rgba.size[1]) // 2), mask=output_rgba.split()[3]) + + # -------------------------------------------------------------------------- + # LAPIS 2: Satpam Bentuk (CAE 64x64) - Menyaring Geometri Kasar + # -------------------------------------------------------------------------- + img_cae = black_bg.resize((64, 64)) + img_arr_cae = img_to_array(img_cae) / 255.0 + img_arr_cae = np.expand_dims(img_arr_cae, axis=0) - # Hitung posisi agar biji kopi tepat di tengah - paste_x = (max_dim - output_rgba.size[0]) // 2 - paste_y = (max_dim - output_rgba.size[1]) // 2 - - # Tempelkan gambar menggunakan mask (untuk menjaga transparansi) - black_bg.paste(output_rgba, (paste_x, paste_y), mask=output_rgba.split()[3]) - - # D. Resize Standar MobileNetV2 (224x224) + rekonstruksi = satpam_cae.predict(img_arr_cae, verbose=0) + mse_score = np.mean(np.square(img_arr_cae - rekonstruksi)) + if mse_score > THRESHOLD_MSE: + return None, "DITOLAK_CAE", f"Struktur bentuk tidak sesuai standar (MSE: {mse_score:.5f})" + + # -------------------------------------------------------------------------- + # LAPIS 3: Satpam Semantik (Isolation Forest 224x224) - Menyaring Detail Fitur + # -------------------------------------------------------------------------- final_img = black_bg.resize((224, 224)) - - # E. Konversi ke Array & Normalisasi Khusus MobileNetV2 (-1 hingga 1) img_array = img_to_array(final_img) img_array = np.expand_dims(img_array, axis=0) - img_array = preprocess_input(img_array) # INI KUNCINYA + img_array = preprocess_input(img_array) - return img_array + fitur = feature_extractor.predict(img_array, verbose=0) + keputusan_if = iso_forest.predict(fitur)[0] + if keputusan_if == -1: + return None, "DITOLAK_IF", "Karakteristik objek bukan green bean kopi" + + return img_array, "LOLOS", "Semua pos pemeriksaan aman" # ============================================================================== -# 3. ENDPOINT API PREDIKSI +# 3. ENDPOINT API # ============================================================================== @app.route('/predict', methods=['POST']) def predict(): if 'image' not in request.files: - return jsonify({"status": "ERROR", "message": "File gambar tidak ditemukan"}), 400 + return jsonify({"status": "ERROR", "message": "File tidak ditemukan"}), 400 - image_file = request.files['image'] - try: - # Load gambar sebagai RGBA agar rembg bekerja maksimal - img = Image.open(image_file.stream).convert("RGBA") + img = Image.open(request.files['image'].stream).convert("RGBA") + processed_img, status_satpam, keterangan = proses_gambar_strict(img) - # Jalankan Preprocessing - processed_img = preprocess_robust_mobilenet(img) - - # Prediksi menggunakan model - preds = model.predict(processed_img, verbose=0)[0] + # Jika salah satu satpam menolak, langsung return kembalian status DITOLAK + if status_satpam != "LOLOS": + return jsonify({ + "status": "DITOLAK", + "label": "Tidak terdeteksi", + "pesan": f"Objek ditolak pada tahap {status_satpam.split('_')[1]}. Keterangan: {keterangan}." + }), 200 + + # Jika lolos semua satpam, panggil pakar klasifikasi utama + preds = full_model.predict(processed_img, verbose=0)[0] confidence = float(np.max(preds)) predicted_class = classes[np.argmax(preds)] - - # Hitung Entropy (Mengukur tingkat kebingungan model) entropy = -np.sum(preds * np.log(preds + 1e-9)) - # Detail Probabilitas untuk ditampilkan di Frontend - prob_details = {} - for i, cls_name in enumerate(classes): - prob_details[cls_name] = round(float(preds[i]) * 100, 2) - - # --- LOGIKA PENYARINGAN STATUS --- - - # 1. Kasus: Gambar Tidak Jelas (Entropy Terlalu Tinggi) - if entropy > 0.85: - return jsonify({ - "status": "DITOLAK", - "label": "Tidak terdeteksi", - "confidence": str(round(confidence * 100, 2)), - "pesan": "Sistem bingung. Pastikan foto hanya berisi satu biji kopi dengan latar belakang yang tidak terlalu ramai.", - "details": prob_details - }), 200 + if entropy > 0.85 or confidence < 0.70: + return jsonify({"status": "DITOLAK", "label": "Tidak terdeteksi", "pesan": "Sistem ragu dengan objek ini."}), 200 - # 2. Kasus: Model Ditolak (Confidence < 70%) - if confidence < 0.70: - return jsonify({ - "status": "DITOLAK", - "label": "Tidak terdeteksi", - "confidence": str(round(confidence * 100, 2)), - "pesan": f"Sistem ditolak. Tingkat keyakinan hanya {str(round(confidence * 100, 2))}%.", - "details": prob_details - }), 200 - - # 3. Kasus: Berhasil (Sukses, Confidence >= 70%) return jsonify({ "status": "BERHASIL", "label": predicted_class, - # DIBUNGKUS STRING AGAR .replace() DI VUE.JS TIDAK ERROR - "confidence": str(round(confidence * 100, 2)), - "pesan": f"Biji kopi berhasil diidentifikasi sebagai proses {predicted_class.upper()}.", - "details": prob_details + "confidence": str(round(confidence * 100, 2)), + "pesan": f"Biji kopi proses {predicted_class.upper()} terdeteksi.", + "details": {cls: round(float(p) * 100, 2) for cls, p in zip(classes, preds)} }), 200 except Exception as e: return jsonify({"status": "ERROR", "message": str(e)}), 500 if __name__ == '__main__': - # Jalankan pada port 5001 (sesuaikan dengan settingan Laravel/Vue kamu) app.run(host='0.0.0.0', port=5001, debug=False) \ No newline at end of file diff --git a/backend-ai/app.py_old b/backend-ai/app.py_old deleted file mode 100644 index 173048c..0000000 --- a/backend-ai/app.py_old +++ /dev/null @@ -1,72 +0,0 @@ -import os -import io -import numpy as np -import tensorflow as tf -from flask import Flask, request, jsonify -from flask_cors import CORS -from PIL import Image - -app = Flask(__name__) -CORS(app) - -# 1. SETUP PATH MODEL -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) -MODEL_PATH = os.path.join(BASE_DIR, 'model_efficientNet.keras') - -# 2. DUMMY PREPROCESS (Agar tidak error saat load Lambda layer) -def preprocess_input(x): - return x - -print("⏳ Sedang memuat 'Otak AI'...") - -try: - # Menggunakan parameter Keras 3 untuk memuat model lama - model = tf.keras.models.load_model( - MODEL_PATH, - custom_objects={'preprocess_input': preprocess_input}, - compile=False, - safe_mode=False # Kunci agar Keras 3 mau menerima config Keras lama - ) - print("BERHASIL: Model AI readyy!") -except Exception as e: - print(f"GAGAL: {str(e)}") - -# Label klasifikasi kopi kamu -labels = ['Honey', 'Natural', 'Washed'] - -@app.route('/predict', methods=['POST']) -def predict(): - try: - file = request.files['image'] - - # 1. Load Gambar & Resize ke 224x224 - img = Image.open(file.stream).convert('RGB') - img = img.resize((224, 224)) - - # 2. Konversi ke Array - img_array = np.array(img).astype('float32') - - # 3. JURUS SAKTI: Gunakan preprocessing asli EfficientNet - # Ini akan menangani scaling warna agar sama persis dengan saat training - img_array = tf.keras.applications.efficientnet.preprocess_input(img_array) - img_array = np.expand_dims(img_array, axis=0) - - # 4. Prediksi - preds = model.predict(img_array, verbose=0) - class_idx = np.argmax(preds[0]) - confidence = float(np.max(preds[0])) - - print(f"📥 Prediksi: {labels[class_idx]} ({confidence*100:.2f}%)") - - return jsonify({ - 'label': labels[class_idx], - 'confidence': f"{confidence * 100:.2f}%", - 'status': 'success' - }) - except Exception as e: - print(f"❌ ERROR PREDIKSI: {str(e)}") - return jsonify({'error': str(e)}), 500 - -if __name__ == '__main__': - # Jalankan di port 5001 agar tidak diblokir Windows AirPlay - app.run(host='127.0.0.1', port=5001, debug=True) \ No newline at end of file diff --git a/backend-ai/app.py_old2 b/backend-ai/app.py_old2 deleted file mode 100644 index 6c4aac3..0000000 --- a/backend-ai/app.py_old2 +++ /dev/null @@ -1,81 +0,0 @@ -import os -import numpy as np -from flask import Flask, request, jsonify -from tensorflow.keras.models import load_model -from tensorflow.keras.preprocessing.image import img_to_array -from PIL import Image -from rembg import remove -from flask_cors import CORS - -app = Flask(__name__) -CORS(app) # Izinkan semua origin mengakses API ini - -# 1. Load Model (Pastikan file ini satu folder dengan app.py nanti) -MODEL_PATH = 'best_model.keras' -model = load_model(MODEL_PATH) -classes = ['honey', 'natural', 'washed'] - -# 2. Fungsi Preprocessing (Harus sama persis dengan saat training) -def preprocess_image(input_img): - # Hapus background & buat latar hitam - output_rgba = remove(input_img) - black_bg = Image.new("RGB", output_rgba.size, (0, 0, 0)) - black_bg.paste(output_rgba, mask=output_rgba.split()[3]) - - # Resize ke 224x224 sesuai EfficientNetB0 - final_img = black_bg.resize((224, 224)) - - # Konversi ke Array dan tambah dimensi batch (1, 224, 224, 3) - img_array = img_to_array(final_img) - img_array = np.expand_dims(img_array, axis=0) - return img_array - -@app.route('/predict', methods=['POST']) -def predict(): - # Menyesuaikan dengan formData.append('image', ...) dari Scanner.vue - if 'image' not in request.files: - return jsonify({"status": "ERROR", "message": "File gambar tidak ditemukan"}), 400 - - image_file = request.files['image'] - try: - img = Image.open(image_file.stream).convert("RGBA") - processed_img = preprocess_image(img) - - # Prediksi - preds = model.predict(processed_img)[0] - confidence = float(np.max(preds)) - predicted_class = classes[np.argmax(preds)] - - # Hitung Entropy (Mengukur tingkat kebingungan model) - entropy = -np.sum(preds * np.log(preds + 1e-9)) - - # --- LOGIKA PENYARINGAN (Threshold & Entropy) --- - # Jika entropy > 0.8, berarti model bingung (probabilitas terbagi-bagi) - if entropy > 0.85: - return jsonify({ - "status": "DITOLAK", - "pesan": "Sistem bingung. Mohon pastikan foto adalah biji kopi tunggal yang jelas.", - "entropy_score": round(entropy, 4) - }), 200 - - # Jika keyakinan di bawah 75% (threshold) - if confidence < 0.70: - return jsonify({ - "status": "TIDAK YAKIN", - "pesan": f"Model menduga {predicted_class}, tapi kurang yakin ({confidence*100:.1f}%).", - "confidence": str(round(confidence * 100, 2)) # <--- BUNGKUS DENGAN str() DI SINI - }), 200 - - # Lolos verifikasi - return jsonify({ - "status": "SUKSES", - "label": predicted_class, - "confidence": str(round(confidence * 100, 2)), # <--- BUNGKUS DENGAN str() DI SINI - "pesan": f"Biji kopi teridentifikasi sebagai {predicted_class}." - }), 200 - - except Exception as e: - return jsonify({"status": "ERROR", "message": str(e)}), 500 - -if __name__ == '__main__': - app.run(host='0.0.0.0', port=5001) \ No newline at end of file diff --git a/backend-ai/app.py_old3 b/backend-ai/app.py_old3 deleted file mode 100644 index 6f0fcbb..0000000 --- a/backend-ai/app.py_old3 +++ /dev/null @@ -1,109 +0,0 @@ -import os -import numpy as np -from flask import Flask, request, jsonify -from tensorflow.keras.models import load_model -from tensorflow.keras.preprocessing.image import img_to_array -from PIL import Image -from rembg import remove -from flask_cors import CORS - -app = Flask(__name__) -CORS(app) - -# 1. Load Model -# Pastikan file model terbaru sudah kamu download dan ganti namanya menjadi ini -MODEL_PATH = 'train_part4.keras' -model = load_model(MODEL_PATH) - -# Pastikan urutan kelas sesuai dengan test_generator.class_indices -# Tadi di Colab urutannya: honey, natural, wash (bukan washed) -classes = ['honey', 'natural', 'wash'] - -# 2. Fungsi Preprocessing Robust (Sesuai eksperimen terakhir di Colab) -def preprocess_image(input_img): - # A. Hapus Background - output_rgba = remove(input_img) - - # B. Auto-Crop ke Bounding Box (Fokus ke biji kopi saja) - bbox = output_rgba.getbbox() - if bbox: - output_rgba = output_rgba.crop(bbox) - - # C. Center Padding (Membuat kanvas hitam persegi) - max_dim = max(output_rgba.size) - black_bg = Image.new("RGB", (max_dim, max_dim), (0, 0, 0)) - - # Hitung posisi agar biji kopi di tengah - paste_x = (max_dim - output_rgba.size[0]) // 2 - paste_y = (max_dim - output_rgba.size[1]) // 2 - - # Tempelkan gambar transparan ke latar hitam - black_bg.paste(output_rgba, (paste_x, paste_y), mask=output_rgba.split()[3]) - - # D. Resize ke 224x224 (Input EfficientNetB0) - final_img = black_bg.resize((224, 224)) - - # E. Konversi ke Array - img_array = img_to_array(final_img) - img_array = np.expand_dims(img_array, axis=0) - return img_array - -@app.route('/predict', methods=['POST']) -def predict(): - if 'image' not in request.files: - return jsonify({"status": "ERROR", "message": "File gambar tidak ditemukan"}), 400 - - image_file = request.files['image'] - try: - # Load gambar asli sebagai RGBA agar rembg bekerja maksimal - img = Image.open(image_file.stream).convert("RGBA") - - # Jalankan Preprocessing Robust - processed_img = preprocess_image(img) - - # Prediksi - preds = model.predict(processed_img)[0] - confidence = float(np.max(preds)) - predicted_class = classes[np.argmax(preds)] - - # Hitung Entropy (Mengukur tingkat kebingungan model) - entropy = -np.sum(preds * np.log(preds + 1e-9)) - - prob_details = "Analisis Probabilitas Model:\n" - for i, cls_name in enumerate(classes): - prob_details += f"- {cls_name.capitalize()}: {round(float(preds[i]) * 100, 2)}%\n" - prob_details = prob_details.strip() - - # --- LOGIKA PENYARINGAN (Threshold & Entropy) --- - - # 1. Jika entropy tinggi (Model bingung parah) - if entropy > 0.85: - return jsonify({ - "status": "DITOLAK", - "pesan": f"Sistem mendeteksi ketidakjelasan. Pastikan objek adalah biji kopi tunggal dengan pencahayaan cukup.\n\n{prob_details}", - "entropy_score": round(float(entropy), 4) - }), 200 - - # 2. Jika Keyakinan Rendah (Di bawah 70%) - if confidence < 0.70: - return jsonify({ - "status": "TIDAK YAKIN", - "label": predicted_class, - "confidence": str(round(confidence * 100, 2)), - "pesan": f"Model menduga ini proses {predicted_class}, namun tingkat keyakinan rendah.\n\n{prob_details}" - }), 200 - - # 3. Lolos Verifikasi (Status SUKSES) - return jsonify({ - "status": "SUKSES", - "label": predicted_class, - "confidence": str(round(confidence * 100, 2)), - "pesan": f"Biji kopi teridentifikasi sebagai proses {predicted_class}.\n\n{prob_details}" - }), 200 - - except Exception as e: - return jsonify({"status": "ERROR", "message": str(e)}), 500 - -if __name__ == '__main__': - # Pastikan port sesuai dengan yang dibuka di firewall server/local kamu - app.run(host='0.0.0.0', port=5001, debug=False) \ No newline at end of file diff --git a/backend-ai/best_model.keras b/backend-ai/best_model.keras deleted file mode 100644 index 36cc02e..0000000 Binary files a/backend-ai/best_model.keras and /dev/null differ diff --git a/backend-ai/satpam_kopi_cae.keras b/backend-ai/satpam_kopi_cae.keras new file mode 100644 index 0000000..9d0a0eb Binary files /dev/null and b/backend-ai/satpam_kopi_cae.keras differ diff --git a/backend-ai/train_part4.keras b/backend-ai/train_part4.keras deleted file mode 100644 index 9c23c8a..0000000 Binary files a/backend-ai/train_part4.keras and /dev/null differ