feat: zone config loader and static ROI builder
This commit is contained in:
1 parent
5bf44d6457
commit
4ad4284d9a
3 files changed
+134
No files matched your search
@@ -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)
|
||||||
@@ -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
@@ -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"
|
||||||
|
}
|
||||||
Reference in new issue
Block a user