final
This commit is contained in:
parent
2e0400c900
commit
af383d9094
Binary file not shown.
|
|
@ -1,129 +1,131 @@
|
||||||
import os
|
import os
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import joblib
|
||||||
from flask import Flask, request, jsonify
|
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.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 PIL import Image
|
||||||
from rembg import remove
|
from rembg import remove
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
|
|
||||||
app = Flask(__name__)
|
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_CNN_PATH = 'Arsitektur_MobileNetV2.keras'
|
||||||
MODEL_PATH = 'Arsitektur_MobileNetV2.keras'
|
MODEL_CAE_PATH = 'satpam_kopi_cae.keras'
|
||||||
|
SATPAM_IF_PATH = 'Satpam_IsolationForest.pkl'
|
||||||
|
|
||||||
if os.path.exists(MODEL_PATH):
|
print("⏳ Memuat seluruh infrastruktur model...")
|
||||||
model = load_model(MODEL_PATH)
|
full_model = load_model(MODEL_CNN_PATH, compile=False)
|
||||||
print(f"✅ Model {MODEL_PATH} berhasil dimuat.")
|
satpam_cae = load_model(MODEL_CAE_PATH, compile=False)
|
||||||
else:
|
iso_forest = joblib.load(SATPAM_IF_PATH)
|
||||||
print(f"❌ ERROR: File {MODEL_PATH} tidak ditemukan!")
|
|
||||||
|
# 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']
|
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):
|
def proses_gambar_strict(input_img):
|
||||||
# A. Hapus Background (Mengubah objek acak menjadi transparan)
|
# A. Rembg & Crop
|
||||||
output_rgba = remove(input_img)
|
output_rgba = remove(input_img)
|
||||||
|
|
||||||
# B. Auto-Crop ke Bounding Box (Menghilangkan sisa ruang kosong)
|
|
||||||
bbox = output_rgba.getbbox()
|
bbox = output_rgba.getbbox()
|
||||||
if bbox:
|
if bbox: output_rgba = output_rgba.crop(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)
|
max_dim = max(output_rgba.size)
|
||||||
black_bg = Image.new("RGB", (max_dim, max_dim), (0, 0, 0))
|
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])
|
||||||
|
|
||||||
# Hitung posisi agar biji kopi tepat di tengah
|
# --------------------------------------------------------------------------
|
||||||
paste_x = (max_dim - output_rgba.size[0]) // 2
|
# LAPIS 2: Satpam Bentuk (CAE 64x64) - Menyaring Geometri Kasar
|
||||||
paste_y = (max_dim - output_rgba.size[1]) // 2
|
# --------------------------------------------------------------------------
|
||||||
|
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)
|
||||||
|
|
||||||
# Tempelkan gambar menggunakan mask (untuk menjaga transparansi)
|
rekonstruksi = satpam_cae.predict(img_arr_cae, verbose=0)
|
||||||
black_bg.paste(output_rgba, (paste_x, paste_y), mask=output_rgba.split()[3])
|
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})"
|
||||||
|
|
||||||
# D. Resize Standar MobileNetV2 (224x224)
|
# --------------------------------------------------------------------------
|
||||||
|
# LAPIS 3: Satpam Semantik (Isolation Forest 224x224) - Menyaring Detail Fitur
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
final_img = black_bg.resize((224, 224))
|
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 = img_to_array(final_img)
|
||||||
img_array = np.expand_dims(img_array, axis=0)
|
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'])
|
@app.route('/predict', methods=['POST'])
|
||||||
def predict():
|
def predict():
|
||||||
if 'image' not in request.files:
|
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:
|
try:
|
||||||
# Load gambar sebagai RGBA agar rembg bekerja maksimal
|
img = Image.open(request.files['image'].stream).convert("RGBA")
|
||||||
img = Image.open(image_file.stream).convert("RGBA")
|
processed_img, status_satpam, keterangan = proses_gambar_strict(img)
|
||||||
|
|
||||||
# Jalankan Preprocessing
|
# Jika salah satu satpam menolak, langsung return kembalian status DITOLAK
|
||||||
processed_img = preprocess_robust_mobilenet(img)
|
if status_satpam != "LOLOS":
|
||||||
|
return jsonify({
|
||||||
|
"status": "DITOLAK",
|
||||||
|
"label": "Tidak terdeteksi",
|
||||||
|
"pesan": f"Objek ditolak pada tahap {status_satpam.split('_')[1]}. Keterangan: {keterangan}."
|
||||||
|
}), 200
|
||||||
|
|
||||||
# Prediksi menggunakan model
|
# Jika lolos semua satpam, panggil pakar klasifikasi utama
|
||||||
preds = model.predict(processed_img, verbose=0)[0]
|
preds = full_model.predict(processed_img, verbose=0)[0]
|
||||||
confidence = float(np.max(preds))
|
confidence = float(np.max(preds))
|
||||||
predicted_class = classes[np.argmax(preds)]
|
predicted_class = classes[np.argmax(preds)]
|
||||||
|
|
||||||
# Hitung Entropy (Mengukur tingkat kebingungan model)
|
|
||||||
entropy = -np.sum(preds * np.log(preds + 1e-9))
|
entropy = -np.sum(preds * np.log(preds + 1e-9))
|
||||||
|
|
||||||
# Detail Probabilitas untuk ditampilkan di Frontend
|
if entropy > 0.85 or confidence < 0.70:
|
||||||
prob_details = {}
|
return jsonify({"status": "DITOLAK", "label": "Tidak terdeteksi", "pesan": "Sistem ragu dengan objek ini."}), 200
|
||||||
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
|
|
||||||
|
|
||||||
# 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({
|
return jsonify({
|
||||||
"status": "BERHASIL",
|
"status": "BERHASIL",
|
||||||
"label": predicted_class,
|
"label": predicted_class,
|
||||||
# DIBUNGKUS STRING AGAR .replace() DI VUE.JS TIDAK ERROR
|
|
||||||
"confidence": str(round(confidence * 100, 2)),
|
"confidence": str(round(confidence * 100, 2)),
|
||||||
"pesan": f"Biji kopi berhasil diidentifikasi sebagai proses {predicted_class.upper()}.",
|
"pesan": f"Biji kopi proses {predicted_class.upper()} terdeteksi.",
|
||||||
"details": prob_details
|
"details": {cls: round(float(p) * 100, 2) for cls, p in zip(classes, preds)}
|
||||||
}), 200
|
}), 200
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"status": "ERROR", "message": str(e)}), 500
|
return jsonify({"status": "ERROR", "message": str(e)}), 500
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# Jalankan pada port 5001 (sesuaikan dengan settingan Laravel/Vue kamu)
|
|
||||||
app.run(host='0.0.0.0', port=5001, debug=False)
|
app.run(host='0.0.0.0', port=5001, debug=False)
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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)
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue