This commit is contained in:
kizadl 2026-06-21 20:58:03 +07:00
parent 2e0400c900
commit af383d9094
8 changed files with 79 additions and 339 deletions

Binary file not shown.

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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.