158 lines
6.1 KiB
Python
158 lines
6.1 KiB
Python
import cv2
|
|
import numpy as np
|
|
from flask import Flask, request
|
|
from ultralytics import YOLO
|
|
import firebase_admin
|
|
from firebase_admin import credentials, db
|
|
import time
|
|
import base64
|
|
|
|
# ── 1. Inisialisasi Firebase ──────────────────────────────────────────────────
|
|
cred = credentials.Certificate("serviceAccountKey.json")
|
|
firebase_admin.initialize_app(cred, {
|
|
'databaseURL': 'https://smart-donation-adff1-default-rtdb.asia-southeast1.firebasedatabase.app/'
|
|
# Firebase Storage TIDAK dipakai — foto disimpan sebagai base64 di Realtime DB
|
|
# Jika suatu saat upgrade ke Blaze, tambahkan:
|
|
# 'storageBucket': 'smart-donation-adff1.appspot.com'
|
|
})
|
|
|
|
# ── 2. Load Model YOLO ────────────────────────────────────────────────────────
|
|
model = YOLO("best.pt")
|
|
app = Flask(__name__)
|
|
|
|
print("Class model:", model.names) # Verifikasi nama class saat startup
|
|
|
|
NOMINAL_MAP = {
|
|
'1': 1000,
|
|
'2': 2000,
|
|
'5': 5000,
|
|
'10': 10000,
|
|
'20': 20000,
|
|
'50': 50000,
|
|
'100': 100000,
|
|
}
|
|
|
|
|
|
# ── 3. Helper: Simpan foto sebagai Base64 ke Realtime Database ───────────────
|
|
def simpan_foto_terakhir(img_bgr, deteksi_label):
|
|
"""
|
|
Encode foto hasil deteksi ke Base64 lalu simpan langsung ke Realtime DB.
|
|
Tidak butuh Firebase Storage — kompatibel dengan plan Spark (gratis).
|
|
Dashboard web bisa langsung baca via <img src="data:image/jpeg;base64,...">
|
|
"""
|
|
try:
|
|
# Render bounding box dari YOLO
|
|
results = model(img_bgr, conf=0.7, verbose=False)
|
|
annotated = results[0].plot()
|
|
|
|
# Encode ke JPEG bytes lalu konversi ke Base64 string
|
|
# Quality 60 → ukuran ~30-60KB, cukup untuk preview dashboard
|
|
_, buffer = cv2.imencode('.jpg', annotated, [cv2.IMWRITE_JPEG_QUALITY, 60])
|
|
img_base64 = base64.b64encode(buffer).decode('utf-8')
|
|
foto_data = "data:image/jpeg;base64," + img_base64
|
|
|
|
# Simpan ke Realtime DB — selalu overwrite node yang sama
|
|
db.reference('smart_donation_box/last_detection').set({
|
|
'foto_url': foto_data, # Dashboard langsung pakai nilai ini di <img src>
|
|
'label': deteksi_label,
|
|
'timestamp': int(time.time() * 1000)
|
|
})
|
|
|
|
print(f"📸 Foto tersimpan ke Realtime DB ({len(img_base64) // 1024} KB)")
|
|
|
|
except Exception as e:
|
|
print(f"⚠️ Gagal simpan foto: {e}")
|
|
|
|
|
|
# ── 4. Helper: Catat transaksi donasi & update summary ───────────────────────
|
|
def update_firebase(nominal, confidence):
|
|
ref_log = db.reference('smart_donation_box/donasi_log')
|
|
ref_summary = db.reference('smart_donation_box/summary')
|
|
|
|
# Tambah entri baru di donasi_log
|
|
ref_log.push({
|
|
'nominal': nominal,
|
|
'confidence': round(float(confidence), 2),
|
|
'timestamp': int(time.time() * 1000)
|
|
})
|
|
|
|
# Update summary (total & saldo)
|
|
summary = ref_summary.get() or {}
|
|
old_total = summary.get('total_donasi', 0)
|
|
old_pengeluaran = summary.get('total_pengeluaran', 0)
|
|
new_total = old_total + nominal
|
|
new_saldo = new_total - old_pengeluaran
|
|
|
|
ref_summary.update({
|
|
'total_donasi': new_total,
|
|
'donasi_hari_ini': summary.get('donasi_hari_ini', 0) + nominal,
|
|
'saldo': new_saldo
|
|
})
|
|
|
|
print(f"✅ Donasi Rp {nominal:,} | Confidence {confidence:.0%} | Total Rp {new_total:,}")
|
|
|
|
|
|
# ── 5. Endpoint /predict ──────────────────────────────────────────────────────
|
|
@app.route('/predict', methods=['POST'])
|
|
def predict():
|
|
try:
|
|
# Terima raw JPEG bytes dari ESP32-CAM
|
|
file = request.data
|
|
nparr = np.frombuffer(file, np.uint8)
|
|
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
|
|
|
if img is None:
|
|
print("❌ Gagal decode gambar dari ESP32")
|
|
return "Gagal decode gambar", 400
|
|
|
|
print(f"📥 Frame diterima: {len(file) // 1024} KB")
|
|
|
|
# Jalankan prediksi YOLO
|
|
results = model.predict(img, conf=0.7, verbose=False)
|
|
detections = []
|
|
|
|
for r in results:
|
|
for box in r.boxes:
|
|
label = model.names[int(box.cls[0])]
|
|
conf = float(box.conf[0])
|
|
|
|
nominal = NOMINAL_MAP.get(label)
|
|
if nominal is not None:
|
|
update_firebase(nominal, conf)
|
|
detections.append(f"Rp {nominal:,}")
|
|
print(f"🎯 Terdeteksi: label='{label}' → Rp {nominal:,} ({conf:.0%})")
|
|
else:
|
|
print(f"⚠️ Label '{label}' tidak ada di NOMINAL_MAP, dilewati")
|
|
continue
|
|
|
|
# Simpan foto ke DB (selalu, baik ada deteksi maupun tidak)
|
|
label_str = ', '.join(detections) if detections else 'Tidak ada deteksi'
|
|
simpan_foto_terakhir(img, label_str)
|
|
|
|
if not detections:
|
|
print("🔍 Tidak ada uang terdeteksi")
|
|
return "Tidak ada objek terdeteksi", 200
|
|
|
|
print(f"🎯 Terdeteksi: {label_str}")
|
|
return f"Berhasil: {label_str}", 200
|
|
|
|
except Exception as e:
|
|
print(f"❌ Error di /predict: {e}")
|
|
return str(e), 500
|
|
|
|
|
|
# ── 6. Health check endpoint (opsional, untuk test koneksi) ──────────────────
|
|
@app.route('/ping', methods=['GET'])
|
|
def ping():
|
|
return "Smart Donation Box Server OK", 200
|
|
|
|
|
|
# ── 7. Jalankan Server ────────────────────────────────────────────────────────
|
|
if __name__ == '__main__':
|
|
print("=" * 50)
|
|
print(" Smart Donation Box — Flask Server")
|
|
print(" Listening on http://0.0.0.0:5000")
|
|
print(" Endpoint: POST /predict")
|
|
print("=" * 50)
|
|
app.run(host='0.0.0.0', port=5000, debug=False)
|