This commit includes major additions and updates to the frontend and backend architectures, introducing new dataset management, live counting features, batch processing, and triage logic. Includes new UI pages, components, and API routes.
686 lines
29 KiB
Python
686 lines
29 KiB
Python
import os
|
|
import cv2
|
|
import numpy as np
|
|
import time
|
|
import json
|
|
import sqlite3
|
|
from datetime import datetime, timedelta
|
|
from shapely.geometry import Point, Polygon
|
|
import torch
|
|
from ultralytics import YOLO
|
|
|
|
# Import repo rafan modules
|
|
from src.tracking import ByteTrackTracker
|
|
from src.stabilizer import BboxStabilizer
|
|
from src.truck_roi import TruckROI
|
|
from src.counting import LineCrossCounter
|
|
from src.batch import BatchLifecycleManager, BatchRecord
|
|
|
|
# =====================================================================
|
|
# 1. KONFIGURASI PENGUJIAN (TEST CONFIGURATION)
|
|
# =====================================================================
|
|
# Aktifkan MODE CEPAT agar Anda tidak perlu menunggu 5 menit untuk pengujian.
|
|
# Jika True: Timeout dipercepat agar transisi status bisa dipantau secara instan.
|
|
# Jika False: Menggunakan parameter waktu rill yang sama seperti di production.
|
|
FAST_TEST_MODE = True
|
|
|
|
# Lokasi File lokal di folder "D:\untuk asus\algoritma batch"
|
|
MODEL_PATH = r"D:\untuk asus\algoritma batch\v1-best.pt"
|
|
VIDEO_PATH = r"0727.mp4"
|
|
DB_PATH = r"D:\untuk asus\algoritma batch\batch_test.db"
|
|
STATE_FILE = r"D:\untuk asus\algoritma batch\current_batch_test.json"
|
|
ARCHIVE_OUTPUT = r"D:\untuk asus\algoritma batch\archive_output"
|
|
|
|
CAMERA_NAME = "CC1"
|
|
OBJECT_LABEL = "karung-pakan"
|
|
DAILY_CUTOFF_TIME = "00:00"
|
|
|
|
# --- Parameter State Machine ---
|
|
# STRATEGI AKHIR BATCH: Gabungan deteksi TRUK + KARUNG
|
|
# Batch berakhir HANYA jika TIDAK ADA truk DAN TIDAK ADA karung terdeteksi
|
|
# selama TRUCK_GONE_TOLERANCE detik. Ini mengatasi blind spot CCTV:
|
|
# - Truk kapasitas besar (700 karung) bisa menyimpan karung di area blind spot
|
|
# - Selama truk masih terlihat, batch tetap terbuka meskipun karung tidak terlihat
|
|
# - Batch baru berakhir saat truk benar-benar pergi (keluar dari frame)
|
|
SACK_IDLE_TIMEOUT = 5.0 # Jeda sebelum masuk WAITING_FOR_ACTIVITY
|
|
MIN_BATCH_DURATION = 2.0 # Durasi minimal batch
|
|
TRUCK_GONE_TOLERANCE = 5.0 # Toleransi 5 detik setelah truk+karung hilang
|
|
print(f"[INFO] Strategi akhir batch: Truk+Karung gabungan, toleransi {TRUCK_GONE_TOLERANCE}s")
|
|
|
|
|
|
# =====================================================================
|
|
# 2. INISIALISASI DATABASE & STATE BATCH
|
|
# =====================================================================
|
|
active_batch_info = None
|
|
|
|
def get_counting_date(dt=None):
|
|
if dt is None:
|
|
dt = datetime.now()
|
|
try:
|
|
cutoff = datetime.strptime(DAILY_CUTOFF_TIME, "%H:%M").time()
|
|
except Exception:
|
|
cutoff = datetime.strptime("20:00", "%H:%M").time()
|
|
|
|
if cutoff.hour == 0 and cutoff.minute == 0:
|
|
return dt.date().isoformat()
|
|
|
|
if dt.time() < cutoff:
|
|
return dt.date().isoformat()
|
|
return (dt.date() + timedelta(days=1)).isoformat()
|
|
|
|
|
|
def init_db():
|
|
try:
|
|
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cur = conn.cursor()
|
|
cur.execute("""
|
|
CREATE TABLE IF NOT EXISTS batches (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
counting_date TEXT NOT NULL,
|
|
batch_number INTEGER NOT NULL,
|
|
camera_name TEXT NOT NULL,
|
|
object_label TEXT NOT NULL,
|
|
count INTEGER NOT NULL,
|
|
start_time TEXT NOT NULL,
|
|
end_time TEXT NOT NULL,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(counting_date, batch_number, camera_name, object_label)
|
|
)
|
|
""")
|
|
cur.execute("""
|
|
CREATE TABLE IF NOT EXISTS daily_summaries (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
counting_date TEXT NOT NULL,
|
|
camera_name TEXT NOT NULL,
|
|
object_label TEXT NOT NULL,
|
|
total_count INTEGER NOT NULL DEFAULT 0,
|
|
total_batches INTEGER NOT NULL DEFAULT 0,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(counting_date, camera_name, object_label)
|
|
)
|
|
""")
|
|
conn.commit()
|
|
conn.close()
|
|
print(f"[DB Info] Database SQLite siap di: {DB_PATH}")
|
|
except Exception as e:
|
|
print(f"[DB Error] Gagal inisialisasi database: {e}")
|
|
|
|
|
|
def get_next_batch_number(counting_date):
|
|
try:
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cur = conn.cursor()
|
|
cur.execute("""
|
|
SELECT COALESCE(MAX(batch_number), 0)
|
|
FROM batches
|
|
WHERE counting_date = ? AND camera_name = ? AND object_label = ?
|
|
""", (counting_date, CAMERA_NAME, OBJECT_LABEL))
|
|
row = cur.fetchone()
|
|
conn.close()
|
|
return row[0] + 1
|
|
except Exception as e:
|
|
print(f"[DB Error] Gagal mendapatkan batch_number: {e}")
|
|
return 1
|
|
|
|
|
|
def save_active_batch_state():
|
|
global active_batch_info
|
|
if active_batch_info is None:
|
|
try:
|
|
if os.path.exists(STATE_FILE):
|
|
os.remove(STATE_FILE)
|
|
except Exception:
|
|
pass
|
|
return
|
|
try:
|
|
with open(STATE_FILE, 'w', encoding='utf-8') as f:
|
|
json.dump(active_batch_info, f, indent=2, ensure_ascii=False)
|
|
except Exception as e:
|
|
print(f"[DB Error] Gagal menulis {STATE_FILE}: {e}")
|
|
|
|
|
|
def finalize_batch(final_count, start_time_iso, end_time_iso):
|
|
global active_batch_info
|
|
if active_batch_info is None:
|
|
return
|
|
if final_count == 0:
|
|
print(f"\n[BATCH] Batch #{active_batch_info.get('batch_number', 0)} dengan hitungan 0 diabaikan (tidak disimpan ke database).")
|
|
active_batch_info = None
|
|
save_active_batch_state()
|
|
return
|
|
|
|
counting_date = active_batch_info["counting_date"]
|
|
batch_num = active_batch_info["batch_number"]
|
|
try:
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cur = conn.cursor()
|
|
|
|
# 1. Simpan data batch
|
|
cur.execute("""
|
|
INSERT OR REPLACE INTO batches
|
|
(counting_date, batch_number, camera_name, object_label, count, start_time, end_time)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
""", (counting_date, batch_num, CAMERA_NAME, OBJECT_LABEL, final_count, start_time_iso, end_time_iso))
|
|
|
|
# 2. Update ringkasan harian
|
|
cur.execute("""
|
|
SELECT SUM(count), COUNT(id)
|
|
FROM batches
|
|
WHERE counting_date = ? AND camera_name = ? AND object_label = ?
|
|
""", (counting_date, CAMERA_NAME, OBJECT_LABEL))
|
|
sum_row = cur.fetchone()
|
|
tot_count = sum_row[0] if sum_row[0] is not None else 0
|
|
tot_batches = sum_row[1] if sum_row[1] is not None else 0
|
|
|
|
cur.execute("""
|
|
INSERT OR REPLACE INTO daily_summaries
|
|
(counting_date, camera_name, object_label, total_count, total_batches, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
""", (counting_date, CAMERA_NAME, OBJECT_LABEL, tot_count, tot_batches))
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
print(f"\n[DB Info] Batch #{batch_num} sukses disimpan ke DB. Total karung: {final_count}")
|
|
except Exception as e:
|
|
print(f"[DB Error] Gagal menyimpan batch: {e}")
|
|
|
|
active_batch_info = None
|
|
save_active_batch_state()
|
|
|
|
|
|
# =====================================================================
|
|
# 3. FUNGSI GAMBAR VISUALISASI HUD & POLIGON
|
|
# =====================================================================
|
|
def draw_visualization(frame, stable_sacks, roi, counter, batch_mgr, sacks_in_truck, current_tolerance, trucks=None):
|
|
h, w = frame.shape[:2]
|
|
out = frame.copy()
|
|
|
|
# 1. Warna Status
|
|
state_colors = {
|
|
"IDLE": (0, 165, 255), # Oranye
|
|
"TRUCK_STABILIZING": (0, 230, 255), # Kuning
|
|
"COUNTING_SACKS": (0, 200, 0), # Hijau
|
|
"WAITING_FOR_ACTIVITY": (255, 200, 100) # Biru Muda
|
|
}
|
|
state_labels = {
|
|
"IDLE": "WAITING FOR FIRST SACK CROSSING",
|
|
"TRUCK_STABILIZING": "STABILIZING SESSION...",
|
|
"COUNTING_SACKS": "BATCH RUNNING - COUNTING ACTIVE",
|
|
"WAITING_FOR_ACTIVITY": "PAUSED - WAITING FOR SACKS OR DEPARTURE"
|
|
}
|
|
|
|
current_state = batch_mgr.state
|
|
lbl_color = state_colors.get(current_state, (140, 140, 140))
|
|
lbl_text = state_labels.get(current_state, "UNKNOWN")
|
|
|
|
# 2. Gambar Zona Deteksi Sacks (Detection Area)
|
|
scale_x = 1280.0 / 1920.0
|
|
scale_y = 720.0 / 1080.0
|
|
|
|
detection_poly_pts = np.array([
|
|
[int(574 * scale_x), int(50 * scale_y)],
|
|
[int(586 * scale_x), int(1077 * scale_y)],
|
|
[int(1418 * scale_x), int(1076 * scale_y)],
|
|
[int(1397 * scale_x), int(50 * scale_y)],
|
|
], dtype=np.int32)
|
|
cv2.polylines(out, [detection_poly_pts], True, (255, 0, 255), 1, lineType=cv2.LINE_AA)
|
|
|
|
# 3. Gambar ZONA_TRUCK (Tempat monitor sacks)
|
|
truck_poly_pts = np.array([
|
|
[int(600 * scale_x), int(385 * scale_y)],
|
|
[int(609 * scale_x), int(1076 * scale_y)],
|
|
[int(1404 * scale_x), int(1078 * scale_y)],
|
|
[int(1381 * scale_x), int(343 * scale_y)],
|
|
], dtype=np.int32)
|
|
truck_color = (0, 255, 0) if current_state == "COUNTING_SACKS" else (0, 140, 255)
|
|
cv2.polylines(out, [truck_poly_pts], True, truck_color, 2, lineType=cv2.LINE_AA)
|
|
|
|
# Isian Transparan Zona Truck
|
|
overlay = out.copy()
|
|
cv2.fillPoly(overlay, [truck_poly_pts], truck_color)
|
|
cv2.addWeighted(overlay, 0.08, out, 0.92, 0, out)
|
|
|
|
# Label Zona Truk
|
|
cv2.putText(out, "ZONA TRUK (PRESENCE MONITOR)", (truck_poly_pts[0][0] + 10, truck_poly_pts[0][1] - 10),
|
|
cv2.FONT_HERSHEY_SIMPLEX, 0.5, truck_color, 1, cv2.LINE_AA)
|
|
|
|
# 4. Gambar Line Crossing Zone
|
|
if roi is not None:
|
|
y = roi.line_y
|
|
cv2.line(out, (roi.x1, y), (roi.x2, y), (255, 0, 255), 2, lineType=cv2.LINE_AA)
|
|
margin = 20
|
|
cv2.rectangle(overlay, (roi.x1, y - margin), (roi.x2, y + margin), (255, 0, 255), -1)
|
|
cv2.addWeighted(overlay, 0.1, out, 0.9, 0, out)
|
|
cv2.putText(out, f"COUNT LINE Y={y}", (roi.x1 + 10, y - margin - 5),
|
|
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 255), 1, cv2.LINE_AA)
|
|
|
|
# 5a. Gambar TRUCK bounding boxes (hijau)
|
|
if trucks:
|
|
for det in trucks:
|
|
x1, y1, x2, y2 = [int(v) for v in det.bbox]
|
|
tid = det.track_id
|
|
lbl = f"truck"
|
|
if tid is not None:
|
|
lbl += f" #{tid}"
|
|
lbl += f" {det.confidence:.0%}"
|
|
cv2.rectangle(out, (x1, y1), (x2, y2), (0, 200, 0), 2)
|
|
cv2.putText(out, lbl, (x1, y1 - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 200, 0), 1, cv2.LINE_AA)
|
|
|
|
# 5b. Gambar Sacks terdeteksi
|
|
for det in stable_sacks:
|
|
x1, y1, x2, y2 = [int(v) for v in det.bbox]
|
|
cx, cy = int((x1 + x2) / 2), int((y1 + y2) / 2)
|
|
tid = det.track_id
|
|
|
|
lbl = f"sack"
|
|
if tid is not None:
|
|
lbl += f" #{tid}"
|
|
lbl += f" {det.confidence:.0%}"
|
|
|
|
# Tandai karung
|
|
cv2.rectangle(out, (x1, y1), (x2, y2), (0, 255, 255), 2)
|
|
# Garis pemicu (y1 - bagian atas karung)
|
|
cv2.line(out, (x1, y1), (x2, y1), (0, 255, 0), 3)
|
|
# Centroid dot
|
|
cv2.circle(out, (cx, cy), 4, (0, 255, 255), -1)
|
|
cv2.putText(out, lbl, (x1, y1 - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 255, 255), 1, cv2.LINE_AA)
|
|
|
|
# 6. Gambar Dedup Circles (Radius Anti-Double Count)
|
|
now_t = time.time()
|
|
for circle in counter._dedup_circles:
|
|
if (now_t - circle["time"]) <= 3.0:
|
|
cx, cy = int(circle["x"]), int(circle["y"])
|
|
cv2.circle(out, (cx, cy), int(counter._dedup_radius), (0, 255, 255), 2, lineType=cv2.LINE_AA)
|
|
cv2.circle(out, (cx, cy), 4, (0, 255, 0), -1)
|
|
cv2.putText(out, f"DEDUP #{circle['track_id']}", (cx - 25, cy - int(counter._dedup_radius) - 5),
|
|
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 255, 255), 1, cv2.LINE_AA)
|
|
|
|
# 7. Gambar Stats HUD Panel (Top-Left)
|
|
cv2.rectangle(out, (10, 10), (340, 175), (0, 0, 0), -1)
|
|
cv2.rectangle(out, (10, 10), (340, 175), (255, 255, 255), 1)
|
|
|
|
batch_num_text = f"Batch #{batch_mgr.current_batch_id}" if batch_mgr.is_active else "IDLE"
|
|
cv2.putText(out, f"SACK COUNTING BATCH SYSTEM", (20, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (255, 255, 255), 1, cv2.LINE_AA)
|
|
cv2.putText(out, f"Active Session: {batch_num_text}", (20, 55), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 230, 255), 2, cv2.LINE_AA)
|
|
|
|
cv2.putText(out, f"Sacks Counted (IN): {counter.loading_count}", (20, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 255, 0), 2, cv2.LINE_AA)
|
|
cv2.putText(out, f"Sacks In Truck: {sacks_in_truck}", (20, 115), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (255, 255, 0), 2, cv2.LINE_AA)
|
|
|
|
mode_lbl = "FAST TEST MODE (ACCELERATED)" if FAST_TEST_MODE else "NORMAL MODE"
|
|
cv2.putText(out, f"Mode: {mode_lbl}", (20, 145), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (0, 165, 255), 1, cv2.LINE_AA)
|
|
cv2.putText(out, f"Tolerance Limit: {current_tolerance:.0f}s", (20, 162), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 255, 255), 1, cv2.LINE_AA)
|
|
|
|
# 8. Gambar Sesi Riwayat Batch Sebelumnya
|
|
if batch_mgr.history:
|
|
y_hist = 200
|
|
cv2.putText(out, "RECENT BATCH HISTORY:", (20, y_hist), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (0, 230, 255), 1, cv2.LINE_AA)
|
|
for rec in batch_mgr.history[-3:]:
|
|
y_hist += 20
|
|
h_text = f"Batch #{rec.batch_id} -> Count: {rec.loading_count} sacks ({rec.duration_seconds:.1f}s)"
|
|
cv2.putText(out, h_text, (20, y_hist), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 255, 255), 1, cv2.LINE_AA)
|
|
|
|
# 9. Gambar Status Bar di Bagian Bawah
|
|
bar_h = 40
|
|
bar_y = h - bar_h
|
|
cv2.rectangle(out, (0, bar_y), (w, h), (20, 20, 20), -1)
|
|
cv2.line(out, (0, bar_y), (w, bar_y), (255, 255, 255), 1)
|
|
|
|
# Dot Indikator Status
|
|
cv2.circle(out, (25, bar_y + bar_h // 2), 7, lbl_color, -1)
|
|
cv2.circle(out, (25, bar_y + bar_h // 2), 7, (255, 255, 255), 1)
|
|
|
|
# Label Status
|
|
cv2.putText(out, f"STATUS: {current_state} - {lbl_text}", (45, bar_y + bar_h // 2 + 5),
|
|
cv2.FONT_HERSHEY_SIMPLEX, 0.5, lbl_color, 2, cv2.LINE_AA)
|
|
|
|
# 10. Tambah Detail Timer di Status Bar
|
|
if current_state == "COUNTING_SACKS":
|
|
dur = batch_mgr.batch_duration
|
|
idle = batch_mgr.time_since_last_sack_activity
|
|
timer_text = f"Duration: {dur:.1f}s | Idle Time: {idle:.1f}s / {SACK_IDLE_TIMEOUT:.1f}s"
|
|
cv2.putText(out, timer_text, (w - 400, bar_y + bar_h // 2 + 5),
|
|
cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 255, 255), 1, cv2.LINE_AA)
|
|
|
|
elif current_state == "WAITING_FOR_ACTIVITY":
|
|
waiting = batch_mgr.waiting_duration
|
|
time_left = max(0.0, current_tolerance - waiting)
|
|
timer_text = f"Waiting: {waiting:.1f}s | Time Left: {time_left:.1f}s / {current_tolerance:.1f}s"
|
|
cv2.putText(out, timer_text, (w - 400, bar_y + bar_h // 2 + 5),
|
|
cv2.FONT_HERSHEY_SIMPLEX, 0.45, (0, 200, 255), 1, cv2.LINE_AA)
|
|
|
|
return out
|
|
|
|
|
|
# =====================================================================
|
|
# 4. FUNGSI UTAMA PENJALANAN PREDIKSI & ALGORITMA BATCH
|
|
# =====================================================================
|
|
def run_batch_simulation():
|
|
# Inisialisasi Database
|
|
init_db()
|
|
|
|
# Device inferensi
|
|
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
|
print(f"[INFO] Menggunakan device: {device}")
|
|
|
|
# load model YOLO
|
|
print(f"[INFO] Membuka model: {MODEL_PATH}")
|
|
model = YOLO(MODEL_PATH)
|
|
|
|
# Warm-up model
|
|
print("[INFO] Warm-up model YOLO...")
|
|
dummy_frame = np.zeros((720, 1280, 3), dtype=np.uint8)
|
|
_ = model(dummy_frame, imgsz=640, device=device, verbose=False)
|
|
|
|
# Setup trackers, stabilizers, counters, dll.
|
|
tracker = ByteTrackTracker(model, conf=0.25)
|
|
stabilizer = BboxStabilizer(
|
|
ema_alpha=0.35,
|
|
max_hold_frames=10,
|
|
max_height_ratio=1.5,
|
|
min_height_ratio=0.70,
|
|
)
|
|
|
|
# Skala koordinat
|
|
scale_x = 1280.0 / 1920.0
|
|
scale_y = 720.0 / 1080.0
|
|
|
|
# Poligon untuk verifikasi sacks di area
|
|
detection_poly_pts = [
|
|
[int(574 * scale_x), int(50 * scale_y)],
|
|
[int(586 * scale_x), int(1077 * scale_y)],
|
|
[int(1418 * scale_x), int(1076 * scale_y)],
|
|
[int(1397 * scale_x), int(50 * scale_y)],
|
|
]
|
|
detection_polygon = Polygon(detection_poly_pts)
|
|
|
|
truck_poly_pts = [
|
|
[int(600 * scale_x), int(385 * scale_y)],
|
|
[int(609 * scale_x), int(1076 * scale_y)],
|
|
[int(1404 * scale_x), int(1078 * scale_y)],
|
|
[int(1381 * scale_x), int(343 * scale_y)],
|
|
]
|
|
truck_polygon = Polygon(truck_poly_pts)
|
|
|
|
static_line_y = int(330 * scale_y)
|
|
static_line_x_start = int(577 * scale_x)
|
|
static_line_x_end = int(1401 * scale_x)
|
|
|
|
static_roi = TruckROI(
|
|
x1=int(600 * scale_x),
|
|
y1=int(343 * scale_y),
|
|
x2=int(1404 * scale_x),
|
|
y2=int(1078 * scale_y),
|
|
line_y=static_line_y,
|
|
confidence=1.0
|
|
)
|
|
|
|
counter = LineCrossCounter(
|
|
line_y=static_line_y,
|
|
line_x_start=static_line_x_start,
|
|
line_x_end=static_line_x_end,
|
|
margin=20,
|
|
dedup_radius=60.0
|
|
)
|
|
|
|
# Setup Batch Manager
|
|
# CATATAN: truck_gone_tolerance awal harus > 0 agar TRUCK_STABILIZING tidak langsung
|
|
# kembali ke IDLE di frame berikutnya saat has_crossing=False.
|
|
# Toleransi 0.0s (instan) hanya diterapkan saat WAITING_FOR_ACTIVITY.
|
|
batch_mgr = BatchLifecycleManager(
|
|
stabilize_seconds=0.0, # Start batch secara instan ketika ada karung melintasi garis
|
|
stabilize_threshold_px=9999.0, # Matikan displacement threshold
|
|
sack_idle_timeout=SACK_IDLE_TIMEOUT,
|
|
min_batch_duration=MIN_BATCH_DURATION,
|
|
truck_gone_tolerance=3.0, # Toleransi awal 3s untuk TRUCK_STABILIZING
|
|
)
|
|
|
|
# Membuka file video
|
|
print(f"[INFO] Membuka file video: {VIDEO_PATH}")
|
|
cap = cv2.VideoCapture(VIDEO_PATH)
|
|
if not cap.isOpened():
|
|
print(f"[ERROR] Gagal membuka video: {VIDEO_PATH}")
|
|
return
|
|
|
|
fps = cap.get(cv2.CAP_PROP_FPS)
|
|
if fps <= 0 or np.isnan(fps):
|
|
fps = 25.0
|
|
|
|
frame_idx = 0
|
|
|
|
# --- Video Recording per Batch ---
|
|
video_writer = None
|
|
video_path = None
|
|
video_frame_count = 0
|
|
|
|
global active_batch_info
|
|
|
|
print("\n" + "=" * 50)
|
|
print("MULAILAH MENYAKSIKAN ALGORITMA BATCH BEKERJA")
|
|
print("Tekan 'q' pada jendela video untuk berhenti.")
|
|
print("=" * 50 + "\n")
|
|
|
|
try:
|
|
while cap.isOpened():
|
|
ret, frame = cap.read()
|
|
if not ret:
|
|
print("[INFO] Video selesai dibaca.")
|
|
break
|
|
|
|
# Resize ke 1280x720 untuk koordinat statis
|
|
frame = cv2.resize(frame, (1280, 720))
|
|
timestamp = time.time()
|
|
frame_idx += 1
|
|
|
|
# Simpan state status batch sebelum update untuk melacak transisi
|
|
prev_active = batch_mgr.is_active
|
|
prev_state = batch_mgr.state
|
|
|
|
# 1. Update Tracker YOLO (menggunakan custom config tracker.yaml)
|
|
raw_tracked_all = tracker.update(frame, [])
|
|
|
|
# Hanya proses objek yang pusatnya berada di dalam area deteksi (poligon ungu)
|
|
raw_tracked_filtered = [
|
|
d for d in raw_tracked_all
|
|
if detection_polygon.contains(Point((d.bbox[0] + d.bbox[2]) / 2.0, (d.bbox[1] + d.bbox[3]) / 2.0))
|
|
]
|
|
|
|
# Filter per class (model v1: {0:'truck', 1:'sack'})
|
|
raw_tracked_sacks = [d for d in raw_tracked_filtered if d.class_name == "sack"]
|
|
raw_tracked_trucks = [d for d in raw_tracked_filtered if d.class_name == "truck"]
|
|
|
|
# 2. Update Stabilizer Bounding Box
|
|
stable = stabilizer.update(raw_tracked_sacks)
|
|
|
|
# 4. Hitung jumlah karung di dalam ZONA TRUCK (hanya y >= cutoff)
|
|
min_ty, max_ty = truck_polygon.bounds[1], truck_polygon.bounds[3]
|
|
truck_height = max_ty - min_ty
|
|
truck_cutoff_y = min_ty + 0.30 * truck_height
|
|
|
|
sacks_in_truck_area = 0
|
|
for d in stable:
|
|
cx = (d.bbox[0] + d.bbox[2]) / 2.0
|
|
cy = (d.bbox[1] + d.bbox[3]) / 2.0
|
|
if truck_polygon.contains(Point(cx, cy)) and cy >= truck_cutoff_y:
|
|
sacks_in_truck_area += 1
|
|
|
|
# 5. Filter ROI & hitung penyeberangan garis
|
|
tracked_sacks_in_roi = [d for d in stable if static_roi.contains_x((d.bbox[0] + d.bbox[2]) / 2.0)]
|
|
events = counter.update(tracked_sacks_in_roi)
|
|
has_crossing = len(events) > 0
|
|
|
|
# ================================================================
|
|
# LOGIKA ALGORITMA PENENTUAN BATCH (STATE MACHINE TRANSITIONS)
|
|
# ================================================================
|
|
current_tolerance = TRUCK_GONE_TOLERANCE
|
|
|
|
# A. Mulai Batch (Start Batch) - Jika status IDLE atau STABILIZING
|
|
if batch_mgr.state in ("IDLE", "TRUCK_STABILIZING"):
|
|
# Picu transisi batch start saat terdeteksi event penyeberangan karung
|
|
batch_mgr.update_truck(has_crossing, (0.0, 0.0), timestamp)
|
|
|
|
# B. Jalankan Monitoring Batch & Penutupan Dinamis
|
|
if batch_mgr.state in ("COUNTING_SACKS", "WAITING_FOR_ACTIVITY"):
|
|
# Perbarui aktivitas karung di batch manager
|
|
batch_mgr.update_sacks(
|
|
has_crossing_event=has_crossing,
|
|
sacks_in_area_count=sacks_in_truck_area,
|
|
timestamp=timestamp,
|
|
loading_count=counter.loading_count,
|
|
unloading_count=counter.unloading_count,
|
|
)
|
|
|
|
# STRATEGI AKHIR BATCH: Gabungan Truk + Karung
|
|
# Batch tetap terbuka selama SALAH SATU kondisi terpenuhi:
|
|
# 1. Ada karung terdeteksi di area truk, ATAU
|
|
# 2. Ada truk yang terdeteksi di frame
|
|
# Batch berakhir HANYA jika KEDUANYA hilang selama TRUCK_GONE_TOLERANCE detik
|
|
if batch_mgr.state == "WAITING_FOR_ACTIVITY":
|
|
# Sinkronkan _truck_last_seen agar countdown toleransi
|
|
# mulai dari saat WAITING dimulai, bukan dari TRUCK_STABILIZING
|
|
if batch_mgr._truck_last_seen < batch_mgr._waiting_since:
|
|
batch_mgr._truck_last_seen = batch_mgr._waiting_since
|
|
|
|
truck_still_present = len(raw_tracked_trucks) > 0
|
|
anything_detected = (sacks_in_truck_area > 0) or truck_still_present
|
|
batch_mgr._truck_gone_tolerance = TRUCK_GONE_TOLERANCE
|
|
batch_mgr.update_truck(anything_detected, None, timestamp)
|
|
|
|
if frame_idx % 30 == 0: # Log setiap 30 frame
|
|
print(f"[WAITING] truck={truck_still_present} sacks={sacks_in_truck_area} → keep_open={anything_detected}")
|
|
|
|
current_tolerance = TRUCK_GONE_TOLERANCE
|
|
|
|
# Tangani event penyeberangan karung
|
|
for ev in events:
|
|
print(f"[{datetime.now().strftime('%H:%M:%S')}] [KARUNG] Karung #{ev['track_id']} terhitung melintasi garis. Total saat ini: {counter.loading_count}")
|
|
if active_batch_info is not None:
|
|
active_batch_info["count"] = counter.loading_count
|
|
active_batch_info["last_detection_time"] = datetime.now().isoformat()
|
|
save_active_batch_state()
|
|
|
|
# C. Transisi: Batch Baru Saja Terbuka
|
|
if batch_mgr.is_active and not prev_active:
|
|
counting_date = get_counting_date()
|
|
batch_num = get_next_batch_number(counting_date)
|
|
now_iso = datetime.now().isoformat()
|
|
|
|
active_batch_info = {
|
|
"counting_date": counting_date,
|
|
"batch_number": batch_num,
|
|
"count": 0,
|
|
"start_time": now_iso,
|
|
"last_detection_time": now_iso
|
|
}
|
|
save_active_batch_state()
|
|
print(f"\n>>> [TRANSISI BATCH] Batch #{batch_num} DIMULAI pada tanggal {counting_date} (Status: COUNTING_SACKS) <<<")
|
|
|
|
# Mulai rekam video untuk batch ini
|
|
date_folder = os.path.join(ARCHIVE_OUTPUT, counting_date)
|
|
os.makedirs(date_folder, exist_ok=True)
|
|
timestamp_str = datetime.now().strftime("%H-%M-%S")
|
|
video_path = os.path.join(date_folder, f"batch_{batch_num}_{timestamp_str}.mp4")
|
|
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
|
video_writer = cv2.VideoWriter(video_path, fourcc, 10.0, (1280, 720))
|
|
video_frame_count = 0
|
|
if video_writer.isOpened():
|
|
print(f"[RECORD] Mulai merekam -> {video_path}")
|
|
else:
|
|
print(f"[RECORD ERROR] Gagal membuka VideoWriter!")
|
|
video_writer = None
|
|
|
|
# D. Transisi: Batch Baru Saja Selesai/Tutup (Truk Pergi)
|
|
elif not batch_mgr.is_active and prev_active:
|
|
final_count = counter.loading_count
|
|
start_iso = active_batch_info["start_time"] if active_batch_info else datetime.now().isoformat()
|
|
end_iso = datetime.now().isoformat()
|
|
batch_num = active_batch_info["batch_number"] if active_batch_info else 0
|
|
|
|
print(f"\n>>> [TRANSISI BATCH] Truk pergi setelah status WAITING. Batch #{batch_num} SELESAI. Total karung: {final_count} <<<")
|
|
finalize_batch(final_count, start_iso, end_iso)
|
|
|
|
# Simpan video rekaman batch
|
|
if video_writer is not None:
|
|
video_writer.release()
|
|
if video_frame_count > 0:
|
|
print(f"[RECORD] Video batch #{batch_num} disimpan: {video_path} ({video_frame_count} frames)")
|
|
else:
|
|
# Hapus file kosong
|
|
try:
|
|
os.remove(video_path)
|
|
except Exception:
|
|
pass
|
|
video_writer = None
|
|
video_frame_count = 0
|
|
|
|
# Reset penghitung lokal dan stabilizer ID
|
|
counter.reset()
|
|
stabilizer.reset()
|
|
|
|
# E. Logging Transisi Status
|
|
if batch_mgr.state != prev_state:
|
|
print(f"[{datetime.now().strftime('%H:%M:%S')}] [STATE MACHINE] Transisi Status: {prev_state} -> {batch_mgr.state}")
|
|
|
|
# ================================================================
|
|
# TULIS FRAME KE VIDEO (jika batch aktif)
|
|
# ================================================================
|
|
if batch_mgr.is_active and video_writer is not None and video_writer.isOpened():
|
|
video_writer.write(frame)
|
|
video_frame_count += 1
|
|
|
|
# ================================================================
|
|
# VISUALISASI
|
|
# ================================================================
|
|
visualized_frame = draw_visualization(
|
|
frame=frame,
|
|
stable_sacks=tracked_sacks_in_roi,
|
|
roi=static_roi,
|
|
counter=counter,
|
|
batch_mgr=batch_mgr,
|
|
sacks_in_truck=sacks_in_truck_area,
|
|
current_tolerance=current_tolerance,
|
|
trucks=raw_tracked_trucks,
|
|
)
|
|
|
|
# Tampilkan frame di window OpenCV
|
|
cv2.imshow("Pengujian Algoritma Batch", visualized_frame)
|
|
|
|
# Dengarkan tombol key
|
|
key = cv2.waitKey(1) & 0xFF
|
|
if key == ord('q'):
|
|
print("[INFO] Pengujian dihentikan secara manual oleh pengguna.")
|
|
break
|
|
|
|
finally:
|
|
# Bersihkan resource
|
|
if video_writer is not None:
|
|
video_writer.release()
|
|
if video_frame_count > 0 and video_path:
|
|
print(f"[RECORD] Video batch terakhir disimpan: {video_path} ({video_frame_count} frames)")
|
|
cap.release()
|
|
cv2.destroyAllWindows()
|
|
|
|
print("\n" + "=" * 50)
|
|
print("SIMULASI PENGUJIAN SELESAI!")
|
|
if active_batch_info is not None:
|
|
# Simpan batch yang masih terbuka saat program ditutup
|
|
final_count = counter.loading_count
|
|
start_iso = active_batch_info["start_time"]
|
|
end_iso = datetime.now().isoformat()
|
|
batch_num = active_batch_info["batch_number"]
|
|
print(f"[INFO] Menyimpan batch aktif terakhir #{batch_num} sebelum menutup program...")
|
|
finalize_batch(final_count, start_iso, end_iso)
|
|
|
|
# Cetak hasil histori batch uji coba
|
|
print("Histori Batch Terhitung Selama Simulasi:")
|
|
if batch_mgr.history:
|
|
for rec in batch_mgr.history:
|
|
print(f" - Batch #{rec.batch_id}: Mulai={datetime.fromtimestamp(rec.start_time).strftime('%H:%M:%S')}, Selesai={datetime.fromtimestamp(rec.end_time).strftime('%H:%M:%S')}, Jumlah Karung={rec.loading_count}")
|
|
else:
|
|
print(" - Tidak ada batch selesai yang tersimpan (atau semua batch bernilai 0).")
|
|
print("=" * 50)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
run_batch_simulation()
|