From 2e7a2fd780008647560ee2be3f83d4163a5071b4 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Thu, 16 Oct 2025 07:00:38 -0600 Subject: [PATCH] Store and show boxes for attributes in timeline (#20513) * Store and show boxes for attributes in timeline * Simplify --- frigate/timeline.py | 5 + .../overlay/TimelineDataOverlay.tsx | 101 ------------------ .../overlay/detail/ObjectLifecycle.tsx | 25 ++++- web/src/types/timeline.ts | 1 + 4 files changed, 29 insertions(+), 103 deletions(-) delete mode 100644 web/src/components/overlay/TimelineDataOverlay.tsx diff --git a/frigate/timeline.py b/frigate/timeline.py index 4c3d0d457..f8d341660 100644 --- a/frigate/timeline.py +++ b/frigate/timeline.py @@ -142,6 +142,11 @@ class TimelineProcessor(threading.Thread): timeline_entry[Timeline.data]["attribute"] = list( event_data["attributes"].keys() )[0] + timeline_entry[Timeline.data]["attribute_box"] = to_relative_box( + camera_config.detect.width, + camera_config.detect.height, + event_data["current_attributes"][0]["box"], + ) save = True elif event_type == EventStateEnum.end: timeline_entry[Timeline.class_type] = "gone" diff --git a/web/src/components/overlay/TimelineDataOverlay.tsx b/web/src/components/overlay/TimelineDataOverlay.tsx deleted file mode 100644 index a0d6190f6..000000000 --- a/web/src/components/overlay/TimelineDataOverlay.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { ObjectLifecycleSequence } from "@/types/timeline"; -import { useState } from "react"; - -type TimelineEventOverlayProps = { - timeline: ObjectLifecycleSequence; - cameraConfig: { - detect: { - width: number; - height: number; - }; - }; -}; - -export default function TimelineEventOverlay({ - timeline, - cameraConfig, -}: TimelineEventOverlayProps) { - const [isHovering, setIsHovering] = useState(false); - const getHoverStyle = () => { - if (!timeline.data.box) { - return {}; - } - - if (boxLeftEdge < 15) { - // show object stats on right side - return { - left: `${boxLeftEdge + timeline.data.box[2] * 100 + 1}%`, - top: `${boxTopEdge}%`, - }; - } - - return { - right: `${boxRightEdge + timeline.data.box[2] * 100 + 1}%`, - top: `${boxTopEdge}%`, - }; - }; - - const getObjectArea = () => { - if (!timeline.data.box) { - return 0; - } - - const width = timeline.data.box[2] * cameraConfig.detect.width; - const height = timeline.data.box[3] * cameraConfig.detect.height; - return Math.round(width * height); - }; - - const getObjectRatio = () => { - if (!timeline.data.box) { - return 0.0; - } - - const width = timeline.data.box[2] * cameraConfig.detect.width; - const height = timeline.data.box[3] * cameraConfig.detect.height; - return Math.round(100 * (width / height)) / 100; - }; - - if (!timeline.data.box) { - return null; - } - - const boxLeftEdge = Math.round(timeline.data.box[0] * 100); - const boxTopEdge = Math.round(timeline.data.box[1] * 100); - const boxRightEdge = Math.round( - (1 - timeline.data.box[2] - timeline.data.box[0]) * 100, - ); - const boxBottomEdge = Math.round( - (1 - timeline.data.box[3] - timeline.data.box[1]) * 100, - ); - - return ( - <> -
setIsHovering(true)} - onMouseLeave={() => setIsHovering(false)} - onTouchStart={() => setIsHovering(true)} - onTouchEnd={() => setIsHovering(false)} - style={{ - left: `${boxLeftEdge}%`, - top: `${boxTopEdge}%`, - right: `${boxRightEdge}%`, - bottom: `${boxBottomEdge}%`, - }} - > - {timeline.class_type == "entered_zone" ? ( -
- ) : null} -
- {isHovering && ( -
-
{`Area: ${getObjectArea()} px`}
-
{`Ratio: ${getObjectRatio()}`}
-
- )} - - ); -} diff --git a/web/src/components/overlay/detail/ObjectLifecycle.tsx b/web/src/components/overlay/detail/ObjectLifecycle.tsx index 3fc702854..c06afd1e9 100644 --- a/web/src/components/overlay/detail/ObjectLifecycle.tsx +++ b/web/src/components/overlay/detail/ObjectLifecycle.tsx @@ -151,6 +151,8 @@ export default function ObjectLifecycle({ ); const [boxStyle, setBoxStyle] = useState(null); + const [attributeBoxStyle, setAttributeBoxStyle] = + useState(null); const configAnnotationOffset = useMemo(() => { if (!config) { @@ -218,7 +220,7 @@ export default function ObjectLifecycle({ const [timeIndex, setTimeIndex] = useState(0); const handleSetBox = useCallback( - (box: number[]) => { + (box: number[], attrBox: number[] | undefined) => { if (imgRef.current && Array.isArray(box) && box.length === 4) { const imgElement = imgRef.current; const imgRect = imgElement.getBoundingClientRect(); @@ -231,6 +233,19 @@ export default function ObjectLifecycle({ borderColor: `rgb(${getObjectColor(event.label)?.join(",")})`, }; + if (attrBox) { + const attrStyle = { + left: `${attrBox[0] * imgRect.width}px`, + top: `${attrBox[1] * imgRect.height}px`, + width: `${attrBox[2] * imgRect.width}px`, + height: `${attrBox[3] * imgRect.height}px`, + borderColor: `rgb(${getObjectColor(event.label)?.join(",")})`, + }; + setAttributeBoxStyle(attrStyle); + } else { + setAttributeBoxStyle(null); + } + setBoxStyle(style); } }, @@ -292,7 +307,10 @@ export default function ObjectLifecycle({ } else { // lifecycle point setTimeIndex(eventSequence?.[current].timestamp); - handleSetBox(eventSequence?.[current].data.box ?? []); + handleSetBox( + eventSequence?.[current].data.box ?? [], + eventSequence?.[current].data?.attribute_box, + ); setLifecycleZones(eventSequence?.[current].data.zones); } setSelectedZone(""); @@ -448,6 +466,9 @@ export default function ObjectLifecycle({
)} + {attributeBoxStyle && ( +
+ )} {imgRef.current?.width && imgRef.current?.height && pathPoints && diff --git a/web/src/types/timeline.ts b/web/src/types/timeline.ts index 45a0821ed..850b75dc5 100644 --- a/web/src/types/timeline.ts +++ b/web/src/types/timeline.ts @@ -20,6 +20,7 @@ export type ObjectLifecycleSequence = { box?: [number, number, number, number]; region: [number, number, number, number]; attribute: string; + attribute_box?: [number, number, number, number]; zones: string[]; }; class_type: LifecycleClassType;