Files
feedmill-auto-label/algoritma-batch/test_batch_logic.py
T
asus 5c7c122105 feat: add counting bench, triage, and dataset modules
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.
2026-08-14 16:28:52 +07:00

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