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
@@ -25,6 +25,7 @@ import { baseUrl } from "@/api/baseUrl";
import { cn } from "@/lib/utils";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import {
FaArrowRight,
FaCheckCircle,
FaChevronDown,
FaDownload,
@@ -329,6 +330,30 @@ function ObjectDetailsTab({
}
}, [search]);
const averageEstimatedSpeed = useMemo(() => {
if (!search || !search.data?.average_estimated_speed) {
return undefined;
}
if (search.data?.average_estimated_speed != 0) {
return search.data?.average_estimated_speed.toFixed(1);
} else {
return undefined;
}
}, [search]);
const velocityAngle = useMemo(() => {
if (!search || !search.data?.velocity_angle) {
return undefined;
}
if (search.data?.velocity_angle != 0) {
return search.data?.velocity_angle.toFixed(1);
} else {
return undefined;
}
}, [search]);
const updateDescription = useCallback(() => {
if (!search) {
return;
@@ -440,6 +465,29 @@ function ObjectDetailsTab({
{score}%{subLabelScore && ` (${subLabelScore}%)`}
</div>
</div>
{averageEstimatedSpeed && (
<div className="flex flex-col gap-1.5">
<div className="text-sm text-primary/40">Estimated Speed</div>
<div className="flex flex-col space-y-0.5 text-sm">
{averageEstimatedSpeed && (
<div className="flex flex-row items-center gap-2">
{averageEstimatedSpeed}{" "}
{config?.ui.unit_system == "imperial" ? "mph" : "kph"}{" "}
{velocityAngle != undefined && (
<span className="text-primary/40">
<FaArrowRight
size={10}
style={{
transform: `rotate(${(360 - Number(velocityAngle)) % 360}deg)`,
}}
/>
</span>
)}
</div>
)}
</div>
</div>
)}
<div className="flex flex-col gap-1.5">
<div className="text-sm text-primary/40">Camera</div>
<div className="text-sm capitalize">
@@ -71,9 +71,11 @@ export default function SearchFilterDialog({
currentFilter &&
(currentFilter.time_range ||
(currentFilter.min_score ?? 0) > 0.5 ||
(currentFilter.min_speed ?? 1) > 1 ||
(currentFilter.has_snapshot ?? 0) === 1 ||
(currentFilter.has_clip ?? 0) === 1 ||
(currentFilter.max_score ?? 1) < 1 ||
(currentFilter.max_speed ?? 150) < 150 ||
(currentFilter.zones?.length ?? 0) > 0 ||
(currentFilter.sub_labels?.length ?? 0) > 0),
[currentFilter],
@@ -124,6 +126,14 @@ export default function SearchFilterDialog({
setCurrentFilter({ ...currentFilter, min_score: min, max_score: max })
}
/>
<SpeedFilterContent
config={config}
minSpeed={currentFilter.min_speed}
maxSpeed={currentFilter.max_speed}
setSpeedRange={(min, max) =>
setCurrentFilter({ ...currentFilter, min_speed: min, max_speed: max })
}
/>
<SnapshotClipFilterContent
config={config}
hasSnapshot={
@@ -178,6 +188,8 @@ export default function SearchFilterDialog({
search_type: undefined,
min_score: undefined,
max_score: undefined,
min_speed: undefined,
max_speed: undefined,
has_snapshot: undefined,
has_clip: undefined,
}));
@@ -521,6 +533,62 @@ export function ScoreFilterContent({
);
}
type SpeedFilterContentProps = {
config?: FrigateConfig;
minSpeed: number | undefined;
maxSpeed: number | undefined;
setSpeedRange: (min: number | undefined, max: number | undefined) => void;
};
export function SpeedFilterContent({
config,
minSpeed,
maxSpeed,
setSpeedRange,
}: SpeedFilterContentProps) {
return (
<div className="overflow-x-hidden">
<DropdownMenuSeparator className="mb-3" />
<div className="mb-3 text-lg">
Estimated Speed ({config?.ui.unit_system == "metric" ? "kph" : "mph"})
</div>
<div className="flex items-center gap-1">
<Input
className="w-14 text-center"
inputMode="numeric"
value={minSpeed ?? 1}
onChange={(e) => {
const value = e.target.value;
if (value) {
setSpeedRange(parseInt(value), maxSpeed ?? 1.0);
}
}}
/>
<DualThumbSlider
className="mx-2 w-full"
min={1}
max={150}
step={1}
value={[minSpeed ?? 1, maxSpeed ?? 150]}
onValueChange={([min, max]) => setSpeedRange(min, max)}
/>
<Input
className="w-14 text-center"
inputMode="numeric"
value={maxSpeed ?? 150}
onChange={(e) => {
const value = e.target.value;
if (value) {
setSpeedRange(minSpeed ?? 1, parseInt(value));
}
}}
/>
</div>
</div>
);
}
type SnapshotClipContentProps = {
config?: FrigateConfig;
hasSnapshot: boolean | undefined;