refactor: single predict.py entrypoint (production + CLI), archive experiments
ci / smoke (push) Canceled after 0s

This commit is contained in:
andrew committed 2026-09-10 15:56:17 +07:00
1 parent beb9c9fb6b
commit 52d3127302
36 files changed
+161 -48

No files matched your search

+1388
View File
File diff suppressed because it is too large. Load diff
+31
View File
@@ -0,0 +1,31 @@
import cv2
import time
def main():
source = "rtsp://192.168.192.96:8554/cam"
print("Connecting to stream...")
cap = cv2.VideoCapture(source)
if not cap.isOpened():
print("Error: Could not open RTSP source.")
return
print("Warming up reader...")
time.sleep(3.0)
# Read a few frames to clear the buffer
for _ in range(15):
ret, frame = cap.read()
if ret and frame is not None:
h, w, c = frame.shape
print(f"Captured frame with resolution: {w}x{h}")
cv2.imwrite("/home/jetson/karung/live_frame_native.png", frame)
print("Frame saved successfully on Jetson.")
else:
print("Error: Failed to read frame from stream.")
cap.release()
if __name__ == '__main__':
main()
+8
View File
@@ -0,0 +1,8 @@
import sqlite3
conn = sqlite3.connect('/opt/jetson-counter/jetson_counter.db')
cur = conn.cursor()
cur.execute("SELECT id, counting_date, batch_number, count, start_time, end_time FROM batches WHERE counting_date = '2026-08-20' AND batch_number >= 19 ORDER BY batch_number ASC")
rows = cur.fetchall()
for r in rows:
print(r)
conn.close()
+8
View File
@@ -0,0 +1,8 @@
import sqlite3
conn = sqlite3.connect('/opt/jetson-counter/jetson_counter.db')
cur = conn.cursor()
cur.execute("SELECT id, counting_date, batch_number, count, start_time, end_time FROM batches WHERE counting_date = '2026-08-20' AND batch_number >= 25 ORDER BY batch_number ASC")
rows = cur.fetchall()
for r in rows:
print(r)
conn.close()
+8
View File
@@ -0,0 +1,8 @@
import sqlite3
conn = sqlite3.connect('/opt/jetson-counter/jetson_counter.db')
cur = conn.cursor()
cur.execute("SELECT id, counting_date, batch_number, count, start_time, end_time FROM batches WHERE counting_date >= '2026-08-20' AND batch_number >= 35 ORDER BY counting_date ASC, batch_number ASC")
rows = cur.fetchall()
for r in rows:
print(r)
conn.close()
+94
View File
@@ -0,0 +1,94 @@
import sys
import os
import cv2
import torch
import time
import numpy as np
from ultralytics import YOLO
from shapely.geometry import Polygon, Point
def main():
print("=== Detailed Jetson Detection Test ===")
# 1. Load Zones
zones_path = "/home/jetson/karung/zones.json"
truck_pts = []
if os.path.exists(zones_path):
import json
with open(zones_path, 'r') as f:
data = json.load(f)
truck_pts = data.get('truck', [])
print(f"Zones.json truck points: {truck_pts}")
# Target resolution
target_w, target_h = 1280, 720
# Scale factors assuming zones were drawn on 1920x1080
orig_w, orig_h = 1920, 1080
scale_x = target_w / orig_w
scale_y = target_h / orig_h
scaled_truck_pts = [[int(p[0] * scale_x), int(p[1] * scale_y)] for p in truck_pts] if truck_pts else [
[389, 294], [398, 718], [885, 719], [885, 277]
]
truck_polygon = Polygon(scaled_truck_pts)
print(f"Scaled truck polygon: {scaled_truck_pts}")
# 2. Open Stream
source = "rtsp://192.168.192.96:8554/cam"
print(f"Connecting to RTSP stream: {source}...")
cap = cv2.VideoCapture(source)
if not cap.isOpened():
print("Error: Could not open RTSP source.")
return
# Wait for the stream to warm up and buffer
print("Warming up stream reader for 3 seconds...")
time.sleep(3.0)
# 3. Load Model
model_path = "/home/jetson/karung/model_karung_truk.engine"
print(f"Loading TensorRT Model: {model_path}...")
model = YOLO(model_path)
print("Running 10 frames of inference...")
detections_summary = {}
frame_count = 0
attempts = 0
while frame_count < 10 and attempts < 100:
ret, frame = cap.read()
attempts += 1
if not ret or frame is None:
time.sleep(0.1)
continue
frame_count += 1
frame_resized = cv2.resize(frame, (target_w, target_h))
results = model(frame_resized, conf=0.01, imgsz=640, device="cuda", verbose=False)
result = results[0]
detected_in_frame = []
for box in result.boxes:
cls_id = int(box.cls[0])
name = model.names[cls_id]
conf = float(box.conf[0])
x1, y1, x2, y2 = box.xyxy[0].tolist()
tcx = (x1 + x2) / 2.0
tcy = (y1 + y2) / 2.0
# Check if inside truck polygon
pt = Point(tcx, tcy)
in_poly = truck_polygon.contains(pt)
detected_in_frame.append(f"{name} ({conf:.3f}) at ({tcx:.1f},{tcy:.1f}) in_poly={in_poly}")
detections_summary[name] = detections_summary.get(name, 0) + 1
print(f"Frame {frame_count} (attempt {attempts}): {', '.join(detected_in_frame) if detected_in_frame else 'None'}")
cap.release()
print("\nSummary of detected objects over processed frames:")
print(detections_summary)
if __name__ == "__main__":
main()
+13
View File
@@ -0,0 +1,13 @@
import sqlite3
conn = sqlite3.connect('/opt/jetson-counter/jetson_counter.db')
cur = conn.cursor()
cur.execute("SELECT DISTINCT counting_date FROM batches ORDER BY counting_date DESC LIMIT 5")
dates = cur.fetchall()
print("Dates:", dates)
if dates:
latest = dates[0][0]
cur.execute("SELECT id, counting_date, batch_number, count, start_time, end_time FROM batches WHERE counting_date = ? ORDER BY batch_number ASC", (latest,))
rows = cur.fetchall()
for r in rows:
print(r)
conn.close()
+16
View File
@@ -0,0 +1,16 @@
import os
from ultralytics import YOLO
pt_path = '/home/jetson/karung/v4-best.pt'
print('[INFO] Inspecting new model...')
model = YOLO(pt_path)
print('Model Names:', model.names)
print('Model Task:', model.task)
print('[INFO] Exporting v4-best.pt to TensorRT engine (half=True, imgsz=640)...')
try:
engine_path = model.export(format='engine', device=0, half=True, imgsz=640)
print('[SUCCESS] TensorRT Engine exported:', engine_path)
except Exception as e:
print('[WARNING] TensorRT export exception:', e)
+35
View File
@@ -0,0 +1,35 @@
import cv2
import sys
def main():
source = "2026-07-27 09-11-50.mp4"
if len(sys.argv) > 1:
source = sys.argv[1]
print(f"Connecting to: {source}")
cap = cv2.VideoCapture(source)
if not cap.isOpened():
print("Error: Could not open source!")
return
# Read a few frames to let the camera stabilize exposure/stream
print("Reading frames...")
frame = None
for i in range(10):
ret, temp_frame = cap.read()
if ret:
frame = temp_frame
if frame is None:
print("Error: Could not read any frame from the source!")
cap.release()
return
output_filename = "calib_frame.jpg"
cv2.imwrite(output_filename, frame)
print(f"Successfully saved clean frame as {output_filename}!")
print(f"Resolution: {frame.shape[1]}x{frame.shape[0]}")
cap.release()
if __name__ == "__main__":
main()
+207
View File
@@ -0,0 +1,207 @@
import cv2
import os
import json
import numpy as np
# State constants
STATE_TRUCK = 0
STATE_DETECTION = 1
STATE_LINE = 2
STATE_DONE = 3
state = STATE_TRUCK
points_truck = []
points_detection = []
points_line = []
def click_event(event, x, y, flags, params):
global state
if event == cv2.EVENT_LBUTTONDOWN:
if state == STATE_TRUCK:
points_truck.append([x, y])
print(f"Truck Area - Point {len(points_truck)}: [{x}, {y}]")
if len(points_truck) == 4:
state = STATE_DETECTION
print("\n-> Area Truk Berhasil Dipilih (4 titik).")
print("-> SILAHKAN PILIH AREA DETEKSI (Klik Kiri 4 Titik secara berurutan: Top-Left, Top-Right, Bottom-Right, Bottom-Left).")
elif state == STATE_DETECTION:
points_detection.append([x, y])
print(f"Detection Area - Point {len(points_detection)}: [{x}, {y}]")
if len(points_detection) == 4:
state = STATE_LINE
print("\n-> Area Deteksi Berhasil Dipilih (4 titik).")
print("-> SILAHKAN PILIH COUNT LINE (Klik Kiri Titik Mulai dan Titik Selesai).")
elif state == STATE_LINE:
points_line.append([x, y])
print(f"Count Line - Point {len(points_line)}: [{x}, {y}]")
if len(points_line) == 2:
state = STATE_DONE
print("\n-> Count Line Berhasil Dipilih.")
print("-> Semua koordinat telah lengkap! Tekan 's' untuk mencetak & menyimpan koordinat.")
draw_frame()
def draw_frame():
img_copy = img.copy()
h, w, _ = img_copy.shape
# 1. Draw Area Truk (Orange Polygon)
for pt in points_truck:
cv2.circle(img_copy, tuple(pt), 5, (0, 165, 255), -1)
if len(points_truck) == 4:
pts = np.array(points_truck, np.int32)
cv2.polylines(img_copy, [pts], True, (0, 165, 255), 2)
cv2.putText(img_copy, "TRUCK AREA", tuple(points_truck[0]),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 165, 255), 1)
# 2. Draw Area Deteksi (Cyan Polygon)
for pt in points_detection:
cv2.circle(img_copy, tuple(pt), 5, (255, 255, 0), -1)
if len(points_detection) == 4:
pts = np.array(points_detection, np.int32)
cv2.polylines(img_copy, [pts], True, (255, 255, 0), 2)
cv2.putText(img_copy, "DETECTION AREA", tuple(points_detection[0]),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 0), 1)
# 3. Draw Count Line (Magenta Line)
if len(points_line) > 0:
cv2.circle(img_copy, tuple(points_line[0]), 5, (255, 0, 255), -1)
if len(points_line) == 2:
cv2.line(img_copy, tuple(points_line[0]), tuple(points_line[1]), (255, 0, 255), 3)
# Draw midpoint circle
mid_x = int((points_line[0][0] + points_line[1][0]) / 2)
mid_y = int((points_line[0][1] + points_line[1][1]) / 2)
cv2.circle(img_copy, (mid_x, mid_y), 6, (0, 255, 0), -1)
cv2.putText(img_copy, f"LINE (y={mid_y})", (mid_x + 10, mid_y - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 255), 1)
# Draw Instruction Overlay on Top
overlay_y = 35
if state == STATE_TRUCK:
txt = f"1. PILIH AREA TRUK (Klik Kiri 4 Titik, saat ini: {len(points_truck)}/4)"
color = (0, 165, 255)
elif state == STATE_DETECTION:
txt = f"2. PILIH AREA DETEKSI (Klik Kiri 4 Titik, saat ini: {len(points_detection)}/4)"
color = (255, 255, 0)
elif state == STATE_LINE:
txt = f"3. PILIH COUNT LINE (Klik Kiri Titik Awal lalu Titik Akhir, saat ini: {len(points_line)}/2)"
color = (255, 0, 255)
else:
txt = "SELESAI! Tekan 's' untuk simpan atau 'c' untuk ulang."
color = (0, 255, 0)
# Draw background panel for text
cv2.rectangle(img_copy, (10, 10), (w - 10, 50), (0, 0, 0), -1)
cv2.putText(img_copy, txt, (20, overlay_y), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
cv2.imshow('Interactive 4-Point Zone Selector', img_copy)
if __name__ == "__main__":
source_img = "gambar_terbaru.jpg" if os.path.exists("gambar_terbaru.jpg") else "calib_frame.jpg"
video_source = "0727.mp4"
if os.path.exists(source_img):
img = cv2.imread(source_img)
print(f"Loaded image: {source_img}")
elif os.path.exists(video_source):
print(f"Grabbing frame from video: {video_source}")
cap = cv2.VideoCapture(video_source)
for _ in range(10):
ret, img = cap.read()
cap.release()
if not ret:
print("Error: Could not grab frame from video.")
exit(1)
else:
print("Error: Neither calib_frame.jpg nor the video file exists.")
exit(1)
cv2.namedWindow('Interactive 4-Point Zone Selector')
cv2.setMouseCallback('Interactive 4-Point Zone Selector', click_event)
print("\n=== OpenCV 4-Point Zone Coordinate Selector ===")
print("Instructions:")
print("1. Left-click to select coordinates for each step.")
print("2. Press 'c' at any time to clear selection and restart.")
print("3. Press 's' when done to print python snippets and save to zones_output.json.")
print("4. Press 'q' or 'ESC' to quit.")
print("========================================\n")
print("-> SILAHKAN PILIH AREA TRUK (Klik Kiri 4 Titik secara berurutan: Top-Left, Top-Right, Bottom-Right, Bottom-Left).")
draw_frame()
while True:
key = cv2.waitKey(1) & 0xFF
if key == ord('c') or key == ord('C'):
state = STATE_TRUCK
points_truck = []
points_detection = []
points_line = []
print("\nCleared selection. Restarting from Step 1 (Area Truk)...")
draw_frame()
elif key == ord('s') or key == ord('S'):
if state != STATE_DONE:
print(f"Warning: Harap selesaikan semua langkah terlebih dahulu. State saat ini: {state}")
continue
lx1, ly1 = points_line[0]
lx2, ly2 = points_line[1]
line_y_avg = int((ly1 + ly2) / 2)
output_data = {
"truck_poly": points_truck,
"detection_poly": points_detection,
"count_line": {
"x_start": lx1, "x_end": lx2, "y": line_y_avg
}
}
# Print configuration code snippets
print("\n" + "="*50)
print("KOORDINAT BERHASIL DI-GENERATE!")
print("="*50)
print("\n--- SALIN KODE DI BAWAH INI KE predict.py ---\n")
print(f" # 1. Detection Area (4-point Polygon)")
print(f" detection_poly_pts = [")
for pt in points_detection:
print(f" [int({pt[0]} * scale_x), int({pt[1]} * scale_y)],")
print(f" ]")
print(f" detection_polygon = Polygon(detection_poly_pts)")
print()
print(f" # 2. Count Line coordinates")
print(f" static_line_y = int({line_y_avg} * scale_y)")
print(f" static_line_x_start = int({lx1} * scale_x)")
print(f" static_line_x_end = int({lx2} * scale_x)")
print()
print(f" # 3. Truck Area (4-point Polygon for presence check)")
print(f" truck_poly_pts = [")
for pt in points_truck:
print(f" [int({pt[0]} * scale_x), int({pt[1]} * scale_y)],")
print(f" ]")
print(f" truck_polygon = Polygon(truck_poly_pts)")
print()
# Calculate bounding box of truck_polygon to maintain backward compatibility with static_roi
tx_coords = [p[0] for p in points_truck]
ty_coords = [p[1] for p in points_truck]
min_tx, max_tx = min(tx_coords), max(tx_coords)
min_ty, max_ty = min(ty_coords), max(ty_coords)
print(f" static_roi = TruckROI(")
print(f" x1=int({min_tx} * scale_x),")
print(f" y1=int({min_ty} * scale_y),")
print(f" x2=int({max_tx} * scale_x),")
print(f" y2=int({max_ty} * scale_y),")
print(f" line_y=static_line_y,")
print(f" confidence=1.0")
print(f" )")
print("\n" + "="*50)
# Save to json file
with open("zones_output.json", "w") as f:
json.dump(output_data, f, indent=4)
print("Koordinat juga telah disimpan ke 'zones_output.json'\n")
elif key == ord('q') or key == 27:
break
cv2.destroyAllWindows()
+2100
View File
File diff suppressed because it is too large. Load diff
+76
View File
@@ -0,0 +1,76 @@
import sqlite3
import shutil
from datetime import datetime
db_path = '/opt/jetson-counter/jetson_counter.db'
# 1. Backup database
backup_path = f'/opt/jetson-counter/jetson_counter.db.bak_{datetime.now().strftime("%Y%m%d_%H%M%S")}'
shutil.copy2(db_path, backup_path)
print(f"Backup created at: {backup_path}")
conn = sqlite3.connect(db_path)
cur = conn.cursor()
# Batches to merge: 20 to 23 on 2026-08-20
# IDs: 1544 (b20: 8), 1545 (b21: 30), 1546 (b22: 14), 1547 (b23: 72)
# Total count = 8 + 30 + 14 + 72 = 124
# Start time = 2026-08-20T19:56:13.676368 (from batch 20)
# End time = 2026-08-20T20:07:26.493341 (from batch 23)
# Step A: Update Batch #20 (id 1544) to contain merged total
cur.execute('''
UPDATE batches
SET count = 124,
start_time = '2026-08-20T19:56:13.676368',
end_time = '2026-08-20T20:07:26.493341'
WHERE id = 1544
''')
# Step B: Delete merged batches 21..23 (ids 1545, 1546, 1547)
cur.execute('''
DELETE FROM batches
WHERE id IN (1545, 1546, 1547)
''')
# Step C: Shift subsequent batches (batch_number > 23) down by 3
# (e.g. Batch 24 becomes 21, Batch 25 becomes 22, ... Batch 29 becomes 26)
cur.execute('''
SELECT id, batch_number FROM batches
WHERE counting_date = '2026-08-20' AND batch_number > 23
ORDER BY batch_number ASC
''')
shift_rows = cur.fetchall()
for bid, bnum in shift_rows:
new_bnum = bnum - 3
cur.execute("UPDATE batches SET batch_number = ? WHERE id = ?", (new_bnum, bid))
# Step D: Recalculate daily_summaries for 2026-08-20
cur.execute('''
SELECT SUM(count), COUNT(id)
FROM batches
WHERE counting_date = '2026-08-20' AND camera_name = 'CC1' AND object_label = 'karung-pakan'
''')
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 ('2026-08-20', 'CC1', 'karung-pakan', ?, ?, CURRENT_TIMESTAMP)
''', (tot_count, tot_batches))
conn.commit()
# Verify new data around batch 19..26
cur.execute("SELECT id, batch_number, count, start_time, end_time FROM batches WHERE counting_date = '2026-08-20' AND batch_number >= 19 ORDER BY batch_number ASC")
new_rows = cur.fetchall()
print("\nAfter merge 20..23:")
for r in new_rows:
print(r)
cur.execute("SELECT * FROM daily_summaries WHERE counting_date = '2026-08-20'")
print("\nDaily Summary:", cur.fetchall())
conn.close()
+77
View File
@@ -0,0 +1,77 @@
import sqlite3
import shutil
from datetime import datetime
db_path = '/opt/jetson-counter/jetson_counter.db'
# 1. Backup database
backup_path = f'/opt/jetson-counter/jetson_counter.db.bak_{datetime.now().strftime("%Y%m%d_%H%M%S")}'
shutil.copy2(db_path, backup_path)
print(f"Backup created at: {backup_path}")
conn = sqlite3.connect(db_path)
cur = conn.cursor()
# Batches to merge: 28 to 35 on 2026-08-20
# IDs: 1552 (b28: 6), 1553 (b29: 14), 1554 (b30: 19), 1555 (b31: 27),
# 1556 (b32: 44), 1557 (b33: 44), 1558 (b34: 42), 1559 (b35: 411)
# Total count = 6 + 14 + 19 + 27 + 44 + 44 + 42 + 411 = 607
# Start time = 2026-08-20T21:09:35.192219 (from batch 28)
# End time = 2026-08-20T22:00:29.667740 (from batch 35)
# Step A: Update Batch #28 (id 1552) to contain merged total
cur.execute('''
UPDATE batches
SET count = 607,
start_time = '2026-08-20T21:09:35.192219',
end_time = '2026-08-20T22:00:29.667740'
WHERE id = 1552
''')
# Step B: Delete merged batches 29..35 (ids 1553..1559)
cur.execute('''
DELETE FROM batches
WHERE id IN (1553, 1554, 1555, 1556, 1557, 1558, 1559)
''')
# Step C: Shift Batch #36 (id 1560) and any subsequent batches down by 7
# (Batch 36 becomes Batch 29)
cur.execute('''
SELECT id, batch_number FROM batches
WHERE counting_date = '2026-08-20' AND batch_number > 35
ORDER BY batch_number ASC
''')
shift_rows = cur.fetchall()
for bid, bnum in shift_rows:
new_bnum = bnum - 7
cur.execute("UPDATE batches SET batch_number = ? WHERE id = ?", (new_bnum, bid))
# Step D: Recalculate daily_summaries for 2026-08-20
cur.execute('''
SELECT SUM(count), COUNT(id)
FROM batches
WHERE counting_date = '2026-08-20' AND camera_name = 'CC1' AND object_label = 'karung-pakan'
''')
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 ('2026-08-20', 'CC1', 'karung-pakan', ?, ?, CURRENT_TIMESTAMP)
''', (tot_count, tot_batches))
conn.commit()
# Verify new data around batch 27..30
cur.execute("SELECT id, batch_number, count, start_time, end_time FROM batches WHERE counting_date = '2026-08-20' AND batch_number >= 25 ORDER BY batch_number ASC")
new_rows = cur.fetchall()
print("\nAfter merge:")
for r in new_rows:
print(r)
cur.execute("SELECT * FROM daily_summaries WHERE counting_date = '2026-08-20'")
print("\nDaily Summary:", cur.fetchall())
conn.close()
+209
View File
@@ -0,0 +1,209 @@
import paramiko
import base64
def run():
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect('192.168.192.96', username='jetson', password='jetson', timeout=10)
# 1. Backup DB first
backup_cmd = "cp /opt/jetson-counter/jetson_counter.db /opt/jetson-counter/jetson_counter.db.bak_$(date +%Y%m%d_%H%M%S)"
stdin, stdout, stderr = client.exec_command(backup_cmd)
print("Backup output:", stdout.read().decode(), stderr.read().decode())
# 2. Python migration script on Jetson
code = """
import sqlite3
import shutil
from datetime import datetime
db_path = '/opt/jetson-counter/jetson_counter.db'
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
cur = conn.cursor()
def process_date_2026_08_21():
print("=== Processing 2026-08-21 ===")
# Fetch all batches
rows = cur.execute("SELECT * FROM batches WHERE counting_date = '2026-08-21' ORDER BY batch_number ASC").fetchall()
batches = [dict(r) for r in rows]
print(f"Initial batches count: {len(batches)}")
# Rules:
# 1. Batch 15 & 16 merge -> start_time = batch 15 start_time, end_time = batch 16 end_time, count = count15 + count16
# 2. Batch 20 hapus
new_batches = []
i = 0
while i < len(batches):
b = batches[i]
b_num = b['batch_number']
if b_num == 15:
# Look for batch 16
b_next = batches[i+1] if i+1 < len(batches) and batches[i+1]['batch_number'] == 16 else None
if b_next:
merged = {
'camera_name': b['camera_name'],
'object_label': b['object_label'],
'count': b['count'] + b_next['count'],
'start_time': b['start_time'],
'end_time': b_next['end_time']
}
new_batches.append(merged)
i += 2
continue
else:
new_batches.append(b)
i += 1
continue
elif b_num == 20:
# Delete / skip
print(f"Deleting batch 20 (count: {b['count']})")
i += 1
continue
else:
new_batches.append({
'camera_name': b['camera_name'],
'object_label': b['object_label'],
'count': b['count'],
'start_time': b['start_time'],
'end_time': b['end_time']
})
i += 1
# Delete existing batches for 2026-08-21
cur.execute("DELETE FROM batches WHERE counting_date = '2026-08-21'")
# Re-insert with renumbered batch_number (1 to N)
total_count = 0
for idx, b in enumerate(new_batches, start=1):
total_count += b['count']
cur.execute(\"\"\"
INSERT INTO batches (counting_date, batch_number, camera_name, object_label, count, start_time, end_time)
VALUES ('2026-08-21', ?, ?, ?, ?, ?, ?)
\"\"\", (idx, b['camera_name'], b['object_label'], b['count'], b['start_time'], b['end_time']))
total_batches = len(new_batches)
print(f"New total batches for 2026-08-21: {total_batches}, total count: {total_count}")
# Update daily_summaries
cur.execute(\"\"\"
INSERT INTO daily_summaries (counting_date, camera_name, object_label, total_count, total_batches, updated_at)
VALUES ('2026-08-21', 'CC1', 'karung-pakan', ?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(counting_date, camera_name, object_label) DO UPDATE SET
total_count = excluded.total_count,
total_batches = excluded.total_batches,
updated_at = CURRENT_TIMESTAMP
\"\"\", (total_count, total_batches))
def process_date_2026_08_22():
print("=== Processing 2026-08-22 ===")
# Fetch all batches
rows = cur.execute("SELECT * FROM batches WHERE counting_date = '2026-08-22' ORDER BY batch_number ASC").fetchall()
batches = [dict(r) for r in rows]
print(f"Initial batches count: {len(batches)}")
# Rules:
# 1. Batch 5 & batch 6 gabungkan
# 2. Batch 27 hapus
# 3. Batch 28 & batch 29 gabungkan
new_batches = []
i = 0
while i < len(batches):
b = batches[i]
b_num = b['batch_number']
if b_num == 5:
# merge with 6
b_next = batches[i+1] if i+1 < len(batches) and batches[i+1]['batch_number'] == 6 else None
if b_next:
merged = {
'camera_name': b['camera_name'],
'object_label': b['object_label'],
'count': b['count'] + b_next['count'],
'start_time': b['start_time'],
'end_time': b_next['end_time']
}
new_batches.append(merged)
i += 2
continue
else:
new_batches.append(b)
i += 1
continue
elif b_num == 27:
# Delete / skip
print(f"Deleting batch 27 (count: {b['count']})")
i += 1
continue
elif b_num == 28:
# merge with 29
b_next = batches[i+1] if i+1 < len(batches) and batches[i+1]['batch_number'] == 29 else None
if b_next:
merged = {
'camera_name': b['camera_name'],
'object_label': b['object_label'],
'count': b['count'] + b_next['count'],
'start_time': b['start_time'],
'end_time': b_next['end_time']
}
new_batches.append(merged)
i += 2
continue
else:
new_batches.append(b)
i += 1
continue
else:
new_batches.append({
'camera_name': b['camera_name'],
'object_label': b['object_label'],
'count': b['count'],
'start_time': b['start_time'],
'end_time': b['end_time']
})
i += 1
# Delete existing batches for 2026-08-22
cur.execute("DELETE FROM batches WHERE counting_date = '2026-08-22'")
# Re-insert with renumbered batch_number (1 to N)
total_count = 0
for idx, b in enumerate(new_batches, start=1):
total_count += b['count']
cur.execute(\"\"\"
INSERT INTO batches (counting_date, batch_number, camera_name, object_label, count, start_time, end_time)
VALUES ('2026-08-22', ?, ?, ?, ?, ?, ?)
\"\"\", (idx, b['camera_name'], b['object_label'], b['count'], b['start_time'], b['end_time']))
total_batches = len(new_batches)
print(f"New total batches for 2026-08-22: {total_batches}, total count: {total_count}")
# Update daily_summaries
cur.execute(\"\"\"
INSERT INTO daily_summaries (counting_date, camera_name, object_label, total_count, total_batches, updated_at)
VALUES ('2026-08-22', 'CC1', 'karung-pakan', ?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(counting_date, camera_name, object_label) DO UPDATE SET
total_count = excluded.total_count,
total_batches = excluded.total_batches,
updated_at = CURRENT_TIMESTAMP
\"\"\", (total_count, total_batches))
process_date_2026_08_21()
process_date_2026_08_22()
conn.commit()
conn.close()
print("Migration completed successfully!")
"""
b64 = base64.b64encode(code.encode()).decode()
stdin, stdout, stderr = client.exec_command(f"python3 -c \"import base64; exec(base64.b64decode('{b64}'))\"")
print("Migration stdout:\n", stdout.read().decode())
print("Migration stderr:\n", stderr.read().decode())
client.close()
if __name__ == '__main__':
run()
+842
View File
@@ -0,0 +1,842 @@
import os
os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;tcp|threads;1|buffer_size;20480000|max_delay;500000|reorder_queue_size;500"
import cv2
import numpy as np
import json
import time
import sqlite3
import threading
from datetime import datetime, timedelta
from collections import defaultdict, deque
from shapely.geometry import Point, Polygon, box
from ultralytics import YOLO
class RTSPBufferlessCapture:
"""Bufferless Capture using cap.grab() in main thread - 100% thread-safe on Windows."""
def __init__(self, source_path):
self.source_path = source_path
self.cap = cv2.VideoCapture(source_path, cv2.CAP_FFMPEG)
if self.cap.isOpened():
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
self.width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
self.height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
self.fps = self.cap.get(cv2.CAP_PROP_FPS)
else:
self.width, self.height, self.fps = 1920, 1080, 25.0
if self.fps <= 0 or np.isnan(self.fps):
self.fps = 25.0
def isOpened(self):
return self.cap is not None and self.cap.isOpened()
def get(self, propId):
if propId == cv2.CAP_PROP_FRAME_WIDTH:
return self.width
elif propId == cv2.CAP_PROP_FRAME_HEIGHT:
return self.height
elif propId == cv2.CAP_PROP_FPS:
return self.fps
elif self.cap is not None:
return self.cap.get(propId)
return 0
def read(self):
if self.cap is None or not self.cap.isOpened():
return False, None
# Flush buffer to get latest live frame
self.cap.grab()
ret, frame = self.cap.retrieve()
if not ret or frame is None:
ret, frame = self.cap.read()
return ret, frame
def release(self):
if self.cap is not None:
self.cap.release()
self.cap = None
# =====================================================================
# SYSTEM DATABASES AND CONFIGURATION FOR LIVE DASHBOARD
# =====================================================================
if os.name == 'nt':
_DEFAULT_DIR = "d:/Belajar/menghitung karung"
DB_PATH = os.getenv('DB_PATH', f"{_DEFAULT_DIR}/jetson_counter.db")
STATE_FILE = os.getenv('STATE_FILE', f"{_DEFAULT_DIR}/current_batch.json")
LIVE_STREAM_FRAME_PATH = os.getenv('LIVE_STREAM_FRAME_PATH', f"{_DEFAULT_DIR}/live_frame.jpg")
else:
_DEFAULT_DIR = "/opt/jetson-counter"
DB_PATH = os.getenv('DB_PATH', f"{_DEFAULT_DIR}/jetson_counter.db")
STATE_FILE = os.getenv('STATE_FILE', f"{_DEFAULT_DIR}/current_batch.json")
LIVE_STREAM_FRAME_PATH = os.getenv('LIVE_STREAM_FRAME_PATH', '/dev/shm/jetson-counter/live_frame.jpg')
CAMERA_NAME = os.getenv('CAMERA_NAME', 'CC1')
OBJECT_LABEL = os.getenv('OBJECT_LABEL', 'karung-pakan')
DAILY_CUTOFF_TIME = os.getenv('DAILY_CUTOFF_TIME', '20:00')
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()
except Exception as e:
print(f"[DB Error] Gagal inisialisasi database: {e}")
def get_counting_date(dt=None, cutoff_str=DAILY_CUTOFF_TIME):
if dt is None:
dt = datetime.now()
try:
cutoff = datetime.strptime(cutoff_str, "%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() - timedelta(days=1)).isoformat()
return dt.date().isoformat()
def get_next_batch_number(date_str):
try:
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute("""
SELECT COALESCE(MAX(batch_number), 0) + 1
FROM batches
WHERE counting_date = ? AND camera_name = ? AND object_label = ?
""", (date_str, CAMERA_NAME, OBJECT_LABEL))
num = cur.fetchone()[0]
conn.close()
return num
except Exception:
return 1
def finalize_batch(final_count, start_time_str, end_time_str, batch_num, counting_date):
try:
init_db()
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
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_str, end_time_str))
cur.execute("""
SELECT COALESCE(SUM(count), 0) as tot_count, COUNT(id) as tot_batches
FROM batches
WHERE counting_date = ? AND camera_name = ? AND object_label = ?
""", (counting_date, CAMERA_NAME, OBJECT_LABEL))
row = cur.fetchone()
tot_count = row[0]
tot_batches = row[1]
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"[DB Info] Sesi batch #{batch_num} disimpan ke database SQLite: {final_count} karung.")
except Exception as e:
print(f"[DB Error] Gagal menyimpan batch ke database: {e}")
# =====================================================================
# 0. PARAMETER KONFIGURASI KALIBRASI (RANCANGAN BRAIN-STORMING)
# =====================================================================
CAMERA_NOISE_DEADBAND = 5 # Filter getaran kamera (pixel)
JARAK_TOLERANSI_DUPLIKAT = 80 # Jarak spasial maksimal untuk anti-double check (pixel)
TOLERANSI_FRAME_HILANG = 120 # Frame timeout untuk Re-ID lost track
MAX_REID_TRANSIT_DISTANCE = 400 # Jarak dasar pencarian Re-ID (pixel)
MAX_REID_FRAMES = 120 # Frame maks untuk memulihkan ID yang hilang
CONFIRM_DELAY_SEC = 0.5 # Delay debounce statis sebelum dihitung (detik)
MAX_STATIC_SPEED = 80.0 # Batas kecepatan maks untuk dikategorikan statis (px/s)
INFERENCE_STRIDE = 2 # Frame skipping (1 = proses semua, 2 = skip 1 frame)
# --- Path Model ---
TRUCK_MODEL_PATH = "truck-detector.pt"
SACK_MODEL_PATH = "karung-dimuat-detection-di-feedmill-yolo26n-seg-200e.pt"
# --- Konstanta State Machine ---
STATE_WAITING_FOR_TRUCK = "WAITING_FOR_TRUCK"
STATE_COUNTING_SACKS = "COUNTING_SACKS"
STATE_TRUCK_LEAVING = "TRUCK_LEAVING"
# =====================================================================
# 1. UTILITY AKURASI (VISUAL SIMILARITY & PERSPECTIVE PROFILE)
# =====================================================================
def get_visual_features(crop):
"""Mengekstrak fitur visual berupa histogram HSV (warna) dan grayscale image (struktur/tekstur) dari crop karung."""
if crop is None or crop.size == 0:
return None, None
try:
resized = cv2.resize(crop, (64, 64))
# 1. Color Profile: HSV Hist
hsv = cv2.cvtColor(resized, cv2.COLOR_BGR2HSV)
hist = cv2.calcHist([hsv], [0, 1], None, [16, 16], [0, 180, 0, 256])
cv2.normalize(hist, hist, 0, 1, cv2.NORM_MINMAX)
# 2. Structural Profile: Grayscale NCC
gray = cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY)
return hist, gray
except Exception as e:
return None, None
def compare_visual_similarity(feat1, feat2):
"""Membandingkan kemiripan visual karung (gabungan korelasi warna HSV 60% dan struktur grayscale NCC 40%)."""
if feat1 is None or feat2 is None:
return 0.0
hist1, gray1 = feat1
hist2, gray2 = feat2
if hist1 is None or hist2 is None or gray1 is None or gray2 is None:
return 0.0
try:
# Kemiripan Warna HSV
color_sim = cv2.compareHist(hist1, hist2, cv2.HISTCMP_CORREL)
color_sim = max(0.0, color_sim) if not np.isnan(color_sim) else 0.0
# Kemiripan Struktur Grayscale NCC
res = cv2.matchTemplate(gray1, gray2, cv2.TM_CCOEFF_NORMED)
struct_sim = max(0.0, res[0][0]) if not np.isnan(res[0][0]) else 0.0
return 0.6 * color_sim + 0.4 * struct_sim
except Exception:
return 0.0
def get_min_valid_area(cy, scale_x=1.0, scale_y=1.0):
"""Menghitung batas luas area minimum secara dinamis berdasarkan perspektif Y (Interpolasi Linier)."""
top_y = 200 * scale_y
top_area = 8000 * scale_x * scale_y
bot_y = 1080 * scale_y
bot_area = 25000 * scale_x * scale_y
if cy <= top_y:
return top_area
if cy >= bot_y:
return bot_area
ratio = (cy - top_y) / (bot_y - top_y)
return top_area + ratio * (bot_area - top_area)
# =====================================================================
# 2. SISTEM DEBOUNCE STATIS & PENYARING DUPLIKAT SPASIAL-VISUAL
# =====================================================================
class SackCounterPipeline:
def __init__(self, output_json_path="hasil_perhitungan.json"):
self.output_json_path = output_json_path
self.system_state = STATE_WAITING_FOR_TRUCK
# Area Deteksi (Poligon Shapely)
self.poly_truck = None
self.poly_palet = None # Ditentukan manual jika zones.json dimuat
# State Monitoring Truk
self.truck_initial_bbox = None
self.truck_static_frames = 0
# Tracking Karung Aktif
self.static_frames = defaultdict(int)
self.moving_frames = defaultdict(int)
self.already_counted = defaultdict(bool)
self.blocked_without_counting = defaultdict(bool)
self.track_positions = defaultdict(lambda: deque(maxlen=30))
self.track_areas = defaultdict(float)
self.track_is_valid_bag = defaultdict(bool)
self.track_visited_palet = defaultdict(bool) # --- TAMBAHAN BARU: LINE CROSSING TRACKER ---
# Registry Visual Karung Terhitung (Anti-Double Count)
self.static_sack_visuals = {} # track_id -> (hist, gray)
# Re-ID Lost Tracks
self.lost_tracks = {} # lost_id -> dict properties
# Metrik Penghitungan Batch
self.total_masuk = 0
self.total_keluar = 0
self.entry_points = {} # track_id -> (ex, ey)
# Database & Active State initialization
init_db()
self.counting_date = get_counting_date()
self.batch_number = get_next_batch_number(self.counting_date)
self.start_time = datetime.now().isoformat()
self.last_detection_time = self.start_time
self.save_active_batch_state()
def save_active_batch_state(self):
try:
os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
state_data = {
"counting_date": self.counting_date,
"batch_number": self.batch_number,
"count": self.total_masuk,
"start_time": self.start_time,
"last_detection_time": self.last_detection_time
}
with open(STATE_FILE, 'w') as f:
json.dump(state_data, f, indent=4)
except Exception:
pass
def clear_active_batch_state(self):
try:
if os.path.exists(STATE_FILE):
os.remove(STATE_FILE)
except Exception:
pass
def reset_batch(self):
"""Reset state tracking dan counter untuk memulai batch truk baru."""
self.static_frames.clear()
self.moving_frames.clear()
self.already_counted.clear()
self.blocked_without_counting.clear()
self.track_positions.clear()
self.track_areas.clear()
self.track_is_valid_bag.clear()
self.track_visited_palet.clear()
self.static_sack_visuals.clear()
self.lost_tracks.clear()
self.entry_points.clear()
self.total_masuk = 0
self.total_keluar = 0
self.counting_date = get_counting_date()
self.batch_number = get_next_batch_number(self.counting_date)
self.start_time = datetime.now().isoformat()
self.last_detection_time = self.start_time
self.save_active_batch_state()
def save_batch_report(self):
"""Menulis file laporan batch JSON ketika truk meninggalkan area."""
timestamp_str = time.strftime("%Y%m%d_%H%M%S")
batch_folder = "batch_history_folder"
os.makedirs(batch_folder, exist_ok=True)
batch_file = os.path.join(batch_folder, f"batch_{timestamp_str}.json")
report_data = {
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
"total_masuk_truck": self.total_masuk,
"total_keluar_truck": self.total_keluar,
"net_karung_di_truck": self.total_masuk - self.total_keluar
}
try:
with open(batch_file, 'w') as f:
json.dump(report_data, f, indent=4)
print(f"\n[REPORT] Laporan Batch disimpan ke: {batch_file}")
# Update juga file output kumulatif
with open(self.output_json_path, 'w') as f:
json.dump(report_data, f, indent=4)
# Simpan ke SQLite database dan bersihkan berkas state aktif
finalize_batch(self.total_masuk, self.start_time, datetime.now().isoformat(), self.batch_number, self.counting_date)
self.clear_active_batch_state()
except Exception as e:
print(f"[ERROR] Gagal menyimpan laporan batch: {e}")
# =====================================================================
# 3. PIPELINE PREDIKSI UTAMA (DUO-MODEL PIPELINE)
# =====================================================================
def run_prediction(source_path, max_frames=None, save_output_video=True, show_live=True):
print("=" * 60)
print("AI SACK COUNTER PIPELINE - DIKEMBANGKAN DARI AWAL (BRAIN-STORMING)")
print("=" * 60)
# 1. Load Model
print("[INFO] Model Truk dinonaktifkan (area truk di-hardcode)...")
model_truck = None
print(f"[INFO] Memuat Model Karung: {SACK_MODEL_PATH}...")
model_sack = YOLO(SACK_MODEL_PATH)
# Deteksi otomatis ID kelas karung dan pekerja
global sack_class_id, person_class_id
sack_class_id = 1
person_class_id = 0
if hasattr(model_sack, 'names') and model_sack.names:
for cid, name in model_sack.names.items():
name_str = str(name).lower()
if any(w in name_str for w in ['karung', 'cuval', 'sack', 'bag']):
sack_class_id = int(cid)
elif any(w in name_str for w in ['person', 'human', 'pekerja', 'manusia']):
person_class_id = int(cid)
print(f"[INFO] Auto-detected Kelas: Karung ID = {sack_class_id}, Pekerja ID = {person_class_id}")
# 2. Buka Video Input (Threaded untuk RTSP stream, direct untuk file lokal)
is_stream = any(str(source_path).startswith(p) for p in ["rtsp://", "rtmp://", "http://", "https://"])
if is_stream:
print(f"[INFO] Membuka RTSP Stream menggunakan RTSPBufferlessCapture: {source_path}")
cap = RTSPBufferlessCapture(source_path)
else:
print(f"[INFO] Membuka file video lokal: {source_path}")
cap = cv2.VideoCapture(source_path)
if not cap.isOpened():
print(f"[ERROR] Gagal membuka video source: {source_path}")
return
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv2.CAP_PROP_FPS)
if fps <= 0 or np.isnan(fps):
fps = 25.0
# Scale faktor terhadap resolusi dasar 1920x1080
scale_x = width / 1920.0
scale_y = height / 1080.0
# Setup Video Writer (jika diaktifkan)
writer = None
if save_output_video:
output_name = "annotated_output.mp4"
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
writer = cv2.VideoWriter(output_name, fourcc, fps, (width, height))
print(f"[INFO] Output video akan disimpan ke: {output_name}")
# Inisialisasi Pipeline State
pipeline = SackCounterPipeline()
# Mulai langsung di mode penghitungan (tidak perlu mendeteksi truk)
pipeline.system_state = STATE_COUNTING_SACKS
# Set default area palet dari pengguna (menggunakan koordinat referensi 1920x1080)
default_palet_pts = np.array([[514, 437], [1112, 439], [1112, 818], [500, 817]], dtype=np.int32)
scaled_palet_pts = np.array([[int(p[0] * scale_x), int(p[1] * scale_y)] for p in default_palet_pts], dtype=np.int32)
pipeline.poly_palet = Polygon(scaled_palet_pts)
print(f"[INFO] Poligon Zona Palet berhasil diinisialisasi: {scaled_palet_pts.tolist()}")
# Set default area truk dari pengguna (menggunakan koordinat referensi 1920x1080)
default_truck_pts = np.array([[566, 1], [547, 496], [1090, 502], [1072, 5]], dtype=np.int32)
scaled_truck_pts = np.array([[int(p[0] * scale_x), int(p[1] * scale_y)] for p in default_truck_pts], dtype=np.int32)
pipeline.poly_truck = Polygon(scaled_truck_pts)
print(f"[INFO] Poligon Zona Truk (Hardcoded) berhasil diinisialisasi: {scaled_truck_pts.tolist()}")
# Muat zones.json default jika ada untuk override
if os.path.exists("zones.json"):
try:
with open("zones.json", 'r') as f:
data = json.load(f)
if 'palet' in data and len(data['palet']) >= 3:
pts = np.array([[int(p[0] * scale_x), int(p[1] * scale_y)] for p in data['palet']], dtype=np.int32)
pipeline.poly_palet = Polygon(pts)
print("[INFO] Poligon Zona Palet berhasil dimuat dari zones.json (override)")
if 'truck' in data and len(data['truck']) >= 3:
pts = np.array([[int(p[0] * scale_x), int(p[1] * scale_y)] for p in data['truck']], dtype=np.int32)
pipeline.poly_truck = Polygon(pts)
print("[INFO] Poligon Zona Truk berhasil dimuat dari zones.json (override)")
except Exception as e:
print(f"[WARNING] Gagal memuat zones.json: {e}")
frame_idx = 0
last_time = time.time()
current_fps = 0.0
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
frame_idx += 1
if max_frames is not None and frame_idx > max_frames:
break
# Hitung durasi interval frame aktual untuk kompensasi FPS rendah
dt = (INFERENCE_STRIDE / fps) if fps > 0 else 0.04
required_frames = max(1, int(CONFIRM_DELAY_SEC * fps))
required_static_updates = max(1, int(required_frames / INFERENCE_STRIDE))
# Bbox list untuk HUD visualizer
visual_bboxes = []
# =====================================================================
# STATE MACHINE LOGIC
# =====================================================================
# STATE 1: WAITING_FOR_TRUCK
if pipeline.system_state == STATE_WAITING_FOR_TRUCK:
if model_truck is None:
pipeline.system_state = STATE_COUNTING_SACKS
continue
res_truck = model_truck(frame, conf=0.5, verbose=False)
best_box = None
best_conf = -1.0
if res_truck[0].boxes is not None and len(res_truck[0].boxes) > 0:
for box_obj in res_truck[0].boxes:
conf = float(box_obj.conf[0].cpu().item())
if conf > best_conf:
best_conf = conf
best_box = box_obj.xyxy[0].cpu().numpy()
if best_box is not None:
x1_t, y1_t, x2_t, y2_t = best_box
cx_t = int((x1_t + x2_t) / 2)
cy_t = int((y1_t + y2_t) / 2)
# Cek stabilitas posisi truk
if pipeline.truck_initial_bbox is None:
pipeline.truck_initial_bbox = best_box
pipeline.truck_static_frames = 0
else:
cx_old = int((pipeline.truck_initial_bbox[0] + pipeline.truck_initial_bbox[2]) / 2)
cy_old = int((pipeline.truck_initial_bbox[1] + pipeline.truck_initial_bbox[3]) / 2)
disp = np.sqrt((cx_t - cx_old)**2 + (cy_t - cy_old)**2)
if disp < CAMERA_NOISE_DEADBAND:
pipeline.truck_static_frames += 1
else:
pipeline.truck_initial_bbox = best_box
pipeline.truck_static_frames = 0
# Truk dianggap berhenti jika stabil selama 45 frame (~1.5s)
if pipeline.truck_static_frames >= 45:
# Kunci area truk dengan margin aman 5% ke dalam bak
w_t = x2_t - x1_t
h_t = y2_t - y1_t
x1_t += w_t * 0.05
x2_t -= w_t * 0.05
y1_t += h_t * 0.05
y2_t -= h_t * 0.05
pts_truck = np.array([[x1_t, y1_t], [x2_t, y1_t], [x2_t, y2_t], [x1_t, y2_t]], dtype=np.int32)
pipeline.poly_truck = Polygon(pts_truck)
# Reset data untuk batch baru
pipeline.reset_batch()
pipeline.system_state = STATE_COUNTING_SACKS
print(f"\n[STATE] Truk diam terkunci di koordinat: {best_box}. Mulai menghitung karung...")
# Append box truk ke visualizer
visual_bboxes.append({
"bbox": [int(x1_t), int(y1_t), int(x2_t), int(y2_t)],
"label": f"MONITORING TRUK: {pipeline.truck_static_frames}/45",
"color": (0, 204, 255),
"thick": 3
})
else:
pipeline.truck_initial_bbox = None
pipeline.truck_static_frames = 0
# STATE 3: TRUCK_LEAVING
elif pipeline.system_state == STATE_TRUCK_LEAVING:
pipeline.save_batch_report()
pipeline.poly_truck = None
pipeline.truck_initial_bbox = None
pipeline.truck_static_frames = 0
pipeline.system_state = STATE_WAITING_FOR_TRUCK
# STATE 2: COUNTING_SACKS
elif pipeline.system_state == STATE_COUNTING_SACKS:
# Pengecekan keberadaan truk dinonaktifkan (area truk di-hardcode)
pass
# Jalankan Tracker Karung dan Pekerja (Inference Stride)
if INFERENCE_STRIDE <= 1 or frame_idx % INFERENCE_STRIDE == 0 or 'last_results' not in locals():
results_sack = model_sack.track(frame, persist=True, tracker="bytetrack.yaml", conf=0.05, classes=[person_class_id, sack_class_id], verbose=False)
last_results = results_sack
else:
results_sack = last_results
current_active_ids = set()
if results_sack[0].boxes.id is not None:
boxes = results_sack[0].boxes.xyxy.cpu().numpy()
track_ids = results_sack[0].boxes.id.int().cpu().numpy()
classes_ids = results_sack[0].boxes.cls.int().cpu().numpy()
for box_coord, track_id, cls_id in zip(boxes, track_ids, classes_ids):
# Jika terdeteksi sebagai pekerja/manusia, gambarkan bbox merah dan lewati logika hitung
if cls_id == person_class_id:
if save_output_video:
x1, y1, x2, y2 = box_coord
cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 0, 255), 2)
cv2.putText(frame, f"PEKERJA #{track_id}", (int(x1), int(y1) - 8),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2)
continue
x1, y1, x2, y2 = box_coord
cx = int((x1 + x2) / 2)
cy = int((y1 + y2) / 2)
box_area = (x2 - x1) * (y2 - y1)
pt = Point(cx, cy)
# 1. Filter Perspektif Adaptif (Perspective Profile)
min_area_thresh = get_min_valid_area(cy, scale_x, scale_y)
is_fragment = (box_area < min_area_thresh) and not pipeline.track_is_valid_bag[track_id]
if is_fragment:
# Abaikan objek kecil/sampah yang terdeteksi
continue
else:
pipeline.track_is_valid_bag[track_id] = True
current_active_ids.add(track_id)
pipeline.track_positions[track_id].append((cx, cy))
pipeline.track_areas[track_id] = box_area
# 2. Cek Re-ID Lost Tracks (Dynamic Search Window)
if len(pipeline.track_positions[track_id]) == 1:
# Jika baru muncul, coba pulihkan dari registry lost track
closest_old_id = None
min_d = float('inf')
for old_id, info in pipeline.lost_tracks.items():
frame_diff = frame_idx - info['frame_idx']
if frame_diff > MAX_REID_FRAMES:
continue
lc = info['last_centroid']
dist_reid = np.sqrt((cx - lc[0])**2 + (cy - lc[1])**2)
# Jendela pencarian melebar seiring pertambahan frame drop (Kompensasi Lag FPS)
dynamic_search_radius = MAX_REID_TRANSIT_DISTANCE * (1.0 + 0.01 * frame_diff)
if dist_reid < dynamic_search_radius:
if dist_reid < min_d:
min_d = dist_reid
closest_old_id = old_id
if closest_old_id is not None:
# Pulihkan state data track lama
old_info = pipeline.lost_tracks[closest_old_id]
pipeline.already_counted[track_id] = old_info['already_counted']
pipeline.blocked_without_counting[track_id] = old_info['blocked_without_counting']
pipeline.static_frames[track_id] = old_info['static_frames']
pipeline.track_visited_palet[track_id] = old_info.get('visited_palet', False)
if old_info['already_counted'] and closest_old_id in pipeline.static_sack_visuals:
pipeline.static_sack_visuals[track_id] = pipeline.static_sack_visuals[closest_old_id]
del pipeline.lost_tracks[closest_old_id]
print(f"[RE-ID] Tracker #{track_id} berhasil dipulihkan dari ID lama #{closest_old_id}")
# 3. Hitung Vektor Kecepatan & Debounce Statis (Velocity Filtering)
speed = 0.0
if len(pipeline.track_positions[track_id]) > 1:
prev_cx, prev_cy = pipeline.track_positions[track_id][-2]
disp = np.sqrt((cx - prev_cx)**2 + (cy - prev_cy)**2)
# Filter getaran kamera (Noise Deadband)
if disp < CAMERA_NOISE_DEADBAND:
disp = 0.0
if dt > 0:
speed = disp / dt
# Update status gerak
if speed < MAX_STATIC_SPEED:
pipeline.static_frames[track_id] += 1
pipeline.moving_frames[track_id] = 0
else:
pipeline.static_frames[track_id] = 0
pipeline.moving_frames[track_id] += 1
# Deteksi zona aktual centroid
in_truck_polygon = pipeline.poly_truck is not None and pipeline.poly_truck.contains(pt)
in_palet_polygon = pipeline.poly_palet is not None and pipeline.poly_palet.contains(pt)
# Logika Perhitungan Sederhana: Bergerak > 50px dari Titik Masuk Area Truk
if in_truck_polygon:
if track_id not in pipeline.entry_points:
pipeline.entry_points[track_id] = (cx, cy)
pipeline.already_counted[track_id] = False
if track_id in pipeline.entry_points:
if not pipeline.already_counted[track_id]:
ex, ey = pipeline.entry_points[track_id]
dist_from_entry = np.sqrt((cx - ex)**2 + (cy - ey)**2)
if dist_from_entry > 50:
pipeline.total_masuk += 1
pipeline.already_counted[track_id] = True
pipeline.last_detection_time = datetime.now().isoformat()
pipeline.save_active_batch_state()
print(f"[COUNTER] Karung #{track_id} terhitung masuk! (Jarak gerak: {dist_from_entry:.1f}px > 50px). Total: {pipeline.total_masuk}")
# 5. Penentuan Kategori Label Visual HUD
if pipeline.blocked_without_counting[track_id]:
color = (128, 128, 128) # Abu-abu
label = f"DUPLIKAT #{track_id}"
elif pipeline.already_counted[track_id]:
color = (0, 255, 0) # Hijau terang
label = f"VERIFIED #{track_id}"
elif in_truck_polygon:
if speed >= MAX_STATIC_SPEED:
color = (0, 255, 255) # Kuning
label = f"TRANSIT #{track_id} ({speed:.0f}px/s)"
else:
color = (0, 165, 255) # Oranye
label = f"NEW_STATIC #{track_id} ({pipeline.static_frames[track_id]}/{required_static_updates})"
elif in_palet_polygon:
color = (255, 255, 0) # Cyan
label = f"PALET #{track_id}"
else:
color = (255, 0, 255) # Magenta
label = f"SACK #{track_id}"
# Tampilkan bounding box, titik tengah, dan label di frame
if True:
cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), color, 2)
cv2.putText(frame, label, (int(x1), int(y1) - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
# 1. Gambar Point (Titik Tengah BBox)
cv2.circle(frame, (cx, cy), 5, (0, 255, 255), -1)
# 2. Menggambar titik acuan masuk, radius 50px, dan indikator perpindahan
if track_id in pipeline.entry_points:
ex, ey = pipeline.entry_points[track_id]
is_counted = pipeline.already_counted[track_id]
# Warna: Hijau jika terhitung (>50px), Oranye jika masih di dalam radius 50px
viz_color = (0, 255, 0) if is_counted else (0, 140, 255)
# Gambar Titik Acuan Awal saat Masuk Area Truk
cv2.circle(frame, (ex, ey), 4, viz_color, -1)
# Gambar Lingkaran Radius 50px
cv2.circle(frame, (ex, ey), 50, viz_color, 2, lineType=cv2.LINE_AA)
# Gambar garis hubung dari titik awal ke titik bbox saat ini
cv2.line(frame, (ex, ey), (cx, cy), viz_color, 1)
# Tampilkan label status jarak
dist_val = np.sqrt((cx - ex)**2 + (cy - ey)**2)
dist_label = f"COUNTED (+1)" if is_counted else f"{dist_val:.0f}/50px"
cv2.putText(frame, dist_label, (ex - 20, ey - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.45, viz_color, 2)
# Daftarkan track yang hilang pada frame ini ke registry Re-ID
for old_id in list(pipeline.track_positions.keys()):
if old_id not in current_active_ids:
# Masukkan ke lost tracks
if len(pipeline.track_positions[old_id]) > 0:
pipeline.lost_tracks[old_id] = {
"frame_idx": frame_idx,
"last_centroid": pipeline.track_positions[old_id][-1],
"already_counted": pipeline.already_counted[old_id],
"blocked_without_counting": pipeline.blocked_without_counting[old_id],
"static_frames": pipeline.static_frames[old_id],
"visited_palet": pipeline.track_visited_palet[old_id],
"positions": pipeline.track_positions[old_id].copy()
}
# Bersihkan dari tracker aktif
pipeline.track_positions.pop(old_id, None)
pipeline.static_frames.pop(old_id, None)
pipeline.moving_frames.pop(old_id, None)
pipeline.track_visited_palet.pop(old_id, None)
# =====================================================================
# RENDER PREMIUM HUD OVERLAY (BURNT INTO FRAME)
# =====================================================================
if True:
# 1. Gambar Batas Zona
if pipeline.poly_palet is not None:
pts = np.array(pipeline.poly_palet.exterior.coords, dtype=np.int32)
cv2.polylines(frame, [pts], True, (255, 255, 0), 2)
cv2.putText(frame, "ZONA PALET", (pts[0][0], pts[0][1] - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 0), 2)
if pipeline.poly_truck is not None:
pts = np.array(pipeline.poly_truck.exterior.coords, dtype=np.int32)
cv2.polylines(frame, [pts], True, (0, 204, 255), 2)
cv2.putText(frame, "ZONA TRUK BATCH", (pts[0][0], pts[0][1] - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 204, 255), 2)
# 2. Gambar Background HUD Panel (Top-Left)
# HUD Glassmorphic Rectangle
overlay = frame.copy()
cv2.rectangle(overlay, (20, 20), (450, 180), (15, 17, 24), -1)
cv2.addWeighted(overlay, 0.75, frame, 0.25, 0, frame)
cv2.rectangle(frame, (20, 20), (450, 180), (255, 255, 255), 1, lineType=cv2.LINE_AA)
# Text HUD info
cv2.putText(frame, "AI SACK COUNTER PIPELINE v2.0", (35, 45), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 240, 255), 2)
cv2.line(frame, (35, 55), (435, 55), (100, 100, 100), 1)
# State System
state_color = (0, 255, 0) if pipeline.system_state == STATE_COUNTING_SACKS else (0, 204, 255)
cv2.putText(frame, f"STATUS: {pipeline.system_state}", (35, 80), cv2.FONT_HERSHEY_SIMPLEX, 0.5, state_color, 2)
# Metrics
cv2.putText(frame, f"TOTAL MASUK : {pipeline.total_masuk}", (35, 115), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
cv2.putText(frame, f"TOTAL KELUAR : {pipeline.total_keluar}", (35, 145), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
# FPS & Frame counter
if frame_idx % 25 == 0:
elapsed = time.time() - last_time
current_fps = 25.0 / elapsed if elapsed > 0 else 0.0
last_time = time.time()
cv2.putText(frame, f"FPS: {current_fps:.1f} | Frame: {frame_idx}", (35, 168), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (200, 200, 200), 1)
# Write annotated frame to output video file if enabled
if save_output_video and writer is not None:
writer.write(frame)
# Write live frame to shared memory RAM disk for dashboard streaming (every 2 frames)
if frame_idx % 2 == 0:
try:
live_path = LIVE_STREAM_FRAME_PATH
os.makedirs(os.path.dirname(live_path), exist_ok=True)
tmp_path = live_path.replace(".jpg", ".tmp.jpg")
cv2.imwrite(tmp_path, frame, [cv2.IMWRITE_JPEG_QUALITY, 80])
os.replace(tmp_path, live_path)
except Exception:
pass
# Tampilkan Live Preview jika show_live aktif
if show_live:
display_frame = cv2.resize(frame, (1280, 720)) if (width > 1280 or height > 720) else frame
cv2.imshow("AI Sack Counter - Live Preview", display_frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
print("\n[INFO] Live preview dihentikan oleh pengguna (menekan tombol 'q').")
break
# Log status periodic ke konsol
if frame_idx % 25 == 0:
print(f"[INFO] Frame {frame_idx} - State: {pipeline.system_state} - Masuk: {pipeline.total_masuk} - Keluar: {pipeline.total_keluar} ({current_fps:.1f} FPS)")
# Clean resources
cap.release()
if writer is not None:
writer.release()
cv2.destroyAllWindows()
# Save final batch report
pipeline.save_batch_report()
print("\n" + "=" * 60)
print("PROSES PIPELINE SELESAI!")
print(f"Hasil Akhir Batch: Masuk = {pipeline.total_masuk}, Keluar = {pipeline.total_keluar}")
print("=" * 60)
if __name__ == "__main__":
# RTSP Camera Live Stream
SOURCE_INPUT = "rtsp://192.168.192.96:8554/cam"
is_stream = any(str(SOURCE_INPUT).startswith(p) for p in ["http://", "https://", "rtsp://", "rtmp://"])
if is_stream or os.path.exists(SOURCE_INPUT):
try:
run_prediction(
source_path=SOURCE_INPUT,
max_frames=None, # Proses seluruh video
save_output_video=True,
show_live=True # Aktifkan window GUI OpenCV untuk live preview langsung
)
except KeyboardInterrupt:
print("\n[INFO] Program dihentikan secara manual (Ctrl+C).")
else:
print(f"[ERROR] Video/Stream '{SOURCE_INPUT}' tidak ditemukan.")
+12
View File
@@ -0,0 +1,12 @@
{
"video_width": 1920,
"video_height": 1080,
"orientation": "horizontal",
"direction": "bottom_to_top",
"zone_x_min": 858,
"zone_x_max": 1231,
"zone_y_min": 7,
"zone_y_max": 581,
"line_y": 580,
"line_x": null
}
@@ -0,0 +1,15 @@
{
"video_width": 1920,
"video_height": 1080,
"orientation": "horizontal",
"direction": "bottom_to_top",
"zone_x_min": 598,
"zone_x_max": 1919,
"zone_y_min": 215,
"zone_y_max": 1079,
"line_y": 993,
"line_x": null,
"auto_calibrate_mode": "sack_cluster",
"source_video": "Camera2_segments\\clip_033.mp4",
"model": "karung-dimuat-seg-200e.pt"
}
@@ -0,0 +1,12 @@
{
"video_width": 1920,
"video_height": 1080,
"orientation": "horizontal",
"direction": "bottom_to_top",
"zone_x_min": 858,
"zone_x_max": 1231,
"zone_y_min": 7,
"zone_y_max": 581,
"line_y": 580,
"line_x": null
}
@@ -0,0 +1,11 @@
{
"line_y": 590,
"zone_x_min": 747,
"zone_x_max": 1314,
"zone_y_min": 0,
"zone_y_max": 595,
"video_width": 1920,
"video_height": 1080,
"orientation": "horizontal",
"direction": "bottom_to_top"
}
+14
View File
@@ -0,0 +1,14 @@
{
"cameras": [
{
"id": "cam1",
"name": "Kamera Utama (Gate 1)",
"rtsp_url": "rtsp://admin:K0l0r4n123@10.38.250.21/cam/realmonitor?channel=1&subtype=1"
},
{
"id": "cam2",
"name": "Kamera Samping (Gate 2)",
"rtsp_url": "rtsp://admin:K0l0r4n123@10.38.250.22/cam/realmonitor?channel=1&subtype=1"
}
]
}
@@ -0,0 +1,21 @@
{
"same_sack_radius": 78.0,
"stack_sack_radius": 45.0,
"min_approach_depth": 10.0,
"outside_confirm_frames": 2,
"crossing_point_ratio": 0.82,
"count_cooldown_dist": 95.0,
"count_cooldown_frames": 40,
"staging_cooldown_frames": 8,
"min_staging_depth": 45.0,
"min_track_frames": 0,
"ghost_track_frames": 999,
"conf": 0.2,
"tuned_accuracy": 71.8,
"tuned_exact": "4/6",
"tuned_total_ai": 25,
"tuned_total_manual": 25,
"tuned_mae": 0.333,
"counting_logic": "geometric_v15",
"updated_at": "2026-07-08T23:29:00"
}
@@ -0,0 +1,21 @@
{
"same_sack_radius": 78.0,
"stack_sack_radius": 45.0,
"min_approach_depth": 4.0,
"outside_confirm_frames": 2,
"crossing_point_ratio": 0.82,
"count_cooldown_dist": 95.0,
"count_cooldown_frames": 40,
"staging_cooldown_frames": 8,
"min_staging_depth": 45.0,
"min_track_frames": 8,
"ghost_track_frames": 6,
"clip_warmup_frames": 25,
"min_post_cross_inside_depth": 0.0,
"burst_cooldown_frames": 10,
"burst_cooldown_dist": 50.0,
"conf": 0.2,
"counting_logic": "geometric_v15",
"model": "karung-dimuat-seg-200e.pt",
"note": "Override klip last_truck — burst dedup + conf gelap"
}
@@ -0,0 +1,17 @@
{
"active_model_id": "karung-dimuat-seg-200e",
"models": {
"karung-dimuat-seg-200e": {
"filename": "karung-dimuat-seg-200e.pt",
"version": "1.0.0",
"released_at": "2026-07-09T08:00:00Z",
"download_url": "https://github.com/rrabbanifasha-alt/feedmill-semarang/releases/download/v1.0.0/karung-dimuat-seg-200e.pt",
"md5": "d41d8cd98f00b204e9800998ecf8427e",
"description": "Baseline model trained for 200 epochs on feedmill sacks dataset",
"metrics": {
"mAP50_mask": 0.899,
"validation_mae": 0.67
}
}
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"enabled": true,
"token": "8654129536:AAHrx4x7OPm84WRDhRLj3oIMgecUDKSfqgs",
"chat_id": "-5147118224"
}
File diff suppressed because it is too large. Load diff
+625
View File
@@ -0,0 +1,625 @@
import os
os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;tcp|buffer_size;20480000|max_delay;500000|reorder_queue_size;500"
import cv2
import numpy as np
import json
import time
import sqlite3
import threading
from pathlib import Path
from dataclasses import replace
from datetime import datetime
from ultralytics import YOLO
# Import kustom dari repositori rpo iki (Count Engine buatan teman)
from count import (
LineCounter, BoundarySettings, SACK_CLASS_ID, DEFAULT_MASK_ALPHA,
annotate_tracks, draw_persisted_sacks, draw_count_hud,
draw_truck_counter_box, draw_count_flashes, draw_boundary,
draw_blind_truck_overlay, tick_flashes, track_points, tracking_point,
CountFlash, load_counting_params
)
# =====================================================================
# PATH DATABASES & CONFIGURATION FOR DASHBOARD (PORT 5000 & 8000)
# =====================================================================
if os.name == 'nt':
_DEFAULT_DIR = "d:/Belajar/menghitung karung"
else:
_DEFAULT_DIR = "/opt/jetson-counter"
DB_PATH = os.getenv('DB_PATH', f"{_DEFAULT_DIR}/jetson_counter.db")
STATE_FILE = os.getenv('STATE_FILE', f"{_DEFAULT_DIR}/current_batch.json")
LIVE_STREAM_FRAME_PATH = os.getenv('LIVE_STREAM_FRAME_PATH', f"{_DEFAULT_DIR}/live_frame.jpg")
SHM_LIVE_FRAME_PATH = "/dev/shm/jetson-counter/live_frame.jpg"
CAMERA_NAME = "CC1"
OBJECT_LABEL = "Karung Feedmill (RPO IKI Engine)"
class RTSPBufferlessCapture:
"""Thread-safe RTSP Reader untuk Jetson / Windows."""
def __init__(self, source_path):
self.source_path = source_path
self.lock = threading.Lock()
self.cap = cv2.VideoCapture(source_path, cv2.CAP_FFMPEG)
self.frame = None
self.ret = False
self.running = True
if self.cap.isOpened():
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
self.width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
self.height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
self.fps = self.cap.get(cv2.CAP_PROP_FPS)
else:
self.width, self.height, self.fps = 1920, 1080, 25.0
if self.fps <= 0 or np.isnan(self.fps):
self.fps = 25.0
self.thread = threading.Thread(target=self._update, daemon=True)
self.thread.start()
def _update(self):
while self.running:
if self.cap is None or not self.cap.isOpened():
time.sleep(0.05)
continue
ret, frame = self.cap.read()
if ret and frame is not None:
with self.lock:
self.frame = frame
self.ret = True
else:
time.sleep(0.005)
def isOpened(self):
return self.cap is not None and self.cap.isOpened()
def get(self, propId):
if propId == cv2.CAP_PROP_FRAME_WIDTH:
return self.width
elif propId == cv2.CAP_PROP_FRAME_HEIGHT:
return self.height
elif propId == cv2.CAP_PROP_FPS:
return self.fps
return 0
def read(self):
with self.lock:
if self.ret and self.frame is not None:
return True, self.frame.copy()
return False, None
def release(self):
self.running = False
if self.cap is not None:
self.cap.release()
self.cap = None
def init_db():
try:
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS batches (
id INTEGER PRIMARY KEY AUTOINCREMENT,
batch_number TEXT UNIQUE,
start_time TEXT,
end_time TEXT,
total_masuk INTEGER,
total_keluar INTEGER,
net_count INTEGER,
status TEXT
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS daily_summaries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT UNIQUE,
camera_name TEXT,
object_label TEXT,
total_count INTEGER,
total_batches INTEGER,
updated_at TEXT
)
""")
conn.commit()
conn.close()
print(f"[DB Info] Inisialisasi SQLite database berhasil: {DB_PATH}")
except Exception as e:
print(f"[DB Error] Gagal inisialisasi SQLite database: {e}")
from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import ThreadingMixIn
streaming_frame = None
streaming_lock = threading.Lock()
live_stream_enabled = True
class StreamingHandler(BaseHTTPRequestHandler):
def log_message(self, format, *args):
pass
def do_GET(self):
global streaming_frame
if self.path == '/' or self.path == '/stream.mjpg' or self.path == '/stream':
self.send_response(200)
self.send_header('Age', '0')
self.send_header('Cache-Control', 'no-cache, private')
self.send_header('Pragma', 'no-cache')
self.send_header('Content-Type', 'multipart/x-mixed-replace; boundary=frame')
self.end_headers()
try:
while True:
with streaming_lock:
frame_to_stream = streaming_frame.copy() if streaming_frame is not None else None
if frame_to_stream is None:
time.sleep(0.05)
continue
h, w = frame_to_stream.shape[:2]
if w > 960:
frame_to_stream = cv2.resize(frame_to_stream, (960, int(h * 960 / w)))
ret, jpeg = cv2.imencode('.jpg', frame_to_stream, [cv2.IMWRITE_JPEG_QUALITY, 75])
if not ret:
time.sleep(0.05)
continue
frame_bytes = jpeg.tobytes()
self.wfile.write(b'--frame\r\n')
self.send_header('Content-Type', 'image/jpeg')
self.send_header('Content-Length', len(frame_bytes))
self.end_headers()
self.wfile.write(frame_bytes)
self.wfile.write(b'\r\n')
time.sleep(0.04) # ~25 FPS
except Exception:
pass
else:
self.send_error(404, "Path not found")
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
allow_reuse_address = True
daemon_threads = True
def start_streaming_server(port=8000):
try:
server = ThreadedHTTPServer(('0.0.0.0', port), StreamingHandler)
server_thread = threading.Thread(target=server.serve_forever, daemon=True)
server_thread.start()
print(f"[RPO IKI] Live View Server HTTP berjalan di http://0.0.0.0:{port}/")
except Exception as e:
print(f"[WARNING] Gagal membuka HTTP Streaming Server di port {port}: {e}")
def write_live_frame(frame):
"""Simpan frame preview live ke memori & disk untuk Web Server Port 8000."""
global streaming_frame
with streaming_lock:
streaming_frame = frame
try:
if os.path.exists("/dev/shm"):
os.makedirs("/dev/shm/jetson-counter", exist_ok=True)
cv2.imwrite(SHM_LIVE_FRAME_PATH, frame, [cv2.IMWRITE_JPEG_QUALITY, 80])
os.makedirs(os.path.dirname(LIVE_STREAM_FRAME_PATH), exist_ok=True)
cv2.imwrite(LIVE_STREAM_FRAME_PATH, frame, [cv2.IMWRITE_JPEG_QUALITY, 80])
except Exception:
pass
def save_active_batch_state(count=0, status="COUNTING_SACKS", truck_state="LOCKED"):
try:
os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
state_data = {
"batch_number": "BATCH-RPO-01",
"start_time": datetime.now().isoformat(),
"count": count,
"status": status,
"truck_state": truck_state,
"last_detection_time": datetime.now().isoformat(),
"engine": "rpo_iki"
}
with open(STATE_FILE, 'w') as f:
json.dump(state_data, f, indent=2)
except Exception:
pass
def resolve_live_boundary(width=960, height=540):
"""Muat koordinat zona dari zones.json (Web Dashboard) atau configs/area_truk.json."""
# 1. Cek zones.json dari Web Dashboard
zones_json = Path(_DEFAULT_DIR) / "zones.json"
if not zones_json.exists():
zones_json = Path(__file__).parent.parent / "zones.json"
if zones_json.exists():
try:
with open(zones_json, 'r') as f:
zdata = json.load(f)
if 'truck' in zdata and len(zdata['truck']) >= 3:
pts = np.array(zdata['truck'], dtype=np.float32)
scale_x = width / 1920.0
scale_y = height / 1080.0
x_min = int(np.min(pts[:, 0]) * scale_x)
x_max = int(np.max(pts[:, 0]) * scale_x)
y_min = int(np.min(pts[:, 1]) * scale_y)
y_max = int(np.max(pts[:, 1]) * scale_y)
line_y = int(y_max - 5)
print(f"[RPO IKI] Memuat Zona Dinamis dari Web (zones.json): x={x_min}..{x_max}, y={y_min}..{y_max}, line_y={line_y}")
return BoundarySettings(
line_pos=line_y,
orientation="horizontal",
direction="bottom_to_top",
zone_x_min=x_min,
zone_x_max=x_max,
zone_y_min=y_min,
zone_y_max=y_max
)
except Exception as e:
print(f"[WARNING] Gagal membaca zones.json web: {e}")
# 2. Fallback ke configs/area_truk.json
config_file = Path(__file__).parent / "configs" / "area_truk.json"
if config_file.exists():
try:
cdata = json.loads(config_file.read_text(encoding="utf-8"))
vw = cdata.get("video_width", 1920)
vh = cdata.get("video_height", 1080)
scale_x = width / float(vw)
scale_y = height / float(vh)
x_min = int(cdata.get("zone_x_min", 858) * scale_x)
x_max = int(cdata.get("zone_x_max", 1231) * scale_x)
y_min = int(cdata.get("zone_y_min", 7) * scale_y)
y_max = int(cdata.get("zone_y_max", 581) * scale_y)
line_y = int(cdata.get("line_y", 580) * scale_y)
print(f"[RPO IKI] Memuat & Rescale boundary dari configs/area_truk.json: x={x_min}..{x_max}, y={y_min}..{y_max}, line_y={line_y} (scale={scale_x:.2f})")
return BoundarySettings(
line_pos=line_y,
orientation=cdata.get("orientation", "horizontal"),
direction=cdata.get("direction", "bottom_to_top"),
zone_x_min=x_min,
zone_x_max=x_max,
zone_y_min=y_min,
zone_y_max=y_max
)
except Exception as e:
print(f"[WARNING] Gagal membaca configs/area_truk.json: {e}")
return BoundarySettings(
line_pos=290 if height <= 600 else 580,
orientation="horizontal",
direction="bottom_to_top",
zone_x_min=429 if width <= 960 else 858,
zone_x_max=615 if width <= 960 else 1231,
zone_y_min=4 if height <= 600 else 7,
zone_y_max=290 if height <= 600 else 581
)
def draw_friend_hud_overlay(frame, count, truck_state, current_fps):
"""Visualisasi HUD Glassmorphic persis buatan rpo iki (step02_count_live.py)."""
scale = frame.shape[1] / 1280.0
card_w = int(430 * scale)
card_h = int(210 * scale)
cx1, cy1 = int(20 * scale), int(20 * scale)
cx2, cy2 = cx1 + card_w, cy1 + card_h
overlay = frame.copy()
cv2.rectangle(overlay, (cx1, cy1), (cx2, cy2), (20, 24, 33), -1)
cv2.addWeighted(overlay, 0.72, frame, 0.28, 0, frame)
accent_w = int(6 * scale)
accent_color = (0, 180, 255) # Orange default
if truck_state == "LOCKED":
accent_color = (16, 185, 129) # Emerald Green
elif truck_state == "WAITING":
accent_color = (59, 130, 246) # Blue
cv2.rectangle(frame, (cx1, cy1), (cx1 + accent_w, cy2), accent_color, -1)
cv2.rectangle(frame, (cx1, cy1), (cx2, cy2), (64, 74, 95), max(1, int(1 * scale)))
tx = cx1 + int(18 * scale)
cv2.putText(
frame,
f"AI COUNTER MONITOR | CC1 | LATCH: {truck_state}",
(tx, cy1 + int(24 * scale)),
cv2.FONT_HERSHEY_SIMPLEX,
0.44 * scale,
(148, 163, 184),
max(1, int(1 * scale)),
)
cv2.putText(
frame,
"MUAT TRUK:",
(tx, cy1 + int(64 * scale)),
cv2.FONT_HERSHEY_SIMPLEX,
0.55 * scale,
(226, 232, 240),
max(1, int(1 * scale)),
)
cv2.putText(
frame,
f"{count}",
(tx + int(130 * scale), cy1 + int(72 * scale)),
cv2.FONT_HERSHEY_SIMPLEX,
1.35 * scale,
accent_color,
max(1, int(3 * scale)),
)
cv2.putText(
frame,
f"Status Truk: {truck_state}",
(tx, cy1 + int(108 * scale)),
cv2.FONT_HERSHEY_SIMPLEX,
0.56 * scale,
(241, 245, 249),
max(1, int(1 * scale)),
)
cv2.putText(
frame,
f"FPS: {current_fps:.1f} | Engine: RPO IKI (Friends)",
(tx, cy1 + int(192 * scale)),
cv2.FONT_HERSHEY_SIMPLEX,
0.44 * scale,
(100, 116, 139),
max(1, int(1 * scale)),
)
def run_rpo_iki_prediction():
print("=" * 60)
print("MEMULAI LIVE PREDICTION ENGINE (RPO IKI - FRIENDS ALGORITHM)")
print("============================================================")
init_db()
start_streaming_server(port=8000)
# Path model karung
model_path = Path(__file__).parent / "DATA" / "models" / "karung-dimuat-seg-200e.pt"
if not model_path.exists():
model_path = Path(_DEFAULT_DIR) / "karung-dimuat-detection-di-feedmill-yolo26n-seg-200e.pt"
if not model_path.exists():
model_path = Path("karung-dimuat-detection-di-feedmill-yolo26n-seg-200e.pt")
print(f"[RPO IKI] Memuat Model YOLO Karung: {model_path}")
model = YOLO(str(model_path))
# Path model truk
truck_model_path = Path(_DEFAULT_DIR) / "truck-detector.pt"
if not truck_model_path.exists():
truck_model_path = Path("/home/jetson/karung/truck-detector.pt")
if not truck_model_path.exists():
truck_model_path = Path(__file__).parent.parent / "truck-detector.pt"
if not truck_model_path.exists():
truck_model_path = Path("truck-detector.pt")
truck_model = None
truck_class_id = 0
if truck_model_path.exists():
print(f"[RPO IKI AI Latch] Memuat model detektor truk: {truck_model_path}")
truck_model = YOLO(str(truck_model_path))
truck_class_id = 0 if "truck-detector" in str(truck_model_path) else 7
else:
print(f"[RPO IKI] Model detektor truk tidak ditemukan, menggunakan mode ROI Statis Locked.")
# Tentukan device inferensi
import torch
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(f"[RPO IKI] Device inferensi diset ke: {device}")
# Source RTSP
source_path = os.getenv("RTSP_URL", "rtsp://admin:K0l0r4n123@10.38.250.21/cam/realmonitor?channel=1&subtype=0")
is_stream = any(str(source_path).startswith(p) for p in ["rtsp://", "rtmp://", "http://", "https://"])
if is_stream:
print(f"[RPO IKI] Membuka RTSP Stream via Threaded Bufferless Reader: {source_path}")
cap = RTSPBufferlessCapture(source_path)
else:
print(f"[RPO IKI] Membuka File Video Lokal: {source_path}")
cap = cv2.VideoCapture(source_path)
if not cap.isOpened():
print(f"[ERROR] Gagal membuka stream / video: {source_path}")
return
raw_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) or 1920
raw_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) or 1080
fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
is_1080p = (raw_width == 1920 and raw_height == 1080)
width = 960 if is_1080p else raw_width
height = 540 if is_1080p else raw_height
boundary = resolve_live_boundary(width, height)
params = load_counting_params()
counter = LineCounter(
line_pos=boundary.line_pos,
orientation=boundary.orientation,
direction=boundary.direction,
min_approach_depth=params.get("min_approach_depth", 15.0),
same_sack_radius=params.get("same_sack_radius", 70.0),
stack_sack_radius=params.get("stack_sack_radius", 58.0),
outside_confirm_frames=params.get("outside_confirm_frames", 2),
count_cooldown_dist=params.get("count_cooldown_dist", 85.0),
count_cooldown_frames=params.get("count_cooldown_frames", 40),
staging_cooldown_frames=params.get("staging_cooldown_frames", 15),
min_staging_depth=params.get("min_staging_depth", 40.0),
min_track_frames=params.get("min_track_frames", 8),
ghost_track_frames=params.get("ghost_track_frames", 0),
clip_warmup_frames=params.get("clip_warmup_frames", 25),
zone_x_min=boundary.zone_x_min,
zone_x_max=boundary.zone_x_max,
zone_y_min=boundary.zone_y_min,
zone_y_max=boundary.zone_y_max,
)
# State machine truk
truck_state = "LOCKED" # Default locked agar langsung menghitung karung
consecutive_truck_detections = 0
locked_bbox = None
flashes: list[CountFlash] = []
frame_idx = 0
last_time = time.time()
current_fps = 0.0
print(f"[RPO IKI] Counter Siap! Resized Frame: {width}x{height}, Line: {boundary.orientation} @ {counter._line_pos}, Zone: x={counter.zone_x_min}..{counter.zone_x_max}, y={counter.zone_y_min}..{counter.zone_y_max}")
config_file = Path(__file__).parent / "configs" / "area_truk.json"
last_config_mtime = config_file.stat().st_mtime if config_file.exists() else 0.0
while cap.isOpened():
ret, frame = cap.read()
if not ret or frame is None:
time.sleep(0.01)
continue
frame_idx += 1
# Hot-reload konfigurasi jika configs/area_truk.json diperbarui di disk / web
if frame_idx % 25 == 0 and config_file.exists():
try:
mtime = config_file.stat().st_mtime
if mtime > last_config_mtime:
last_config_mtime = mtime
boundary = resolve_live_boundary(width, height)
counter._line_pos = boundary.line_pos
counter.zone_x_min = boundary.zone_x_min
counter.zone_x_max = boundary.zone_x_max
counter.zone_y_min = boundary.zone_y_min
counter.zone_y_max = boundary.zone_y_max
print(f"[Live Config Reload] Boundary diperbarui secara dinamis: x={boundary.zone_x_min}..{boundary.zone_x_max}, line_y={boundary.line_pos}")
except Exception:
pass
# Resize ke 960x540 jika 1080p agar presisi dengan area_truk.json rpo iki
if is_1080p and frame.shape[1] == 1920 and frame.shape[0] == 1080:
frame = cv2.resize(frame, (960, 540))
annotated = frame.copy()
# 1. Dynamic Truk Latch State Machine (Jika truck_model aktif)
if truck_model is not None and truck_state == "WAITING":
if frame_idx % 10 == 0:
results_t = truck_model(frame, classes=[truck_class_id], conf=0.40, verbose=False)
boxes_t = results_t[0].boxes
if len(boxes_t) > 0:
sorted_boxes = sorted(boxes_t, key=lambda b: (b.xyxy[0][2] - b.xyxy[0][0]) * (b.xyxy[0][3] - b.xyxy[0][1]), reverse=True)
t_box = sorted_boxes[0].xyxy[0].cpu().numpy()
tx1, ty1, tx2, ty2 = map(int, t_box)
consecutive_truck_detections += 1
if consecutive_truck_detections >= 3:
locked_bbox = (tx1, ty1, tx2, ty2)
truck_state = "LOCKED"
consecutive_truck_detections = 0
line_y = int(ty2 - 5)
boundary = replace(
boundary,
line_pos=line_y,
zone_x_min=tx1,
zone_x_max=tx2,
zone_y_min=ty1,
zone_y_max=ty2,
)
counter._line_pos = boundary.line_pos
counter.zone_x_min = boundary.zone_x_min
counter.zone_x_max = boundary.zone_x_max
counter.zone_y_min = boundary.zone_y_min
counter.zone_y_max = boundary.zone_y_max
print(f"[AI Latch] Truk Terdeteksi Stabil! Mengunci ROI Bak Truk: x={tx1}..{tx2}, y={ty1}..{ty2}, line_y={line_y}")
# 2. Tracking Karung & Counting (Saat truck_state == "LOCKED")
boxes = None
masks = None
if truck_state in ("LOCKED", "DEPARTING"):
results = model.track(
frame,
persist=True,
classes=[SACK_CLASS_ID],
conf=params.get("conf", 0.15),
tracker="bytetrack.yaml",
device=device,
verbose=False
)
boxes = results[0].boxes
masks = results[0].masks
if boxes is not None and boxes.id is not None:
for box_coord, track_id in zip(boxes.xyxy.cpu().numpy(), boxes.id.int().cpu().tolist()):
cross, foot = track_points(box_coord)
count_event = counter.update(track_id, cross, frame_idx, foot)
if count_event is not None:
flashes.append(CountFlash(number=counter.count, x=int(cross[0]), y=int(cross[1]), frames_left=int(fps * 0.35)))
print(f"[RPO IKI COUNTER] Karung #{track_id} TERHITUNG! Total: {counter.count}")
save_active_batch_state(count=counter.count, truck_state=truck_state)
# 3. Render Visualisasi Asli rpo iki (Garis Hijau Batas, Kotak Truk Orange, HUD)
draw_blind_truck_overlay(annotated, boundary, boundary.line_pos)
draw_boundary(
annotated,
boundary.line_pos,
boundary.orientation,
boundary.zone_x_min,
boundary.zone_x_max,
boundary.zone_y_min,
boundary.zone_y_max,
)
# Gambar BBox Karung yang sedang mendekati garis
if boxes is not None and boxes.id is not None:
annotate_tracks(
annotated,
boxes,
counter,
flashes,
fps,
frame_idx,
blind_truck=True,
masks=masks,
show_mask=True
)
tick_flashes(flashes)
draw_count_flashes(annotated, flashes, fps)
draw_friend_hud_overlay(annotated, counter.count, truck_state, current_fps)
# Hitung FPS
if frame_idx % 25 == 0:
now = time.time()
elapsed = now - last_time
if elapsed > 0:
current_fps = 25.0 / elapsed
last_time = now
print(f"[INFO] Frame {frame_idx} - State: {truck_state} - Terhitung: {counter.count} karung ({current_fps:.2f} FPS)")
save_active_batch_state(count=counter.count, truck_state=truck_state)
# Tulis live frame preview untuk Web Server Port 8000
write_live_frame(annotated)
if __name__ == "__main__":
try:
run_rpo_iki_prediction()
except KeyboardInterrupt:
print("\n[RPO IKI] Program dihentikan secara manual (Ctrl+C).")
+68
View File
@@ -0,0 +1,68 @@
import os
# Optimize OpenMP and MKL thread allocation for AMD Ryzen 5 6600H (6 Cores)
os.environ["OMP_NUM_THREADS"] = "6"
os.environ["MKL_NUM_THREADS"] = "6"
import cv2
import torch
from ultralytics import YOLO
# 1. Load the PyTorch YOLO segmentation model
model_path = "best.pt"
model = YOLO(model_path)
# Optimize PyTorch CPU thread pools for 6 physical cores to avoid SMT hyperthreading overhead
torch.set_num_threads(6)
print("Thread PyTorch diset ke 6 (Physical Cores) untuk optimalisasi CPU AMD Ryzen 5.")
# Auto-detect device
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Device inferensi diset ke: {device}")
# 2. Open the video file
video_path = r"D:\Belajar\Menghitung karung\0727.mp4"
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
print(f"Error: Gagal membuka video di {video_path}")
exit(1)
# Optimasi 1: Frame Stride (Frame Skipping)
# FRAME_STRIDE = 3 artinya memproses 1 dari setiap 3 frame (sangat berguna untuk video 60fps agar CPU tidak overload)
FRAME_STRIDE = 3
frame_idx = 0
print("=== Simple Predict (Optimized for AMD Ryzen) Running ===")
print("Tekan 'q' di jendela video untuk keluar.\n")
annotated_frame = None
while cap.isOpened():
ret, frame = cap.read()
if not ret:
print("Video selesai diputar atau tidak terbaca.")
break
frame_idx += 1
# Hanya jalankan deteksi model pada frame tertentu berdasarkan STRIDE
if FRAME_STRIDE <= 1 or frame_idx % FRAME_STRIDE == 0 or annotated_frame is None:
# Optimasi 2: perkecil resolusi inferensi imgsz=320 untuk kecepatan maksimal
# Optimasi 3: gunakan device yang sesuai (cpu)
results = model(frame, conf=0.15, classes=[0], imgsz=320, device=device, verbose=False)
# Optimasi 4: Gambar hasil deteksi (diset masks=False untuk kecepatan menggambar di CPU)
annotated_frame = results[0].plot(masks=False)
# 5. Tampilkan frame di jendela
resized_frame = cv2.resize(annotated_frame, (960, 540))
cv2.imshow("YOLO Live Predict - Karung (Optimized)", resized_frame)
# Keluar jika tombol 'q' ditekan
if cv2.waitKey(1) & 0xFF == ord('q'):
break
# 6. Bersihkan resource
cap.release()
cv2.destroyAllWindows()
print("Proses selesai.")
+28
View File
@@ -0,0 +1,28 @@
import cv2
import numpy as np
from ultralytics import YOLO
import sys
def main():
model_path = "/home/jetson/karung/model_karung_truk.engine"
print("Loading model...")
model = YOLO(model_path)
# Warm up
print("Warming up model...")
dummy = np.zeros((720, 1280, 3), dtype=np.uint8)
results = model(dummy, imgsz=640, device="cuda", verbose=False)
print("Warm up complete!")
# Test tracking with standard bytetrack
print("Testing standard bytetrack on 10 frames...")
for i in range(10):
print(f"Tracking frame {i+1}...")
results = model.track(dummy, persist=True, tracker="bytetrack.yaml", verbose=False)
print(f"Frame {i+1} track complete! Detections count: {len(results[0])}")
print("Standard ByteTrack test passed successfully!")
if __name__ == '__main__':
main()
+28
View File
@@ -0,0 +1,28 @@
import cv2
import numpy as np
from ultralytics import YOLO
import sys
def main():
model_path = "/home/jetson/karung/model_karung_truk.engine"
print("Loading model...")
model = YOLO(model_path)
# Warm up
print("Warming up model...")
dummy = np.zeros((720, 1280, 3), dtype=np.uint8)
results = model(dummy, imgsz=640, device="cuda", verbose=False)
print("Warm up complete!")
# Test tracking
print("Testing track on 5 frames...")
for i in range(5):
print(f"Tracking frame {i+1}...")
results = model.track(dummy, persist=True, verbose=False)
print(f"Frame {i+1} track complete! Detections count: {len(results[0])}")
print("All tests passed successfully!")
if __name__ == '__main__':
main()
+77
View File
@@ -0,0 +1,77 @@
import sqlite3
import shutil
from datetime import datetime
db_path = '/opt/jetson-counter/jetson_counter.db'
# 1. Backup database
backup_path = f'/opt/jetson-counter/jetson_counter.db.bak_{datetime.now().strftime("%Y%m%d_%H%M%S")}'
shutil.copy2(db_path, backup_path)
print(f"Backup created at: {backup_path}")
conn = sqlite3.connect(db_path)
cur = conn.cursor()
# Check current batches on 2026-08-20
cur.execute("SELECT id, batch_number, count, start_time, end_time FROM batches WHERE counting_date = '2026-08-20' ORDER BY batch_number ASC")
old_rows = cur.fetchall()
print("Before update:")
for r in old_rows:
print(r)
# Step A: Shift batch 2..10 to batch + 2
cur.execute("SELECT id, batch_number FROM batches WHERE counting_date = '2026-08-20' AND batch_number >= 2 ORDER BY batch_number DESC")
shift_rows = cur.fetchall()
for bid, bnum in shift_rows:
new_bnum = bnum + 2
cur.execute("UPDATE batches SET batch_number = ? WHERE id = ?", (new_bnum, bid))
# Step B: Update batch 1 (id 1525) to batch 1 with 219 sacks
cur.execute('''
UPDATE batches
SET count = 219,
start_time = '2026-08-20T09:56:44',
end_time = '2026-08-20T10:16:22'
WHERE id = 1525
''')
# Step C: Insert batch 2 (168 sacks) and batch 3 (174 sacks)
cur.execute('''
INSERT INTO batches (counting_date, batch_number, camera_name, object_label, count, start_time, end_time)
VALUES ('2026-08-20', 2, 'CC1', 'karung-pakan', 168, '2026-08-20T10:24:47', '2026-08-20T10:55:49')
''')
cur.execute('''
INSERT INTO batches (counting_date, batch_number, camera_name, object_label, count, start_time, end_time)
VALUES ('2026-08-20', 3, 'CC1', 'karung-pakan', 174, '2026-08-20T11:03:30', '2026-08-20T11:19:19')
''')
# Step D: Recalculate daily_summaries for 2026-08-20
cur.execute('''
SELECT SUM(count), COUNT(id)
FROM batches
WHERE counting_date = '2026-08-20' AND camera_name = 'CC1' AND object_label = 'karung-pakan'
''')
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 ('2026-08-20', 'CC1', 'karung-pakan', ?, ?, CURRENT_TIMESTAMP)
''', (tot_count, tot_batches))
conn.commit()
# Verify new data
cur.execute("SELECT id, batch_number, count, start_time, end_time FROM batches WHERE counting_date = '2026-08-20' ORDER BY batch_number ASC")
new_rows = cur.fetchall()
print("\nAfter update:")
for r in new_rows:
print(r)
cur.execute("SELECT * FROM daily_summaries WHERE counting_date = '2026-08-20'")
print("\nDaily Summary:", cur.fetchall())
conn.close()