feat: config-authoritative conf gates + cross-model IoU sack suppression
ci / smoke (push) Canceled after 0s
ci / smoke (push) Canceled after 0s
This commit is contained in:
1 parent
1e4dc4201c
commit
a12056a5ad
7 files changed
+80
-7
No files matched your search
+5
-2
@@ -79,11 +79,11 @@ models:
|
||||
# perspective min_valid_area filter still applies on top).
|
||||
detection_params:
|
||||
truck:
|
||||
conf: 0.35
|
||||
conf: 0.45 # live gate was hardcoded 0.45; config authoritative now
|
||||
iou: 0.7
|
||||
min_bbox_area: 5000
|
||||
sack:
|
||||
conf: 0.3 # default 0.35
|
||||
conf: 0.4 # live gate used to hardcode 0.50; 0.40 compromise (config is authoritative)
|
||||
iou: 0.7
|
||||
min_bbox_area: 1500
|
||||
box:
|
||||
@@ -107,6 +107,9 @@ counting:
|
||||
camera_noise_deadband: 50
|
||||
# Effective values below match zones.json (which previously overrode predict.py defaults).
|
||||
duplicate_circle_radius: 30
|
||||
# Drop sack detections overlapping a box detection (cross-model IoU) — v4 has
|
||||
# no box class, so white boxes get labeled "sack"; box model always wins.
|
||||
cross_class_iou: 0.4
|
||||
min_valid_area: 15000
|
||||
max_reid_transit_distance: 400
|
||||
circle_stay_timeout_sec: 10.0
|
||||
|
||||
@@ -16,7 +16,11 @@ cfg/tracker.yaml tracker hyperparams (FastTrack/ByteTrack tuning)
|
||||
Model modes are **data** (`config.yaml` → `models.modes`): each preset declares
|
||||
only `engines` (path key + contributed classes) and `class_filters`.
|
||||
Per-class `conf`/`iou`/`min_bbox_area` live in `models.detection_params` and
|
||||
apply to ALL modes. Adding mode E/F/... is a YAML-only change — `predict.py`
|
||||
apply to ALL modes. `detection_params.*.conf` is the **authoritative detection
|
||||
floor** — `predict.py` no longer hardcodes a second gate (old 0.50 sack / 0.45
|
||||
truck overrides are gone). `counting.cross_class_iou` (default 0.4) drops sack
|
||||
detections that overlap a box detection (v4 has no box class); `<= 0` disables.
|
||||
Adding mode E/F/... is a YAML-only change — `predict.py`
|
||||
derives tracker roles structurally, and the dashboard `/api/model-modes`
|
||||
endpoint lists them automatically.
|
||||
|
||||
|
||||
+18
-4
@@ -21,7 +21,7 @@ from src.detection import SackDetector, TruckDetector, BoxDetector
|
||||
from src.tracking import ByteTrackTracker
|
||||
from src.stabilizer import BboxStabilizer
|
||||
from src.truck_roi import TruckROITracker
|
||||
from src.counting import LineCrossCounter, MultiClassLineCounter
|
||||
from src.counting import LineCrossCounter, MultiClassLineCounter, drop_sacks_overlapping_boxes
|
||||
from src.batch import BatchLifecycleManager, BatchRecord
|
||||
from src.dashboard import DashboardOverlay
|
||||
from src.config_loader import (
|
||||
@@ -454,6 +454,7 @@ MIN_DY_COUNTING_ZONE = -3
|
||||
# --- Parameter Lingkaran Duplikat Statis ---
|
||||
DUPLICATE_CIRCLE_RADIUS_REF = 35
|
||||
DUPLICATE_CIRCLE_RADIUS = 35
|
||||
CROSS_CLASS_IOU = 0.4 # drop sack dets overlapping a box det (cross-model)
|
||||
SHOW_ALL_BBOXES = False
|
||||
counted_sack_positions = []
|
||||
CIRCLE_STAY_TIMEOUT_SEC = 10.0
|
||||
@@ -632,6 +633,11 @@ def build_model_pipeline(mode, cfg, device, base_dir=None):
|
||||
"sack": dp_sack.min_bbox_area,
|
||||
"box": dp_box.min_bbox_area,
|
||||
},
|
||||
"confs": {
|
||||
"truck": dp_truck.conf,
|
||||
"sack": dp_sack.conf,
|
||||
"box": dp_box.conf,
|
||||
},
|
||||
}
|
||||
|
||||
# =====================================================================
|
||||
@@ -1264,6 +1270,7 @@ def run_prediction(model_path, source_path,
|
||||
global DUPLICATE_CIRCLE_RADIUS, MIN_VALID_AREA, JARAK_TOLERANSI_DUPLIKAT, MAX_REID_TRANSIT_DISTANCE
|
||||
global ENTRY_OVERLAP_THRESHOLD, EXIT_OVERLAP_THRESHOLD, TOLERANSI_FRAME_HILANG
|
||||
global MAX_REID_FRAMES, DEBOUNCE_FRAMES
|
||||
global CROSS_CLASS_IOU
|
||||
global DB_PATH, STATE_FILE, BATCH_MODE_FILE, LIVE_STREAM_FRAME_PATH
|
||||
global CAMERA_NAME, OBJECT_LABEL, DAILY_CUTOFF_TIME, BATCH_MERGE_THRESHOLD_SECONDS
|
||||
global CIRCLE_STAY_TIMEOUT_SEC, _CFG
|
||||
@@ -1315,6 +1322,7 @@ def run_prediction(model_path, source_path,
|
||||
TOLERANSI_FRAME_HILANG = cfg.counting.tolerance_missing_frames
|
||||
MAX_REID_FRAMES = cfg.counting.max_reid_frames
|
||||
DEBOUNCE_FRAMES = cfg.counting.debounce_frames
|
||||
CROSS_CLASS_IOU = cfg.counting.cross_class_iou
|
||||
|
||||
# Legacy batch_mode.json no longer drives the mode — nudge once if stale.
|
||||
_legacy_warn = check_legacy_batch_mode(cfg, BATCH_MODE_FILE)
|
||||
@@ -1372,6 +1380,7 @@ def run_prediction(model_path, source_path,
|
||||
box_tracker = pipe["box_tracker"]
|
||||
separate_truck_model = pipe["separate_truck_model"]
|
||||
min_areas = pipe["min_areas"]
|
||||
confs = pipe["confs"]
|
||||
stabilizer = BboxStabilizer(
|
||||
ema_alpha=0.35,
|
||||
max_hold_frames=10,
|
||||
@@ -1519,13 +1528,14 @@ def run_prediction(model_path, source_path,
|
||||
else:
|
||||
raw_tracked_all = last_raw_tracked_all
|
||||
|
||||
# Filter sack (+box, modes B-D) detections (confidence >= 0.50).
|
||||
# Filter sack (+box, modes B-D) detections. Per-class conf floor
|
||||
# comes from config.yaml detection_params (authoritative).
|
||||
# Counter splits by class_name downstream; MultiClassLineCounter
|
||||
# ignores anything that is not sack/box.
|
||||
# min_bbox_area guardrails come from config.yaml detection_params.
|
||||
raw_tracked_sacks = [
|
||||
d for d in raw_tracked_all
|
||||
if d.class_name in ("sack", "box") and d.confidence >= 0.50
|
||||
if d.class_name in ("sack", "box") and d.confidence >= confs.get(d.class_name, 0.50)
|
||||
and _bbox_area_ok(d, min_areas.get(d.class_name, 0))
|
||||
]
|
||||
|
||||
@@ -1543,7 +1553,7 @@ def run_prediction(model_path, source_path,
|
||||
# Filter truck detections: Wajib 100% berada di dalam detection_polygon & ambil maksimal 1 bbox terbaik
|
||||
valid_trucks = []
|
||||
for d in truck_candidates:
|
||||
if d.class_name == "truck" and d.confidence >= 0.45:
|
||||
if d.class_name == "truck" and d.confidence >= confs.get("truck", 0.45):
|
||||
x1, y1, x2, y2 = d.bbox
|
||||
# Bounding box truk 100% harus berada di dalam detection_polygon
|
||||
truck_bbox_poly = box(x1, y1, x2, y2)
|
||||
@@ -1589,6 +1599,10 @@ def run_prediction(model_path, source_path,
|
||||
]
|
||||
|
||||
# Combined list for ROI filter / counting / viz (counter splits by class).
|
||||
# Cross-model suppression: v4 has no box class (white boxes labeled
|
||||
# "sack"); box model always wins. cross_class_iou <= 0 disables.
|
||||
if stable_boxes and CROSS_CLASS_IOU > 0:
|
||||
stable = drop_sacks_overlapping_boxes(stable, stable_boxes, CROSS_CLASS_IOU)
|
||||
stable_all = stable + stable_boxes
|
||||
|
||||
# Count sacks currently visible in the bottom 85% of truck area (for batch start/end condition)
|
||||
|
||||
@@ -90,6 +90,7 @@ class CountingConfig:
|
||||
exit_overlap_threshold: float = 0.05
|
||||
camera_noise_deadband: int = 50
|
||||
duplicate_circle_radius: int = 30
|
||||
cross_class_iou: float = 0.4
|
||||
min_valid_area: int = 15000
|
||||
max_reid_transit_distance: int = 400
|
||||
circle_stay_timeout_sec: float = 10.0
|
||||
@@ -467,6 +468,7 @@ def load_config(path: str | Path = "config.yaml") -> Config:
|
||||
exit_overlap_threshold=float(counting_raw.get("exit_overlap_threshold", 0.05)),
|
||||
camera_noise_deadband=int(counting_raw.get("camera_noise_deadband", 50)),
|
||||
duplicate_circle_radius=int(counting_raw.get("duplicate_circle_radius", 30)),
|
||||
cross_class_iou=float(counting_raw.get("cross_class_iou", 0.4)),
|
||||
min_valid_area=int(counting_raw.get("min_valid_area", 15000)),
|
||||
max_reid_transit_distance=int(counting_raw.get("max_reid_transit_distance", 400)),
|
||||
circle_stay_timeout_sec=float(counting_raw.get("circle_stay_timeout_sec", 10.0)),
|
||||
|
||||
@@ -337,3 +337,36 @@ class MultiClassLineCounter:
|
||||
def reset(self) -> None:
|
||||
self._sack.reset()
|
||||
self._box.reset()
|
||||
|
||||
|
||||
def _bbox_iou(a, b) -> float:
|
||||
ax1, ay1, ax2, ay2 = a
|
||||
bx1, by1, bx2, by2 = b
|
||||
ix1, iy1 = max(ax1, bx1), max(ay1, by1)
|
||||
ix2, iy2 = min(ax2, bx2), min(ay2, by2)
|
||||
iw, ih = max(0.0, ix2 - ix1), max(0.0, iy2 - iy1)
|
||||
inter = iw * ih
|
||||
if inter <= 0.0:
|
||||
return 0.0
|
||||
union = (ax2 - ax1) * (ay2 - ay1) + (bx2 - bx1) * (by2 - by1) - inter
|
||||
return inter / union if union > 0.0 else 0.0
|
||||
|
||||
|
||||
def drop_sacks_overlapping_boxes(
|
||||
sacks: list[Detection],
|
||||
boxes: list[Detection],
|
||||
iou_thr: float = 0.4,
|
||||
) -> list[Detection]:
|
||||
"""Cross-model suppression: drop `sack` detections overlapping a `box`.
|
||||
|
||||
Combined engine (v4) has no `box` class, so white boxes on a truck get
|
||||
labeled `sack`; the box-only model always wins. `iou_thr <= 0` disables.
|
||||
"""
|
||||
if iou_thr <= 0 or not sacks or not boxes:
|
||||
return list(sacks)
|
||||
box_bbs = [b.bbox for b in boxes]
|
||||
return [
|
||||
s for s in sacks
|
||||
if s.class_name != "sack"
|
||||
or all(_bbox_iou(s.bbox, bb) < iou_thr for bb in box_bbs)
|
||||
]
|
||||
@@ -84,6 +84,7 @@ def test_counting_knobs_match_production_defaults(repo_config_path):
|
||||
assert c.exit_overlap_threshold == 0.05
|
||||
assert c.camera_noise_deadband == 50
|
||||
assert c.duplicate_circle_radius == 30
|
||||
assert c.cross_class_iou == 0.4
|
||||
assert c.min_valid_area == 15000
|
||||
assert c.max_reid_transit_distance == 400
|
||||
assert c.circle_stay_timeout_sec == 10.0
|
||||
|
||||
@@ -96,3 +96,19 @@ def test_multiclass_geometry_sync_and_reset():
|
||||
assert c.loading_count == 1
|
||||
c.reset()
|
||||
assert (c.loading_count, c.box_loading_count, c.box_net_count) == (0, 0, 0)
|
||||
|
||||
def test_drop_sacks_overlapping_boxes():
|
||||
from src.counting import drop_sacks_overlapping_boxes
|
||||
box = _det(9, y1=100, cx=500.0, class_name="box")
|
||||
# identical footprint -> IoU 1.0 >= 0.4 -> sack dropped
|
||||
assert drop_sacks_overlapping_boxes([_det(1, y1=100, cx=500.0)], [box]) == []
|
||||
# partial overlap (cx 515) -> IoU ~0.45 -> dropped
|
||||
assert drop_sacks_overlapping_boxes([_det(2, y1=100, cx=515.0)], [box]) == []
|
||||
# no overlap -> kept
|
||||
kept = drop_sacks_overlapping_boxes([_det(3, y1=100, cx=700.0)], [box])
|
||||
assert len(kept) == 1 and kept[0].track_id == 3
|
||||
# no boxes / disabled -> kept
|
||||
assert len(drop_sacks_overlapping_boxes([_det(4, y1=100)], [], 0.4)) == 1
|
||||
assert len(drop_sacks_overlapping_boxes([_det(5, y1=100)], [box], 0.0)) == 1
|
||||
# box dets in sack list never dropped
|
||||
assert len(drop_sacks_overlapping_boxes([_det(6, y1=100, class_name="box")], [box])) == 1
|
||||
Reference in new issue
Block a user