131 lines
5.5 KiB
Python
131 lines
5.5 KiB
Python
import os
|
|
import numpy as np
|
|
import joblib
|
|
from flask import Flask, request, jsonify
|
|
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
|
|
from PIL import Image
|
|
from rembg import remove
|
|
from flask_cors import CORS
|
|
|
|
app = Flask(__name__)
|
|
CORS(app)
|
|
|
|
# ==============================================================================
|
|
# 1. LOAD SEMUA MODEL (KLASIFIKATOR + SATPAM 1 & 2)
|
|
# ==============================================================================
|
|
MODEL_CNN_PATH = 'Arsitektur_MobileNetV2.keras'
|
|
MODEL_CAE_PATH = 'satpam_kopi_cae.keras'
|
|
SATPAM_IF_PATH = 'Satpam_IsolationForest.pkl'
|
|
|
|
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))
|
|
|
|
classes = ['honey', 'natural', 'wash']
|
|
THRESHOLD_GOSONG = 51
|
|
THRESHOLD_MSE = 0.0021
|
|
|
|
print("✅ Sistem Keamanan Berlapis Berhasil Diaktifkan.")
|
|
|
|
# ==============================================================================
|
|
# 2. PIPELINE PENYARINGAN BERLAPIS
|
|
# ==============================================================================
|
|
def proses_gambar_strict(input_img):
|
|
# A. Rembg & Crop
|
|
output_rgba = remove(input_img)
|
|
bbox = output_rgba.getbbox()
|
|
if bbox: output_rgba = output_rgba.crop(bbox)
|
|
|
|
# --------------------------------------------------------------------------
|
|
# 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)
|
|
|
|
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))
|
|
img_array = img_to_array(final_img)
|
|
img_array = np.expand_dims(img_array, axis=0)
|
|
img_array = preprocess_input(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
|
|
# ==============================================================================
|
|
@app.route('/predict', methods=['POST'])
|
|
def predict():
|
|
if 'image' not in request.files:
|
|
return jsonify({"status": "ERROR", "message": "File tidak ditemukan"}), 400
|
|
|
|
try:
|
|
img = Image.open(request.files['image'].stream).convert("RGBA")
|
|
processed_img, status_satpam, keterangan = proses_gambar_strict(img)
|
|
|
|
# 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)]
|
|
entropy = -np.sum(preds * np.log(preds + 1e-9))
|
|
|
|
if entropy > 0.85 or confidence < 0.70:
|
|
return jsonify({"status": "DITOLAK", "label": "Tidak terdeteksi", "pesan": "Sistem ragu dengan objek ini."}), 200
|
|
|
|
return jsonify({
|
|
"status": "BERHASIL",
|
|
"label": predicted_class,
|
|
"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__':
|
|
app.run(host='0.0.0.0', port=5001, debug=False) |