File size: 5,283 Bytes
6c65498
d12d00d
6c65498
 
 
 
 
1f3bac1
6c65498
d12d00d
6c65498
d12d00d
 
6c65498
 
5d257ae
d12d00d
 
 
 
 
5d257ae
 
 
6c65498
 
d12d00d
6c65498
 
d12d00d
 
6c65498
1f3bac1
6c65498
 
 
 
 
 
 
1f3bac1
6c65498
 
 
 
 
46f8ebc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d12d00d
6c65498
 
d12d00d
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
"""
Pydantic models for per-frame detection.

These models represent the results of detecting various elements in video frames:
scorebug presence and timeout indicators.
"""

from typing import Any, Optional, Tuple, List

from pydantic import BaseModel, Field


class ScorebugDetection(BaseModel):
    """Results from scorebug detection."""

    detected: bool = Field(..., description="Whether scorebug was detected (for play tracking)")
    confidence: float = Field(..., description="Confidence score (0.0 to 1.0)")
    bbox: Optional[Tuple[int, int, int, int]] = Field(None, description="Bounding box (x, y, width, height)")
    method: str = Field("unknown", description="Detection method used")
    left_confidence: Optional[float] = Field(None, description="Left half confidence (when split detection enabled)")
    right_confidence: Optional[float] = Field(None, description="Right half confidence (when split detection enabled)")
    # In fixed coords mode, detected=True (assumed present for play tracking),
    # but template_matched indicates actual visibility (for special play end detection)
    template_matched: Optional[bool] = Field(None, description="Actual template match result (None if not using fixed coords mode)")


class TimeoutRegionConfig(BaseModel):
    """Configuration for a team's timeout indicator region."""

    team_name: str = Field(..., description="'home' or 'away'")
    bbox: Tuple[int, int, int, int] = Field(..., description="x, y, width, height for the 3-oval group")

    def to_dict(self) -> dict[str, object]:
        """Convert to dictionary for JSON serialization."""
        return {
            "team_name": self.team_name,
            "bbox": {"x": self.bbox[0], "y": self.bbox[1], "width": self.bbox[2], "height": self.bbox[3]},
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "TimeoutRegionConfig":
        """Create from dictionary."""
        bbox = (data["bbox"]["x"], data["bbox"]["y"], data["bbox"]["width"], data["bbox"]["height"])
        return cls(team_name=data["team_name"], bbox=bbox)


class OvalLocation(BaseModel):
    """Precise location of a single timeout oval within a team's timeout region."""

    x: int = Field(..., description="X position within timeout region (relative to region top-left)")
    y: int = Field(..., description="Y position within timeout region (relative to region top-left)")
    width: int = Field(..., description="Width of the oval bounding box")
    height: int = Field(..., description="Height of the oval bounding box")
    baseline_brightness: float = Field(0.0, description="Mean brightness when oval is 'available' (calibrated)")

    def to_dict(self) -> dict[str, object]:
        """Convert to dictionary for JSON serialization."""
        return {
            "x": self.x,
            "y": self.y,
            "width": self.width,
            "height": self.height,
            "baseline_brightness": self.baseline_brightness,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "OvalLocation":
        """Create from dictionary."""
        return cls(
            x=data["x"],
            y=data["y"],
            width=data["width"],
            height=data["height"],
            baseline_brightness=data.get("baseline_brightness", 0.0),
        )


class CalibratedTimeoutRegion(BaseModel):
    """Timeout region with discovered oval sub-coordinates from calibration."""

    team_name: str = Field(..., description="'home' or 'away'")
    bbox: Tuple[int, int, int, int] = Field(..., description="Overall region: x, y, width, height")
    ovals: List[OvalLocation] = Field(default_factory=list, description="3 oval sub-coordinates within the region")
    calibration_timestamp: float = Field(0.0, description="Video timestamp when calibration was performed")

    def to_dict(self) -> dict[str, object]:
        """Convert to dictionary for JSON serialization."""
        return {
            "team_name": self.team_name,
            "bbox": {"x": self.bbox[0], "y": self.bbox[1], "width": self.bbox[2], "height": self.bbox[3]},
            "ovals": [oval.to_dict() for oval in self.ovals],
            "calibration_timestamp": self.calibration_timestamp,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "CalibratedTimeoutRegion":
        """Create from dictionary."""
        bbox = (data["bbox"]["x"], data["bbox"]["y"], data["bbox"]["width"], data["bbox"]["height"])
        ovals = [OvalLocation.from_dict(o) for o in data.get("ovals", [])]
        return cls(
            team_name=data["team_name"],
            bbox=bbox,
            ovals=ovals,
            calibration_timestamp=data.get("calibration_timestamp", 0.0),
        )


class TimeoutReading(BaseModel):
    """Results from timeout indicator reading."""

    home_timeouts: int = Field(..., description="0-3 timeouts remaining")
    away_timeouts: int = Field(..., description="0-3 timeouts remaining")
    confidence: float = Field(..., description="Overall confidence in reading")
    home_oval_states: Optional[List[bool]] = Field(None, description="True = white (available), False = dark (used)")
    away_oval_states: Optional[List[bool]] = Field(None, description="True = white (available), False = dark (used)")