mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-18 09:04:28 +03:00
implement speed_threshold for zone presence
This commit is contained in:
parent
55effc2f8e
commit
d12298d9e3
@ -1,13 +1,16 @@
|
||||
# this uses the base model because the color is an extra attribute
|
||||
import logging
|
||||
from typing import Optional, Union
|
||||
|
||||
import numpy as np
|
||||
from pydantic import BaseModel, Field, PrivateAttr, field_validator
|
||||
from pydantic import BaseModel, Field, PrivateAttr, field_validator, model_validator
|
||||
|
||||
from .objects import FilterConfig
|
||||
|
||||
__all__ = ["ZoneConfig"]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ZoneConfig(BaseModel):
|
||||
filters: dict[str, FilterConfig] = Field(
|
||||
@ -30,6 +33,11 @@ class ZoneConfig(BaseModel):
|
||||
ge=0,
|
||||
title="Number of seconds that an object must loiter to be considered in the zone.",
|
||||
)
|
||||
speed_threshold: Optional[float] = Field(
|
||||
default=None,
|
||||
ge=0.1,
|
||||
title="Minimum speed value for an object to be considered in the zone.",
|
||||
)
|
||||
objects: Union[str, list[str]] = Field(
|
||||
default_factory=list,
|
||||
title="List of objects that can trigger the zone.",
|
||||
@ -71,6 +79,16 @@ class ZoneConfig(BaseModel):
|
||||
|
||||
return distances
|
||||
|
||||
@model_validator(mode="after")
|
||||
def check_loitering_time_constraints(self):
|
||||
if self.loitering_time > 0 and (
|
||||
self.speed_threshold is not None or len(self.distances) > 0
|
||||
):
|
||||
logger.warning(
|
||||
"loitering_time should not be set on a zone if speed_threshold or distances is set."
|
||||
)
|
||||
return self
|
||||
|
||||
def __init__(self, **config):
|
||||
super().__init__(**config)
|
||||
|
||||
@ -107,7 +125,7 @@ class ZoneConfig(BaseModel):
|
||||
if explicit:
|
||||
self.coordinates = ",".join(
|
||||
[
|
||||
f'{round(int(p.split(",")[0]) / frame_shape[1], 3)},{round(int(p.split(",")[1]) / frame_shape[0], 3)}'
|
||||
f"{round(int(p.split(',')[0]) / frame_shape[1], 3)},{round(int(p.split(',')[1]) / frame_shape[0], 3)}"
|
||||
for p in coordinates
|
||||
]
|
||||
)
|
||||
|
||||
@ -146,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():
|
||||
@ -154,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
|
||||
@ -184,44 +239,8 @@ class TrackedObject:
|
||||
if 0 < zone_score < zone.inertia:
|
||||
self.zone_presence[name] = zone_score - 1
|
||||
|
||||
# update speed
|
||||
if (
|
||||
zone.distances
|
||||
and name in self.current_zones
|
||||
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.active
|
||||
else (0, 0)
|
||||
)
|
||||
if self.ui_config.unit_system == "metric":
|
||||
# Convert m/s to km/h
|
||||
self.current_estimated_speed = speed_magnitude * 3.6
|
||||
elif self.ui_config.unit_system == "imperial":
|
||||
# Convert ft/s to mph
|
||||
self.current_estimated_speed = speed_magnitude * 0.681818
|
||||
|
||||
# only keep the last 10 speeds
|
||||
if len(self.speed_history) > 10:
|
||||
self.speed_history = self.speed_history[-10:]
|
||||
|
||||
self.speed_history.append(self.current_estimated_speed)
|
||||
self.average_estimated_speed = sum(self.speed_history) / len(
|
||||
self.speed_history
|
||||
)
|
||||
logger.debug(
|
||||
f"Camera: {self.camera_config.name}, tracked object ID: {self.obj_data['id']}, zone: {name}, pixel velocity: {str(tuple(np.round(self.obj_data['estimate_velocity']).flatten().astype(int)))}, speed magnitude: {speed_magnitude}, velocity angle: {self.velocity_angle}, estimated speed: {self.current_estimated_speed:.1f}, average speed: {self.average_estimated_speed:.1f}, length: {len(self.speed_history)}"
|
||||
)
|
||||
else:
|
||||
# ensure we do not display an estimate if the object is not currently in a speed tracking zone
|
||||
# Reset speed if not in speed zone
|
||||
if zone.distances and name not in current_zones:
|
||||
self.current_estimated_speed = 0
|
||||
|
||||
# update loitering status
|
||||
@ -392,7 +411,7 @@ 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
|
||||
|
||||
@ -181,6 +181,13 @@ export default function ZoneEditPane({
|
||||
})
|
||||
.optional()
|
||||
.or(z.literal("")),
|
||||
speed_threshold: z.coerce
|
||||
.number()
|
||||
.min(0.1, {
|
||||
message: "Speed threshold must be greater than or equal to 0.1",
|
||||
})
|
||||
.optional()
|
||||
.or(z.literal("")),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
@ -193,6 +200,21 @@ export default function ZoneEditPane({
|
||||
message: "All distance fields must be filled to use speed estimation.",
|
||||
path: ["speedEstimation"],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
// Prevent speed estimation when loitering_time is greater than 0
|
||||
return !(
|
||||
data.speedEstimation &&
|
||||
data.loitering_time &&
|
||||
data.loitering_time > 0
|
||||
);
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Zones with loitering times greater than 0 should not be used with speed estimation.",
|
||||
path: ["loitering_time"],
|
||||
},
|
||||
);
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
@ -215,6 +237,10 @@ export default function ZoneEditPane({
|
||||
lineB,
|
||||
lineC,
|
||||
lineD,
|
||||
speed_threshold:
|
||||
polygon?.camera &&
|
||||
polygon?.name &&
|
||||
config?.cameras[polygon.camera]?.zones[polygon.name]?.speed_threshold,
|
||||
},
|
||||
});
|
||||
|
||||
@ -243,6 +269,7 @@ export default function ZoneEditPane({
|
||||
lineB,
|
||||
lineC,
|
||||
lineD,
|
||||
speed_threshold,
|
||||
}: ZoneFormValuesType, // values submitted via the form
|
||||
objects: string[],
|
||||
) => {
|
||||
@ -349,9 +376,22 @@ export default function ZoneEditPane({
|
||||
}
|
||||
}
|
||||
|
||||
let speedThresholdQuery = "";
|
||||
if (speed_threshold >= 0 && speedEstimation) {
|
||||
speedThresholdQuery = `&cameras.${polygon?.camera}.zones.${zoneName}.speed_threshold=${speed_threshold}`;
|
||||
} else {
|
||||
if (
|
||||
polygon?.camera &&
|
||||
polygon?.name &&
|
||||
config?.cameras[polygon.camera]?.zones[polygon.name]?.speed_threshold
|
||||
) {
|
||||
speedThresholdQuery = `&cameras.${polygon?.camera}.zones.${zoneName}.speed_threshold`;
|
||||
}
|
||||
}
|
||||
|
||||
axios
|
||||
.put(
|
||||
`config/set?cameras.${polygon?.camera}.zones.${zoneName}.coordinates=${coordinates}${inertiaQuery}${loiteringTimeQuery}${distancesQuery}${objectQueries}${alertQueries}${detectionQueries}`,
|
||||
`config/set?cameras.${polygon?.camera}.zones.${zoneName}.coordinates=${coordinates}${inertiaQuery}${loiteringTimeQuery}${speedThresholdQuery}${distancesQuery}${objectQueries}${alertQueries}${detectionQueries}`,
|
||||
{ requires_restart: 0 },
|
||||
)
|
||||
.then((res) => {
|
||||
@ -573,6 +613,14 @@ export default function ZoneEditPane({
|
||||
);
|
||||
return;
|
||||
}
|
||||
const loiteringTime =
|
||||
form.getValues("loitering_time");
|
||||
|
||||
if (checked && loiteringTime && loiteringTime > 0) {
|
||||
toast.error(
|
||||
"Zones with loitering times greater than 0 should not be used with speed estimation.",
|
||||
);
|
||||
}
|
||||
field.onChange(checked);
|
||||
}}
|
||||
/>
|
||||
@ -607,6 +655,7 @@ export default function ZoneEditPane({
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||
{...field}
|
||||
onFocus={() => setActiveLine(1)}
|
||||
onBlur={() => setActiveLine(undefined)}
|
||||
@ -629,6 +678,7 @@ export default function ZoneEditPane({
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||
{...field}
|
||||
onFocus={() => setActiveLine(2)}
|
||||
onBlur={() => setActiveLine(undefined)}
|
||||
@ -651,6 +701,7 @@ export default function ZoneEditPane({
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||
{...field}
|
||||
onFocus={() => setActiveLine(3)}
|
||||
onBlur={() => setActiveLine(undefined)}
|
||||
@ -673,6 +724,7 @@ export default function ZoneEditPane({
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||
{...field}
|
||||
onFocus={() => setActiveLine(4)}
|
||||
onBlur={() => setActiveLine(undefined)}
|
||||
@ -681,6 +733,31 @@ export default function ZoneEditPane({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="speed_threshold"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Speed Threshold (
|
||||
{config?.ui.unit_system == "imperial" ? "mph" : "kph"})
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Specifies a minimum speed for objects to be considered
|
||||
in this zone.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@ -24,6 +24,7 @@ export type ZoneFormValuesType = {
|
||||
lineB: number;
|
||||
lineC: number;
|
||||
lineD: number;
|
||||
speed_threshold: number;
|
||||
};
|
||||
|
||||
export type ObjectMaskFormValuesType = {
|
||||
|
||||
@ -219,6 +219,7 @@ export interface CameraConfig {
|
||||
filters: Record<string, unknown>;
|
||||
inertia: number;
|
||||
loitering_time: number;
|
||||
speed_threshold: number;
|
||||
objects: string[];
|
||||
color: number[];
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user