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