Store and show boxes for attributes in timeline (#20513)

* Store and show boxes for attributes in timeline

* Simplify
This commit is contained in:
Nicolas Mowen 2025-10-16 07:00:38 -06:00 committed by GitHub
parent 4e99ee0c33
commit 2e7a2fd780
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 29 additions and 103 deletions

View File

@ -142,6 +142,11 @@ class TimelineProcessor(threading.Thread):
timeline_entry[Timeline.data]["attribute"] = list( timeline_entry[Timeline.data]["attribute"] = list(
event_data["attributes"].keys() event_data["attributes"].keys()
)[0] )[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 save = True
elif event_type == EventStateEnum.end: elif event_type == EventStateEnum.end:
timeline_entry[Timeline.class_type] = "gone" timeline_entry[Timeline.class_type] = "gone"

View File

@ -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<boolean>(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 (
<>
<div
className="absolute border-4 border-red-600"
onMouseEnter={() => 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" ? (
<div className="absolute bottom-0 left-[50%] h-2 w-2 -translate-x-1/2 translate-y-3/4 bg-yellow-500" />
) : null}
</div>
{isHovering && (
<div
className="absolute block bg-white p-4 text-lg text-black dark:bg-slate-800 dark:text-white"
style={getHoverStyle()}
>
<div>{`Area: ${getObjectArea()} px`}</div>
<div>{`Ratio: ${getObjectRatio()}`}</div>
</div>
)}
</>
);
}

View File

@ -151,6 +151,8 @@ export default function ObjectLifecycle({
); );
const [boxStyle, setBoxStyle] = useState<React.CSSProperties | null>(null); const [boxStyle, setBoxStyle] = useState<React.CSSProperties | null>(null);
const [attributeBoxStyle, setAttributeBoxStyle] =
useState<React.CSSProperties | null>(null);
const configAnnotationOffset = useMemo(() => { const configAnnotationOffset = useMemo(() => {
if (!config) { if (!config) {
@ -218,7 +220,7 @@ export default function ObjectLifecycle({
const [timeIndex, setTimeIndex] = useState(0); const [timeIndex, setTimeIndex] = useState(0);
const handleSetBox = useCallback( const handleSetBox = useCallback(
(box: number[]) => { (box: number[], attrBox: number[] | undefined) => {
if (imgRef.current && Array.isArray(box) && box.length === 4) { if (imgRef.current && Array.isArray(box) && box.length === 4) {
const imgElement = imgRef.current; const imgElement = imgRef.current;
const imgRect = imgElement.getBoundingClientRect(); const imgRect = imgElement.getBoundingClientRect();
@ -231,6 +233,19 @@ export default function ObjectLifecycle({
borderColor: `rgb(${getObjectColor(event.label)?.join(",")})`, 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); setBoxStyle(style);
} }
}, },
@ -292,7 +307,10 @@ export default function ObjectLifecycle({
} else { } else {
// lifecycle point // lifecycle point
setTimeIndex(eventSequence?.[current].timestamp); setTimeIndex(eventSequence?.[current].timestamp);
handleSetBox(eventSequence?.[current].data.box ?? []); handleSetBox(
eventSequence?.[current].data.box ?? [],
eventSequence?.[current].data?.attribute_box,
);
setLifecycleZones(eventSequence?.[current].data.zones); setLifecycleZones(eventSequence?.[current].data.zones);
} }
setSelectedZone(""); setSelectedZone("");
@ -448,6 +466,9 @@ export default function ObjectLifecycle({
<div className="absolute bottom-[-3px] left-1/2 h-[5px] w-[5px] -translate-x-1/2 transform bg-yellow-500" /> <div className="absolute bottom-[-3px] left-1/2 h-[5px] w-[5px] -translate-x-1/2 transform bg-yellow-500" />
</div> </div>
)} )}
{attributeBoxStyle && (
<div className="absolute border-2" style={attributeBoxStyle} />
)}
{imgRef.current?.width && {imgRef.current?.width &&
imgRef.current?.height && imgRef.current?.height &&
pathPoints && pathPoints &&

View File

@ -20,6 +20,7 @@ export type ObjectLifecycleSequence = {
box?: [number, number, number, number]; box?: [number, number, number, number];
region: [number, number, number, number]; region: [number, number, number, number];
attribute: string; attribute: string;
attribute_box?: [number, number, number, number];
zones: string[]; zones: string[];
}; };
class_type: LifecycleClassType; class_type: LifecycleClassType;