final
This commit is contained in:
parent
2e0400c900
commit
af383d9094
Binary file not shown.
|
|
@ -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)
|
||||
|
|
@ -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