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
|
# this uses the base model because the color is an extra attribute
|
||||||
|
import logging
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
|
|
||||||
import numpy as np
|
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
|
from .objects import FilterConfig
|
||||||
|
|
||||||
__all__ = ["ZoneConfig"]
|
__all__ = ["ZoneConfig"]
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ZoneConfig(BaseModel):
|
class ZoneConfig(BaseModel):
|
||||||
filters: dict[str, FilterConfig] = Field(
|
filters: dict[str, FilterConfig] = Field(
|
||||||
@ -30,6 +33,11 @@ class ZoneConfig(BaseModel):
|
|||||||
ge=0,
|
ge=0,
|
||||||
title="Number of seconds that an object must loiter to be considered in the zone.",
|
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(
|
objects: Union[str, list[str]] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
title="List of objects that can trigger the zone.",
|
title="List of objects that can trigger the zone.",
|
||||||
@ -71,6 +79,16 @@ class ZoneConfig(BaseModel):
|
|||||||
|
|
||||||
return distances
|
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):
|
def __init__(self, **config):
|
||||||
super().__init__(**config)
|
super().__init__(**config)
|
||||||
|
|
||||||
@ -107,7 +125,7 @@ class ZoneConfig(BaseModel):
|
|||||||
if explicit:
|
if explicit:
|
||||||
self.coordinates = ",".join(
|
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
|
for p in coordinates
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@ -146,6 +146,7 @@ class TrackedObject:
|
|||||||
current_zones = []
|
current_zones = []
|
||||||
bottom_center = (obj_data["centroid"][0], obj_data["box"][3])
|
bottom_center = (obj_data["centroid"][0], obj_data["box"][3])
|
||||||
in_loitering_zone = False
|
in_loitering_zone = False
|
||||||
|
in_speed_zone = False
|
||||||
|
|
||||||
# check each zone
|
# check each zone
|
||||||
for name, zone in self.camera_config.zones.items():
|
for name, zone in self.camera_config.zones.items():
|
||||||
@ -154,12 +155,66 @@ class TrackedObject:
|
|||||||
continue
|
continue
|
||||||
contour = zone.contour
|
contour = zone.contour
|
||||||
zone_score = self.zone_presence.get(name, 0) + 1
|
zone_score = self.zone_presence.get(name, 0) + 1
|
||||||
|
|
||||||
# check if the object is in the zone
|
# check if the object is in the zone
|
||||||
if cv2.pointPolygonTest(contour, bottom_center, False) >= 0:
|
if cv2.pointPolygonTest(contour, bottom_center, False) >= 0:
|
||||||
# if the object passed the filters once, dont apply again
|
# if the object passed the filters once, dont apply again
|
||||||
if name in self.current_zones or not zone_filtered(self, zone.filters):
|
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_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 the zone has loitering time, update loitering status
|
||||||
if zone.loitering_time > 0:
|
if zone.loitering_time > 0:
|
||||||
in_loitering_zone = True
|
in_loitering_zone = True
|
||||||
@ -184,44 +239,8 @@ class TrackedObject:
|
|||||||
if 0 < zone_score < zone.inertia:
|
if 0 < zone_score < zone.inertia:
|
||||||
self.zone_presence[name] = zone_score - 1
|
self.zone_presence[name] = zone_score - 1
|
||||||
|
|
||||||
# update speed
|
# Reset speed if not in speed zone
|
||||||
if (
|
if zone.distances and name not in current_zones:
|
||||||
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
|
|
||||||
self.current_estimated_speed = 0
|
self.current_estimated_speed = 0
|
||||||
|
|
||||||
# update loitering status
|
# update loitering status
|
||||||
@ -392,7 +411,7 @@ class TrackedObject:
|
|||||||
box[2],
|
box[2],
|
||||||
box[3],
|
box[3],
|
||||||
self.obj_data["label"],
|
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}"
|
f" {self.thumbnail_data['current_estimated_speed']:.1f}"
|
||||||
if self.thumbnail_data["current_estimated_speed"] != 0
|
if self.thumbnail_data["current_estimated_speed"] != 0
|
||||||
|
|||||||
@ -181,6 +181,13 @@ export default function ZoneEditPane({
|
|||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.or(z.literal("")),
|
.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(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
@ -193,6 +200,21 @@ export default function ZoneEditPane({
|
|||||||
message: "All distance fields must be filled to use speed estimation.",
|
message: "All distance fields must be filled to use speed estimation.",
|
||||||
path: ["speedEstimation"],
|
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>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
@ -215,6 +237,10 @@ export default function ZoneEditPane({
|
|||||||
lineB,
|
lineB,
|
||||||
lineC,
|
lineC,
|
||||||
lineD,
|
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,
|
lineB,
|
||||||
lineC,
|
lineC,
|
||||||
lineD,
|
lineD,
|
||||||
|
speed_threshold,
|
||||||
}: ZoneFormValuesType, // values submitted via the form
|
}: ZoneFormValuesType, // values submitted via the form
|
||||||
objects: string[],
|
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
|
axios
|
||||||
.put(
|
.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 },
|
{ requires_restart: 0 },
|
||||||
)
|
)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
@ -573,6 +613,14 @@ export default function ZoneEditPane({
|
|||||||
);
|
);
|
||||||
return;
|
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);
|
field.onChange(checked);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -607,6 +655,7 @@ export default function ZoneEditPane({
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<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}
|
{...field}
|
||||||
onFocus={() => setActiveLine(1)}
|
onFocus={() => setActiveLine(1)}
|
||||||
onBlur={() => setActiveLine(undefined)}
|
onBlur={() => setActiveLine(undefined)}
|
||||||
@ -629,6 +678,7 @@ export default function ZoneEditPane({
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<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}
|
{...field}
|
||||||
onFocus={() => setActiveLine(2)}
|
onFocus={() => setActiveLine(2)}
|
||||||
onBlur={() => setActiveLine(undefined)}
|
onBlur={() => setActiveLine(undefined)}
|
||||||
@ -651,6 +701,7 @@ export default function ZoneEditPane({
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<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}
|
{...field}
|
||||||
onFocus={() => setActiveLine(3)}
|
onFocus={() => setActiveLine(3)}
|
||||||
onBlur={() => setActiveLine(undefined)}
|
onBlur={() => setActiveLine(undefined)}
|
||||||
@ -673,6 +724,7 @@ export default function ZoneEditPane({
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<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}
|
{...field}
|
||||||
onFocus={() => setActiveLine(4)}
|
onFocus={() => setActiveLine(4)}
|
||||||
onBlur={() => setActiveLine(undefined)}
|
onBlur={() => setActiveLine(undefined)}
|
||||||
@ -681,6 +733,31 @@ export default function ZoneEditPane({
|
|||||||
</FormItem>
|
</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;
|
lineB: number;
|
||||||
lineC: number;
|
lineC: number;
|
||||||
lineD: number;
|
lineD: number;
|
||||||
|
speed_threshold: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ObjectMaskFormValuesType = {
|
export type ObjectMaskFormValuesType = {
|
||||||
|
|||||||
@ -219,6 +219,7 @@ export interface CameraConfig {
|
|||||||
filters: Record<string, unknown>;
|
filters: Record<string, unknown>;
|
||||||
inertia: number;
|
inertia: number;
|
||||||
loitering_time: number;
|
loitering_time: number;
|
||||||
|
speed_threshold: number;
|
||||||
objects: string[];
|
objects: string[];
|
||||||
color: number[];
|
color: number[];
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user