feat: zone config loader and static ROI builder

This commit is contained in:
jetson committed 2026-09-22 12:57:36 +07:00
1 parent 5bf44d6457
commit 4ad4284d9a
3 files changed
+134

No files matched your search

+73
View File
@@ -0,0 +1,73 @@
"""Zone configuration — load/save counting zone presets from zones.json."""
from __future__ import annotations
import json
import os
from typing import Any
from src.truck_roi import TruckROI
def load_zone_config(path: str) -> dict[str, Any]:
"""Load zones.json and return full config dict."""
with open(path) as f:
return json.load(f)
def save_zone_config(config: dict[str, Any], path: str) -> None:
"""Save zone config dict to zones.json."""
with open(path, "w") as f:
json.dump(config, f, indent=2)
def build_static_roi(zone: dict[str, Any], video_w: int, video_h: int) -> TruckROI:
"""Build a TruckROI from a zone config dict.
The zone dict should have:
- truck_area: [[x1,y1], [x2,y2], [x3,y3], [x4,y4]] (4-point polygon)
- count_line: {"x_start": int, "x_end": int, "y": int}
Coordinates are in the video's native resolution (no scaling needed
if the zone was calibrated for this resolution).
"""
truck_pts = zone.get("truck_area", [])
count_line = zone.get("count_line", {})
# Compute bounding box of truck_area polygon
if truck_pts and len(truck_pts) >= 4:
xs = [p[0] for p in truck_pts]
ys = [p[1] for p in truck_pts]
x1 = max(0, min(xs))
y1 = max(0, min(ys))
x2 = min(video_w, max(xs))
y2 = min(video_h, max(ys))
else:
# Fallback: use 50% of frame
x1, y1 = int(video_w * 0.3), int(video_h * 0.1)
x2, y2 = int(video_w * 0.75), int(video_h * 0.95)
line_y = count_line.get("y", int((y1 + y2) / 2))
return TruckROI(
x1=x1, y1=y1, x2=x2, y2=y2,
line_y=line_y,
confidence=1.0,
)
def list_zone_presets(path: str) -> list[dict[str, Any]]:
"""Return list of preset summaries: [{"name": str, "description": str, ...}]."""
config = load_zone_config(path)
presets = config.get("presets", {})
return [
{"name": name, "description": vals.get("description", ""), **vals}
for name, vals in presets.items()
]
def get_active_zone(path: str, preset_name: str | None = None) -> dict[str, Any] | None:
"""Get the active preset dict. Falls back to active_preset field if name not given."""
config = load_zone_config(path)
presets = config.get("presets", {})
name = preset_name or config.get("active_preset")
return presets.get(name)
+47
View File
@@ -0,0 +1,47 @@
import json
import os
import pytest
from src.zone_config import load_zone_config, build_static_roi, list_zone_presets
def test_load_zone_config_reads_json(tmp_path):
cfg = {"presets": {"test": {"count_line": {"x_start": 100, "x_end": 200, "y": 150}}}, "active_preset": "test"}
p = tmp_path / "zones.json"
p.write_text(json.dumps(cfg))
result = load_zone_config(str(p))
assert result["active_preset"] == "test"
assert result["presets"]["test"]["count_line"]["y"] == 150
def test_build_static_roi_from_zone_config():
zone = {
"truck_area": [[100, 50], [110, 300], [400, 310], [390, 40]],
"count_line": {"x_start": 110, "x_end": 400, "y": 150},
}
roi = build_static_roi(zone, video_w=640, video_h=480)
assert roi.line_y == 150
assert roi.confidence == 1.0
assert roi.x1 < roi.x2
assert roi.y1 < roi.y2
def test_build_static_roi_scales_coords():
zone = {
"truck_area": [[600, 385], [609, 1076], [1404, 1078], [1381, 343]],
"count_line": {"x_start": 577, "x_end": 1401, "y": 330},
}
roi = build_static_roi(zone, video_w=1920, video_h=1080)
assert roi.line_y == 330
assert roi.x1 == 600
assert roi.x2 == 1404
def test_list_zone_presets(tmp_path):
cfg = {"presets": {"p1": {"description": "Zone 1"}, "p2": {"description": "Zone 2"}}}
p = tmp_path / "zones.json"
p.write_text(json.dumps(cfg))
presets = list_zone_presets(str(p))
assert len(presets) == 2
names = [p["name"] for p in presets]
assert "p1" in names
assert "p2" in names
+14
View File
@@ -0,0 +1,14 @@
{
"presets": {
"default": {
"truck_area": [[600, 385], [609, 1076], [1404, 1078], [1381, 343]],
"counting_area": [[574, 50], [586, 1077], [1418, 1076], [1397, 50]],
"count_line": {"x_start": 577, "x_end": 1401, "y": 330},
"left_limit": 0.27578,
"right_limit": 0.72578,
"duplicate_circle_radius": 30,
"description": "Default zone for 1920x1080 camera"
}
},
"active_preset": "default"
}