refactor: consolidate counting config through src/config_loader
- config.yaml gains counting.max_reid_frames (200) + debounce_frames (8); CountingConfig + parsing + pinned-value test added - predict.py load_zones() delegates to read_zone_polygons() (geometry only); legacy zones.json knob reads removed (config.yaml is canonical) - run_prediction wires 5 more live knobs from cfg.counting.* (entry/exit overlap, tolerance_missing_frames, max_reid_frames, debounce_frames) - delete deprecated src/config.py (v3 keys) + tests/test_config.py; src/main.py bridges onto unified config so the deprecated entrypoint keeps working - 8 never-read globals (JARAK_ABSORBSI_GHOST, CAMERA_NOISE_DEADBAND, ...) left untouched — flagged for Phase 2 dead-code cleanup Verified: pytest 21 passed; full pipeline runs on synthetic video (engines load, 30 frames, clean finish).
This commit is contained in:
1 parent
0467e0ce68
commit
12b70a3431
7 files changed
+90
-124
No files matched your search
@@ -112,6 +112,8 @@ counting:
|
||||
circle_stay_timeout_sec: 10.0
|
||||
jarak_toleransi_duplikat: 30
|
||||
tolerance_missing_frames: 1200
|
||||
max_reid_frames: 200
|
||||
debounce_frames: 8
|
||||
|
||||
batch:
|
||||
timeout_seconds: 30.0
|
||||
|
||||
+22
-29
@@ -30,7 +30,6 @@ from src.config_loader import (
|
||||
load_config,
|
||||
read_zone_polygons,
|
||||
resolve_active_mode,
|
||||
LEGACY_ZONE_KNOBS,
|
||||
)
|
||||
|
||||
# --- SQLite Database & State Configuration ---
|
||||
@@ -649,39 +648,26 @@ DEFAULT_TRUCK = []
|
||||
|
||||
|
||||
def load_zones():
|
||||
global ZONA_PALET_REF, ZONA_TRUCK_REF, GARIS_COUNTING_REF, DUPLICATE_CIRCLE_RADIUS_REF
|
||||
global MIN_VALID_AREA_REF, JARAK_TOLERANSI_DUPLIKAT_REF, MAX_REID_TRANSIT_DISTANCE_REF
|
||||
global CIRCLE_STAY_TIMEOUT_SEC, INFERENCE_STRIDE, CONFIRM_DELAY_SEC, EXIT_CONFIRM_DELAY_SEC
|
||||
"""Load zone geometry from zones.json (polygons only — knobs live in config.yaml).
|
||||
|
||||
Delegates to src.config_loader.read_zone_polygons: legacy knob keys in
|
||||
zones.json are ignored with a warning (config.yaml counting.* wins).
|
||||
ZONA_COUNTING_REF is derived from the truck polygon (bottom quarter),
|
||||
not from the file's "counting" key — preserved pre-YAML behaviour.
|
||||
"""
|
||||
global ZONA_PALET_REF, ZONA_TRUCK_REF, GARIS_COUNTING_REF
|
||||
global ZONA_COUNTING_REF, left_limit_ref, right_limit_ref, EXTERNAL_STREAM_URL_REF
|
||||
if os.path.exists(ZONES_JSON_PATH):
|
||||
try:
|
||||
with open(ZONES_JSON_PATH, 'r') as f:
|
||||
data = json.load(f)
|
||||
ZONA_PALET_REF = np.array(data.get('palet', []), dtype=np.int32)
|
||||
ZONA_TRUCK_REF = np.array(data.get('truck', []), dtype=np.int32)
|
||||
zones = read_zone_polygons(ZONES_JSON_PATH)
|
||||
ZONA_PALET_REF = np.array(zones.get('palet', []), dtype=np.int32)
|
||||
ZONA_TRUCK_REF = np.array(zones.get('truck', []), dtype=np.int32)
|
||||
ZONA_COUNTING_REF = get_bottom_quarter(ZONA_TRUCK_REF)
|
||||
left_limit_ref = float(data.get('left_limit', 0.05))
|
||||
right_limit_ref = float(data.get('right_limit', 0.95))
|
||||
left_limit_ref = float(zones.get('left_limit', 0.05))
|
||||
right_limit_ref = float(zones.get('right_limit', 0.95))
|
||||
GARIS_COUNTING_REF = ZONA_TRUCK_REF.copy()
|
||||
DUPLICATE_CIRCLE_RADIUS_REF = data.get('duplicate_circle_radius', 30)
|
||||
MIN_VALID_AREA_REF = data.get('min_valid_area', 15000)
|
||||
JARAK_TOLERANSI_DUPLIKAT_REF = data.get('jarak_toleransi_duplikat', 20)
|
||||
MAX_REID_TRANSIT_DISTANCE_REF = data.get('max_reid_transit_distance', 400)
|
||||
CIRCLE_STAY_TIMEOUT_SEC = data.get('circle_stay_timeout_sec', 10.0)
|
||||
INFERENCE_STRIDE = data.get('inference_stride', 2)
|
||||
CONFIRM_DELAY_SEC = data.get('confirm_delay_sec', 0.5)
|
||||
EXIT_CONFIRM_DELAY_SEC = data.get('exit_confirm_delay_sec', 6.0)
|
||||
EXTERNAL_STREAM_URL_REF = data.get('external_stream_url', 'http://192.168.192.96:8888/cam/')
|
||||
if not EXTERNAL_STREAM_URL_REF:
|
||||
EXTERNAL_STREAM_URL_REF = 'http://192.168.192.96:8888/cam/'
|
||||
# Knob keys below are LEGACY: config.yaml counting.* is canonical and
|
||||
# run_prediction overrides these globals from it. Kept reading here
|
||||
# only so import-time defaults stay sane; warn once to guide migration.
|
||||
_legacy_knobs = sorted(LEGACY_ZONE_KNOBS & set(data.keys()))
|
||||
if _legacy_knobs:
|
||||
print(f"[WARN] zones.json knob keys {_legacy_knobs} diabaikan — "
|
||||
f"pakai config.yaml (counting.*) sebagai gantinya.")
|
||||
print("[INFO] Berhasil memuat koordinat zona dan parameter kalibrasi dari zones.json")
|
||||
EXTERNAL_STREAM_URL_REF = zones.get('external_stream_url', '') or 'http://192.168.192.96:8888/cam/'
|
||||
print("[INFO] Berhasil memuat koordinat zona dari zones.json (via src.config_loader)")
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"[WARNING] Gagal memuat zones.json ({e}), menggunakan default.")
|
||||
@@ -1247,6 +1233,8 @@ def run_prediction(model_path, source_path,
|
||||
global width, height, CONFIRM_DELAY_SEC, EXIT_CONFIRM_DELAY_SEC
|
||||
global active_batch_info, system_state
|
||||
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 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
|
||||
@@ -1293,6 +1281,11 @@ def run_prediction(model_path, source_path,
|
||||
JARAK_TOLERANSI_DUPLIKAT = cfg.counting.jarak_toleransi_duplikat
|
||||
MAX_REID_TRANSIT_DISTANCE = cfg.counting.max_reid_transit_distance
|
||||
CIRCLE_STAY_TIMEOUT_SEC = cfg.counting.circle_stay_timeout_sec
|
||||
ENTRY_OVERLAP_THRESHOLD = cfg.counting.entry_overlap_threshold
|
||||
EXIT_OVERLAP_THRESHOLD = cfg.counting.exit_overlap_threshold
|
||||
TOLERANSI_FRAME_HILANG = cfg.counting.tolerance_missing_frames
|
||||
MAX_REID_FRAMES = cfg.counting.max_reid_frames
|
||||
DEBOUNCE_FRAMES = cfg.counting.debounce_frames
|
||||
|
||||
# Legacy batch_mode.json no longer drives the mode — nudge once if stale.
|
||||
_legacy_warn = check_legacy_batch_mode(cfg, BATCH_MODE_FILE)
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
"""Configuration loader — reads .env and exposes typed settings."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Config:
|
||||
"""Immutable application configuration."""
|
||||
|
||||
# Stream sources
|
||||
local_rtsp: str = ""
|
||||
jetson_rtsp: str = ""
|
||||
|
||||
# Model paths (canonical production weights in models/; see models/modelREADME.md)
|
||||
sack_model_path: str = "./models/best.engine"
|
||||
truck_model_path: str = "./models/truck-detector.engine"
|
||||
|
||||
# Counting line (fractions of frame dimensions)
|
||||
counting_line_y: float = 0.60
|
||||
counting_line_x_start: float = 0.38
|
||||
counting_line_x_end: float = 0.72
|
||||
|
||||
# Detection confidence
|
||||
sack_conf: float = 0.40
|
||||
truck_conf: float = 0.50
|
||||
|
||||
# Batch management
|
||||
batch_timeout_seconds: float = 30.0
|
||||
|
||||
# Output
|
||||
csv_output_dir: str = "./output"
|
||||
seed: int = 42
|
||||
|
||||
|
||||
def load_config(env_path: str = ".env") -> Config:
|
||||
"""Load configuration from .env file and environment variables."""
|
||||
load_dotenv(env_path)
|
||||
|
||||
return Config(
|
||||
local_rtsp=os.getenv("LOCAL_RTSP", ""),
|
||||
jetson_rtsp=os.getenv("JETSON_RTSP", ""),
|
||||
sack_model_path=os.getenv("MODEL_SACK_PATH", "./models/best.engine"),
|
||||
truck_model_path=os.getenv(
|
||||
"MODEL_TRUCK_PATH", "./models/truck-detector.engine"
|
||||
),
|
||||
counting_line_y=float(os.getenv("COUNTING_LINE_Y", "0.60")),
|
||||
counting_line_x_start=float(
|
||||
os.getenv("COUNTING_LINE_X_START", "0.38")
|
||||
),
|
||||
counting_line_x_end=float(os.getenv("COUNTING_LINE_X_END", "0.72")),
|
||||
sack_conf=float(os.getenv("SACK_CONF_THRESHOLD", "0.40")),
|
||||
truck_conf=float(os.getenv("TRUCK_CONF_THRESHOLD", "0.50")),
|
||||
batch_timeout_seconds=float(
|
||||
os.getenv("BATCH_TIMEOUT_SECONDS", "30")
|
||||
),
|
||||
csv_output_dir=os.getenv("CSV_OUTPUT_DIR", "./output"),
|
||||
seed=int(os.getenv("DATA_SEED", "42")),
|
||||
)
|
||||
@@ -95,6 +95,8 @@ class CountingConfig:
|
||||
circle_stay_timeout_sec: float = 10.0
|
||||
jarak_toleransi_duplikat: int = 30
|
||||
tolerance_missing_frames: int = 1200
|
||||
max_reid_frames: int = 200
|
||||
debounce_frames: int = 8
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -428,6 +430,8 @@ def load_config(path: str | Path = "config.yaml") -> Config:
|
||||
circle_stay_timeout_sec=float(counting_raw.get("circle_stay_timeout_sec", 10.0)),
|
||||
jarak_toleransi_duplikat=int(counting_raw.get("jarak_toleransi_duplikat", 30)),
|
||||
tolerance_missing_frames=int(counting_raw.get("tolerance_missing_frames", 1200)),
|
||||
max_reid_frames=int(counting_raw.get("max_reid_frames", 200)),
|
||||
debounce_frames=int(counting_raw.get("debounce_frames", 8)),
|
||||
),
|
||||
batch=BatchConfig(
|
||||
timeout_seconds=float(batch_raw.get("timeout_seconds", 30.0)),
|
||||
|
||||
+43
-3
@@ -13,7 +13,7 @@ import time
|
||||
import cv2
|
||||
|
||||
from src.batch import BatchLifecycleManager
|
||||
from src.config import load_config, Config
|
||||
from src.config_loader import load_config as _load_unified_config
|
||||
from src.counting import LineCrossCounter
|
||||
from src.dashboard import DashboardOverlay
|
||||
from src.detection import TruckDetector
|
||||
@@ -24,7 +24,47 @@ from src.tracking import ByteTrackTracker
|
||||
from src.truck_roi import TruckROITracker
|
||||
|
||||
|
||||
def build_pipeline(cfg: Config, source_path: str | None = None):
|
||||
def _legacy_cfg_bridge(env_path: str = ".env"):
|
||||
"""Bridge the deprecated v3 Config API onto the unified config_loader.Config.
|
||||
|
||||
`python -m src.main` is deprecated (use predict.py), but must keep working.
|
||||
Only the fields actually used below are bridged.
|
||||
"""
|
||||
import os
|
||||
from types import SimpleNamespace
|
||||
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(env_path)
|
||||
except ImportError:
|
||||
pass
|
||||
cfg = _load_unified_config("config.yaml")
|
||||
mode = cfg.get_active_mode()
|
||||
truck_key = next((e.path for e in mode.engines if "truck" in e.classes), "combined")
|
||||
sack_key = next((e.path for e in mode.engines if "sack" in e.classes), "combined")
|
||||
base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
return SimpleNamespace(
|
||||
local_rtsp=cfg.stream.rtsp_url,
|
||||
jetson_rtsp="",
|
||||
sack_model_path=cfg.engine_path(sack_key, base),
|
||||
truck_model_path=cfg.engine_path(truck_key, base),
|
||||
counting_line_y=0.60,
|
||||
counting_line_x_start=0.38,
|
||||
counting_line_x_end=0.72,
|
||||
sack_conf=cfg.detection_params_for("sack").conf,
|
||||
truck_conf=cfg.detection_params_for("truck").conf,
|
||||
batch_timeout_seconds=cfg.batch.timeout_seconds,
|
||||
csv_output_dir="./output",
|
||||
seed=42,
|
||||
)
|
||||
|
||||
|
||||
def load_config(env_path: str = ".env"):
|
||||
"""Deprecated shim — use src.config_loader.load_config instead."""
|
||||
return _legacy_cfg_bridge(env_path)
|
||||
|
||||
|
||||
def build_pipeline(cfg, source_path: str | None = None):
|
||||
"""Construct all components from config."""
|
||||
|
||||
# ── Stream source ────────────────────────────────────────────
|
||||
@@ -108,7 +148,7 @@ def _filter_sacks_in_roi(detections, roi):
|
||||
]
|
||||
|
||||
|
||||
def run(cfg: Config, source_path: str | None = None) -> None:
|
||||
def run(cfg, source_path: str | None = None) -> None:
|
||||
"""Main processing loop — processes EVERY frame for accuracy."""
|
||||
p = build_pipeline(cfg, source_path)
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
"""Smoke tests for config loader (src/config.py). Needs python-dotenv only."""
|
||||
|
||||
from src.config import load_config
|
||||
|
||||
_KEYS = [
|
||||
"LOCAL_RTSP", "JETSON_RTSP", "MODEL_SACK_PATH", "MODEL_TRUCK_PATH",
|
||||
"COUNTING_LINE_Y", "COUNTING_LINE_X_START", "COUNTING_LINE_X_END",
|
||||
"SACK_CONF_THRESHOLD", "TRUCK_CONF_THRESHOLD", "BATCH_TIMEOUT_SECONDS",
|
||||
"CSV_OUTPUT_DIR", "DATA_SEED",
|
||||
]
|
||||
|
||||
|
||||
def test_load_from_env_file(tmp_path, monkeypatch):
|
||||
for k in _KEYS:
|
||||
monkeypatch.delenv(k, raising=False)
|
||||
env = tmp_path / "test.env"
|
||||
env.write_text(
|
||||
"LOCAL_RTSP=rtsp://cam/1\n"
|
||||
"MODEL_SACK_PATH=/m/sack.pt\n"
|
||||
"MODEL_TRUCK_PATH=/m/truck.pt\n"
|
||||
"SACK_CONF_THRESHOLD=0.55\n"
|
||||
)
|
||||
cfg = load_config(str(env))
|
||||
assert cfg.local_rtsp == "rtsp://cam/1"
|
||||
assert cfg.sack_model_path == "/m/sack.pt"
|
||||
assert cfg.truck_model_path == "/m/truck.pt"
|
||||
assert cfg.sack_conf == 0.55
|
||||
assert cfg.truck_conf == 0.50 # default preserved
|
||||
@@ -39,6 +39,25 @@ def test_repo_config_loads_and_validates(repo_config_path):
|
||||
assert dp.min_bbox_area >= 0
|
||||
|
||||
|
||||
def test_counting_knobs_match_production_defaults(repo_config_path):
|
||||
"""All live counting knobs in predict.py must come from config.yaml."""
|
||||
cfg = load_config(repo_config_path)
|
||||
c = cfg.counting
|
||||
assert c.confirm_delay_sec == 0.5
|
||||
assert c.exit_confirm_delay_sec == 6.0
|
||||
assert c.entry_overlap_threshold == 0.20
|
||||
assert c.exit_overlap_threshold == 0.05
|
||||
assert c.camera_noise_deadband == 50
|
||||
assert c.duplicate_circle_radius == 30
|
||||
assert c.min_valid_area == 15000
|
||||
assert c.max_reid_transit_distance == 400
|
||||
assert c.circle_stay_timeout_sec == 10.0
|
||||
assert c.jarak_toleransi_duplikat == 30
|
||||
assert c.tolerance_missing_frames == 1200
|
||||
assert c.max_reid_frames == 200
|
||||
assert c.debounce_frames == 8
|
||||
|
||||
|
||||
def test_mode_preset_only_filters_and_engines(repo_config_path):
|
||||
"""Modes must not carry per-class conf/iou — those live in detection_params."""
|
||||
with open(repo_config_path) as f:
|
||||
|
||||
Reference in new issue
Block a user