| from __future__ import annotations |
|
|
| from dataclasses import dataclass |
|
|
|
|
| @dataclass |
| class ZoneEvent: |
| track_id: int |
| label: int |
| kind: str |
| frame_index: int |
| position: tuple[float, float] |
|
|
|
|
| def point_in_polygon(point: tuple[float, float], polygon: list[tuple[float, float]]) -> bool: |
| x, y = point |
| inside = False |
| n = len(polygon) |
| j = n - 1 |
| for i in range(n): |
| xi, yi = polygon[i] |
| xj, yj = polygon[j] |
| if (yi > y) != (yj > y) and x < (xj - xi) * (y - yi) / (yj - yi) + xi: |
| inside = not inside |
| j = i |
| return inside |
|
|
|
|
| class RestrictedZoneMonitor: |
| """Fires an event when a tracked object's ground position crosses a zone boundary. |
| |
| Consumes `Track` objects from `SortTracker.update()`. The zone is a polygon in |
| the same pixel coordinates as the tracked boxes (a rectangle is just a |
| four-point polygon). Uses each box's bottom-center point rather than its |
| centroid, since a zone drawn on a floor plane should key off where someone is |
| standing, not their torso. Only reports the entered/exited transition, not |
| every frame a track spends inside the zone. |
| """ |
|
|
| def __init__(self, polygon: list[tuple[float, float]]) -> None: |
| self.polygon = polygon |
| self._inside_state: dict[int, bool] = {} |
|
|
| def update(self, tracks, frame_index: int = 0) -> list[ZoneEvent]: |
| events: list[ZoneEvent] = [] |
| live_ids = set() |
| for track in tracks: |
| live_ids.add(track.id) |
| x0, y0, x1, y1 = track.box |
| ground_point = ((x0 + x1) / 2.0, y1) |
| is_inside = point_in_polygon(ground_point, self.polygon) |
| was_inside = self._inside_state.get(track.id, False) |
| if is_inside and not was_inside: |
| events.append(ZoneEvent(track.id, track.label, "entered", frame_index, ground_point)) |
| elif was_inside and not is_inside: |
| events.append(ZoneEvent(track.id, track.label, "exited", frame_index, ground_point)) |
| self._inside_state[track.id] = is_inside |
|
|
| for track_id in list(self._inside_state): |
| if track_id not in live_ids: |
| del self._inside_state[track_id] |
|
|
| return events |
|
|