Files
yovision/Brain/src/yovision_brain/rules/models.py
T

85 lines
2.6 KiB
Python

"""Versioned Brain-internal rule configuration and decisions."""
from __future__ import annotations
from dataclasses import dataclass
class RuleConfigError(ValueError):
pass
@dataclass(frozen=True, slots=True)
class NormalizedPoint:
x: float
y: float
def __post_init__(self) -> None:
if not 0.0 <= self.x <= 1.0 or not 0.0 <= self.y <= 1.0:
raise RuleConfigError("rule coordinates must be normalized to 0..1")
@dataclass(frozen=True, slots=True)
class AreaDefinition:
rule_id: str
points: tuple[NormalizedPoint, ...]
@dataclass(frozen=True, slots=True)
class DirectionalLineDefinition:
rule_id: str
start: NormalizedPoint
end: NormalizedPoint
trigger_direction: str
deadband: float = 0.005
@dataclass(frozen=True, slots=True)
class RuleSet:
version: str
profile_id: str
width: int
height: int
areas: tuple[AreaDefinition, ...] = ()
directional_lines: tuple[DirectionalLineDefinition, ...] = ()
def __post_init__(self) -> None:
if not self.version or not self.profile_id or self.width <= 0 or self.height <= 0:
raise RuleConfigError("rule version, profile and dimensions are required")
identifiers = [rule.rule_id for rule in self.areas] + [rule.rule_id for rule in self.directional_lines]
if any(not identifier for identifier in identifiers) or len(set(identifiers)) != len(identifiers):
raise RuleConfigError("rule ids must be non-empty and unique")
for area in self.areas:
if len(area.points) < 3 or abs(_polygon_area(area.points)) < 1e-9:
raise RuleConfigError(f"area {area.rule_id!r} must be a non-degenerate polygon")
for line in self.directional_lines:
if line.start == line.end:
raise RuleConfigError(f"line {line.rule_id!r} must have distinct endpoints")
if line.trigger_direction not in {"left_to_right", "right_to_left"}:
raise RuleConfigError(f"line {line.rule_id!r} has invalid trigger direction")
if not 0.0 <= line.deadband < 0.5:
raise RuleConfigError(f"line {line.rule_id!r} has invalid deadband")
def _polygon_area(points: tuple[NormalizedPoint, ...]) -> float:
return sum(
first.x * second.y - second.x * first.y
for first, second in zip(points, points[1:] + points[:1])
) / 2.0
@dataclass(frozen=True, slots=True)
class RuleDecision:
rule_id: str
rule_type: str
track_id: str
state: str
triggered: bool
reason: str
config_version: str
profile_id: str
width: int
height: int
anchor: NormalizedPoint
timestamp_ns: int