Estimated object speed for zones (#16452)

* utility functions

* backend config

* backend object speed tracking

* draw speed on debug view

* basic frontend zone editor

* remove line sorting

* fix types

* highlight line on canvas when entering value in zone edit pane

* rename vars and add validation

* ensure speed estimation is disabled when user adds more than 4 points

* pixel velocity in debug

* unit_system in config

* ability to define unit system in config

* save max speed to db

* frontend

* docs

* clarify docs

* utility functions

* backend config

* backend object speed tracking

* draw speed on debug view

* basic frontend zone editor

* remove line sorting

* fix types

* highlight line on canvas when entering value in zone edit pane

* rename vars and add validation

* ensure speed estimation is disabled when user adds more than 4 points

* pixel velocity in debug

* unit_system in config

* ability to define unit system in config

* save max speed to db

* frontend

* docs

* clarify docs

* fix duplicates from merge

* include max_estimated_speed in api responses

* add units to zone edit pane

* catch undefined

* add average speed

* clarify docs

* only track average speed when object is active

* rename vars

* ensure points and distances are ordered clockwise

* only store the last 10 speeds like score history

* remove max estimated speed

* update docs

* update docs

* fix point ordering

* improve readability

* docs inertia recommendation

* fix point ordering

* check object frame time

* add velocity angle to frontend

* docs clarity

* add frontend speed filter

* fix mqtt docs

* fix mqtt docs

* don't try to remove distances if they weren't already defined

* don't display estimates on debug view/snapshots if object is not in a speed tracking zone

* docs

* implement speed_threshold for zone presence

* docs for threshold

* better ground plane image

* improve image zone size

* add inertia to speed threshold example
This commit is contained in:
Josh Hawkins
2025-02-10 13:23:42 -07:00
committed by GitHub
parent dd7820e4ee
commit 72209986b6
25 changed files with 1030 additions and 79 deletions
+79 -2
View File
@@ -12,6 +12,7 @@ import numpy as np
from frigate.config import (
CameraConfig,
ModelConfig,
UIConfig,
)
from frigate.review.types import SeverityEnum
from frigate.util.image import (
@@ -22,6 +23,7 @@ from frigate.util.image import (
is_better_thumbnail,
)
from frigate.util.object import box_inside
from frigate.util.velocity import calculate_real_world_speed
logger = logging.getLogger(__name__)
@@ -31,6 +33,7 @@ class TrackedObject:
self,
model_config: ModelConfig,
camera_config: CameraConfig,
ui_config: UIConfig,
frame_cache,
obj_data: dict[str, any],
):
@@ -42,6 +45,7 @@ class TrackedObject:
self.colormap = model_config.colormap
self.logos = model_config.all_attribute_logos
self.camera_config = camera_config
self.ui_config = ui_config
self.frame_cache = frame_cache
self.zone_presence: dict[str, int] = {}
self.zone_loitering: dict[str, int] = {}
@@ -58,6 +62,10 @@ class TrackedObject:
self.frame = None
self.active = True
self.pending_loitering = False
self.speed_history = []
self.current_estimated_speed = 0
self.average_estimated_speed = 0
self.velocity_angle = 0
self.previous = self.to_dict()
@property
@@ -129,6 +137,8 @@ class TrackedObject:
"region": obj_data["region"],
"score": obj_data["score"],
"attributes": obj_data["attributes"],
"current_estimated_speed": self.current_estimated_speed,
"velocity_angle": self.velocity_angle,
}
thumb_update = True
@@ -136,6 +146,7 @@ class TrackedObject:
current_zones = []
bottom_center = (obj_data["centroid"][0], obj_data["box"][3])
in_loitering_zone = False
in_speed_zone = False
# check each zone
for name, zone in self.camera_config.zones.items():
@@ -144,12 +155,66 @@ class TrackedObject:
continue
contour = zone.contour
zone_score = self.zone_presence.get(name, 0) + 1
# check if the object is in the zone
if cv2.pointPolygonTest(contour, bottom_center, False) >= 0:
# if the object passed the filters once, dont apply again
if name in self.current_zones or not zone_filtered(self, zone.filters):
# an object is only considered present in a zone if it has a zone inertia of 3+
# Calculate speed first if this is a speed zone
if (
zone.distances
and obj_data["frame_time"] == current_frame_time
and self.active
):
speed_magnitude, self.velocity_angle = (
calculate_real_world_speed(
zone.contour,
zone.distances,
self.obj_data["estimate_velocity"],
bottom_center,
self.camera_config.detect.fps,
)
)
if self.ui_config.unit_system == "metric":
self.current_estimated_speed = (
speed_magnitude * 3.6
) # m/s to km/h
else:
self.current_estimated_speed = (
speed_magnitude * 0.681818
) # ft/s to mph
self.speed_history.append(self.current_estimated_speed)
if len(self.speed_history) > 10:
self.speed_history = self.speed_history[-10:]
self.average_estimated_speed = sum(self.speed_history) / len(
self.speed_history
)
# we've exceeded the speed threshold on the zone
# or we don't have a speed threshold set
if (
zone.speed_threshold is None
or self.average_estimated_speed > zone.speed_threshold
):
in_speed_zone = True
logger.debug(
f"Camera: {self.camera_config.name}, tracked object ID: {self.obj_data['id']}, "
f"zone: {name}, pixel velocity: {str(tuple(np.round(self.obj_data['estimate_velocity']).flatten().astype(int)))}, "
f"speed magnitude: {speed_magnitude}, velocity angle: {self.velocity_angle}, "
f"estimated speed: {self.current_estimated_speed:.1f}, "
f"average speed: {self.average_estimated_speed:.1f}, "
f"length: {len(self.speed_history)}"
)
# Check zone entry conditions - for speed zones, require both inertia and speed
if zone_score >= zone.inertia:
if zone.distances and not in_speed_zone:
continue # Skip zone entry for speed zones until speed threshold met
# if the zone has loitering time, update loitering status
if zone.loitering_time > 0:
in_loitering_zone = True
@@ -174,6 +239,10 @@ class TrackedObject:
if 0 < zone_score < zone.inertia:
self.zone_presence[name] = zone_score - 1
# Reset speed if not in speed zone
if zone.distances and name not in current_zones:
self.current_estimated_speed = 0
# update loitering status
self.pending_loitering = in_loitering_zone
@@ -255,6 +324,9 @@ class TrackedObject:
"current_attributes": self.obj_data["attributes"],
"pending_loitering": self.pending_loitering,
"max_severity": self.max_severity,
"current_estimated_speed": self.current_estimated_speed,
"average_estimated_speed": self.average_estimated_speed,
"velocity_angle": self.velocity_angle,
}
if include_thumbnail:
@@ -339,7 +411,12 @@ class TrackedObject:
box[2],
box[3],
self.obj_data["label"],
f"{int(self.thumbnail_data['score'] * 100)}% {int(self.thumbnail_data['area'])}",
f"{int(self.thumbnail_data['score'] * 100)}% {int(self.thumbnail_data['area'])}"
+ (
f" {self.thumbnail_data['current_estimated_speed']:.1f}"
if self.thumbnail_data["current_estimated_speed"] != 0
else ""
),
thickness=thickness,
color=color,
)