refactor: single predict.py entrypoint (production + CLI), archive experiments
ci / smoke (push) Canceled after 0s
ci / smoke (push) Canceled after 0s
This commit is contained in:
1 parent
beb9c9fb6b
commit
52d3127302
36 files changed
+161
-48
No files matched your search
+1388
File diff suppressed because it is too large.
Load diff
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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.")
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"enabled": true,
|
||||
"token": "8654129536:AAHrx4x7OPm84WRDhRLj3oIMgecUDKSfqgs",
|
||||
"chat_id": "-5147118224"
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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).")
|
||||
@@ -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.")
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user