mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-09 23:05:43 +03:00
tracking details tweaks
- Add attribute box overlay and area - Add score - Throttle swr revalidation during video component rerendering
This commit is contained in:
parent
2c893aa125
commit
086330a542
@ -109,6 +109,7 @@ class TimelineProcessor(threading.Thread):
|
|||||||
event_data["region"],
|
event_data["region"],
|
||||||
),
|
),
|
||||||
"attribute": "",
|
"attribute": "",
|
||||||
|
"score": event_data["score"],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -42,6 +42,7 @@ type ObjectData = {
|
|||||||
pathPoints: PathPoint[];
|
pathPoints: PathPoint[];
|
||||||
currentZones: string[];
|
currentZones: string[];
|
||||||
currentBox?: number[];
|
currentBox?: number[];
|
||||||
|
currentAttributeBox?: number[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ObjectTrackOverlay({
|
export default function ObjectTrackOverlay({
|
||||||
@ -105,6 +106,12 @@ export default function ObjectTrackOverlay({
|
|||||||
selectedObjectIds.length > 0
|
selectedObjectIds.length > 0
|
||||||
? ["event_ids", { ids: selectedObjectIds.join(",") }]
|
? ["event_ids", { ids: selectedObjectIds.join(",") }]
|
||||||
: null,
|
: null,
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
dedupingInterval: 30000,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fetch timeline data for each object ID using fixed number of hooks
|
// Fetch timeline data for each object ID using fixed number of hooks
|
||||||
@ -112,7 +119,12 @@ export default function ObjectTrackOverlay({
|
|||||||
selectedObjectIds.length > 0
|
selectedObjectIds.length > 0
|
||||||
? `timeline?source_id=${selectedObjectIds.join(",")}&limit=1000`
|
? `timeline?source_id=${selectedObjectIds.join(",")}&limit=1000`
|
||||||
: null,
|
: null,
|
||||||
{ revalidateOnFocus: false },
|
null,
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
dedupingInterval: 30000,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const getZonesFriendlyNames = (zones: string[], config: FrigateConfig) => {
|
const getZonesFriendlyNames = (zones: string[], config: FrigateConfig) => {
|
||||||
@ -270,6 +282,7 @@ export default function ObjectTrackOverlay({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const currentBox = nearbyTimelineEvent?.data?.box;
|
const currentBox = nearbyTimelineEvent?.data?.box;
|
||||||
|
const currentAttributeBox = nearbyTimelineEvent?.data?.attribute_box;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
objectId,
|
objectId,
|
||||||
@ -278,6 +291,7 @@ export default function ObjectTrackOverlay({
|
|||||||
pathPoints: combinedPoints,
|
pathPoints: combinedPoints,
|
||||||
currentZones,
|
currentZones,
|
||||||
currentBox,
|
currentBox,
|
||||||
|
currentAttributeBox,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((obj: ObjectData) => obj.pathPoints.length > 0); // Only include objects with path data
|
.filter((obj: ObjectData) => obj.pathPoints.length > 0); // Only include objects with path data
|
||||||
@ -482,6 +496,20 @@ export default function ObjectTrackOverlay({
|
|||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
)}
|
)}
|
||||||
|
{objData.currentAttributeBox && showBoundingBoxes && (
|
||||||
|
<g>
|
||||||
|
<rect
|
||||||
|
x={objData.currentAttributeBox[0] * videoWidth}
|
||||||
|
y={objData.currentAttributeBox[1] * videoHeight}
|
||||||
|
width={objData.currentAttributeBox[2] * videoWidth}
|
||||||
|
height={objData.currentAttributeBox[3] * videoHeight}
|
||||||
|
fill="none"
|
||||||
|
stroke={objData.color}
|
||||||
|
strokeWidth={boxStroke}
|
||||||
|
opacity="0.9"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
)}
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -75,12 +75,15 @@ export function TrackingDetails({
|
|||||||
setIsVideoLoading(true);
|
setIsVideoLoading(true);
|
||||||
}, [event.id]);
|
}, [event.id]);
|
||||||
|
|
||||||
const { data: eventSequence } = useSWR<TrackingDetailsSequence[]>([
|
const { data: eventSequence } = useSWR<TrackingDetailsSequence[]>(
|
||||||
"timeline",
|
["timeline", { source_id: event.id }],
|
||||||
|
null,
|
||||||
{
|
{
|
||||||
source_id: event.id,
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
dedupingInterval: 30000,
|
||||||
},
|
},
|
||||||
]);
|
);
|
||||||
|
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
@ -104,6 +107,12 @@ export function TrackingDetails({
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
: null,
|
: null,
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
dedupingInterval: 30000,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Convert a timeline timestamp to actual video player time, accounting for
|
// Convert a timeline timestamp to actual video player time, accounting for
|
||||||
@ -714,53 +723,6 @@ export function TrackingDetails({
|
|||||||
)}
|
)}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{eventSequence.map((item, idx) => {
|
{eventSequence.map((item, idx) => {
|
||||||
const isActive =
|
|
||||||
Math.abs(
|
|
||||||
(effectiveTime ?? 0) - (item.timestamp ?? 0),
|
|
||||||
) <= 0.5;
|
|
||||||
const formattedEventTimestamp = config
|
|
||||||
? formatUnixTimestampToDateTime(item.timestamp ?? 0, {
|
|
||||||
timezone: config.ui.timezone,
|
|
||||||
date_format:
|
|
||||||
config.ui.time_format == "24hour"
|
|
||||||
? t(
|
|
||||||
"time.formattedTimestampHourMinuteSecond.24hour",
|
|
||||||
{ ns: "common" },
|
|
||||||
)
|
|
||||||
: t(
|
|
||||||
"time.formattedTimestampHourMinuteSecond.12hour",
|
|
||||||
{ ns: "common" },
|
|
||||||
),
|
|
||||||
time_style: "medium",
|
|
||||||
date_style: "medium",
|
|
||||||
})
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const ratio =
|
|
||||||
Array.isArray(item.data.box) &&
|
|
||||||
item.data.box.length >= 4
|
|
||||||
? (
|
|
||||||
aspectRatio *
|
|
||||||
(item.data.box[2] / item.data.box[3])
|
|
||||||
).toFixed(2)
|
|
||||||
: "N/A";
|
|
||||||
const areaPx =
|
|
||||||
Array.isArray(item.data.box) &&
|
|
||||||
item.data.box.length >= 4
|
|
||||||
? Math.round(
|
|
||||||
(config.cameras[event.camera]?.detect?.width ??
|
|
||||||
0) *
|
|
||||||
(config.cameras[event.camera]?.detect
|
|
||||||
?.height ?? 0) *
|
|
||||||
(item.data.box[2] * item.data.box[3]),
|
|
||||||
)
|
|
||||||
: undefined;
|
|
||||||
const areaPct =
|
|
||||||
Array.isArray(item.data.box) &&
|
|
||||||
item.data.box.length >= 4
|
|
||||||
? (item.data.box[2] * item.data.box[3]).toFixed(4)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`${item.timestamp}-${item.source_id ?? ""}-${idx}`}
|
key={`${item.timestamp}-${item.source_id ?? ""}-${idx}`}
|
||||||
@ -770,11 +732,7 @@ export function TrackingDetails({
|
|||||||
>
|
>
|
||||||
<LifecycleIconRow
|
<LifecycleIconRow
|
||||||
item={item}
|
item={item}
|
||||||
isActive={isActive}
|
event={event}
|
||||||
formattedEventTimestamp={formattedEventTimestamp}
|
|
||||||
ratio={ratio}
|
|
||||||
areaPx={areaPx}
|
|
||||||
areaPct={areaPct}
|
|
||||||
onClick={() => handleLifecycleClick(item)}
|
onClick={() => handleLifecycleClick(item)}
|
||||||
setSelectedZone={setSelectedZone}
|
setSelectedZone={setSelectedZone}
|
||||||
getZoneColor={getZoneColor}
|
getZoneColor={getZoneColor}
|
||||||
@ -798,11 +756,7 @@ export function TrackingDetails({
|
|||||||
|
|
||||||
type LifecycleIconRowProps = {
|
type LifecycleIconRowProps = {
|
||||||
item: TrackingDetailsSequence;
|
item: TrackingDetailsSequence;
|
||||||
isActive?: boolean;
|
event: Event;
|
||||||
formattedEventTimestamp: string;
|
|
||||||
ratio: string;
|
|
||||||
areaPx?: number;
|
|
||||||
areaPct?: string;
|
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
setSelectedZone: (z: string) => void;
|
setSelectedZone: (z: string) => void;
|
||||||
getZoneColor: (zoneName: string) => number[] | undefined;
|
getZoneColor: (zoneName: string) => number[] | undefined;
|
||||||
@ -812,11 +766,7 @@ type LifecycleIconRowProps = {
|
|||||||
|
|
||||||
function LifecycleIconRow({
|
function LifecycleIconRow({
|
||||||
item,
|
item,
|
||||||
isActive,
|
event,
|
||||||
formattedEventTimestamp,
|
|
||||||
ratio,
|
|
||||||
areaPx,
|
|
||||||
areaPct,
|
|
||||||
onClick,
|
onClick,
|
||||||
setSelectedZone,
|
setSelectedZone,
|
||||||
getZoneColor,
|
getZoneColor,
|
||||||
@ -826,9 +776,101 @@ function LifecycleIconRow({
|
|||||||
const { t } = useTranslation(["views/explore", "components/player"]);
|
const { t } = useTranslation(["views/explore", "components/player"]);
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const aspectRatio = useMemo(() => {
|
||||||
|
if (!config) {
|
||||||
|
return 16 / 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
config.cameras[event.camera].detect.width /
|
||||||
|
config.cameras[event.camera].detect.height
|
||||||
|
);
|
||||||
|
}, [config, event]);
|
||||||
|
|
||||||
|
const isActive = useMemo(
|
||||||
|
() => Math.abs((effectiveTime ?? 0) - (item.timestamp ?? 0)) <= 0.5,
|
||||||
|
[effectiveTime, item.timestamp],
|
||||||
|
);
|
||||||
|
|
||||||
|
const formattedEventTimestamp = useMemo(
|
||||||
|
() =>
|
||||||
|
config
|
||||||
|
? formatUnixTimestampToDateTime(item.timestamp ?? 0, {
|
||||||
|
timezone: config.ui.timezone,
|
||||||
|
date_format:
|
||||||
|
config.ui.time_format == "24hour"
|
||||||
|
? t("time.formattedTimestampHourMinuteSecond.24hour", {
|
||||||
|
ns: "common",
|
||||||
|
})
|
||||||
|
: t("time.formattedTimestampHourMinuteSecond.12hour", {
|
||||||
|
ns: "common",
|
||||||
|
}),
|
||||||
|
time_style: "medium",
|
||||||
|
date_style: "medium",
|
||||||
|
})
|
||||||
|
: "",
|
||||||
|
[config, item.timestamp, t],
|
||||||
|
);
|
||||||
|
|
||||||
|
const ratio = useMemo(
|
||||||
|
() =>
|
||||||
|
Array.isArray(item.data.box) && item.data.box.length >= 4
|
||||||
|
? (aspectRatio * (item.data.box[2] / item.data.box[3])).toFixed(2)
|
||||||
|
: "N/A",
|
||||||
|
[aspectRatio, item.data.box],
|
||||||
|
);
|
||||||
|
|
||||||
|
const areaPx = useMemo(
|
||||||
|
() =>
|
||||||
|
Array.isArray(item.data.box) && item.data.box.length >= 4
|
||||||
|
? Math.round(
|
||||||
|
(config?.cameras[event.camera]?.detect?.width ?? 0) *
|
||||||
|
(config?.cameras[event.camera]?.detect?.height ?? 0) *
|
||||||
|
(item.data.box[2] * item.data.box[3]),
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
[config, event.camera, item.data.box],
|
||||||
|
);
|
||||||
|
|
||||||
|
const attributeAreaPx = useMemo(
|
||||||
|
() =>
|
||||||
|
Array.isArray(item.data.attribute_box) &&
|
||||||
|
item.data.attribute_box.length >= 4
|
||||||
|
? Math.round(
|
||||||
|
(config?.cameras[event.camera]?.detect?.width ?? 0) *
|
||||||
|
(config?.cameras[event.camera]?.detect?.height ?? 0) *
|
||||||
|
(item.data.attribute_box[2] * item.data.attribute_box[3]),
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
[config, event.camera, item.data.attribute_box],
|
||||||
|
);
|
||||||
|
|
||||||
|
const attributeAreaPct = useMemo(
|
||||||
|
() =>
|
||||||
|
Array.isArray(item.data.attribute_box) &&
|
||||||
|
item.data.attribute_box.length >= 4
|
||||||
|
? (item.data.attribute_box[2] * item.data.attribute_box[3]).toFixed(4)
|
||||||
|
: undefined,
|
||||||
|
[item.data.attribute_box],
|
||||||
|
);
|
||||||
|
|
||||||
|
const areaPct = useMemo(
|
||||||
|
() =>
|
||||||
|
Array.isArray(item.data.box) && item.data.box.length >= 4
|
||||||
|
? (item.data.box[2] * item.data.box[3]).toFixed(4)
|
||||||
|
: undefined,
|
||||||
|
[item.data.box],
|
||||||
|
);
|
||||||
|
|
||||||
|
const score = useMemo(() => {
|
||||||
|
if (item.data.score !== undefined) {
|
||||||
|
return (item.data.score * 100).toFixed(0) + "%";
|
||||||
|
}
|
||||||
|
return "N/A";
|
||||||
|
}, [item.data.score]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
@ -856,16 +898,28 @@ function LifecycleIconRow({
|
|||||||
<div className="text-md flex items-start break-words text-left">
|
<div className="text-md flex items-start break-words text-left">
|
||||||
{getLifecycleItemDescription(item)}
|
{getLifecycleItemDescription(item)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 flex flex-wrap items-center gap-2 text-xs text-secondary-foreground md:gap-5">
|
<div className="my-2 ml-2 flex flex-col flex-wrap items-start gap-1.5 text-xs text-secondary-foreground">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span className="text-primary-variant">
|
||||||
|
{t("trackingDetails.lifecycleItemDesc.header.score")}
|
||||||
|
</span>
|
||||||
|
<span className="font-medium text-primary">{score}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
<span className="text-primary-variant">
|
<span className="text-primary-variant">
|
||||||
{t("trackingDetails.lifecycleItemDesc.header.ratio")}
|
{t("trackingDetails.lifecycleItemDesc.header.ratio")}
|
||||||
</span>
|
</span>
|
||||||
<span className="font-medium text-primary">{ratio}</span>
|
<span className="font-medium text-primary">{ratio}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1.5">
|
||||||
<span className="text-primary-variant">
|
<span className="text-primary-variant">
|
||||||
{t("trackingDetails.lifecycleItemDesc.header.area")}
|
{t("trackingDetails.lifecycleItemDesc.header.area")}{" "}
|
||||||
|
{attributeAreaPx !== undefined &&
|
||||||
|
attributeAreaPct !== undefined && (
|
||||||
|
<span className="text-primary-variant">
|
||||||
|
({getTranslatedLabel(item.data.label)})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
{areaPx !== undefined && areaPct !== undefined ? (
|
{areaPx !== undefined && areaPct !== undefined ? (
|
||||||
<span className="font-medium text-primary">
|
<span className="font-medium text-primary">
|
||||||
@ -876,9 +930,25 @@ function LifecycleIconRow({
|
|||||||
<span>N/A</span>
|
<span>N/A</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{attributeAreaPx !== undefined &&
|
||||||
|
attributeAreaPct !== undefined && (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span className="text-primary-variant">
|
||||||
|
{t("trackingDetails.lifecycleItemDesc.header.area")} (
|
||||||
|
{getTranslatedLabel(item.data.attribute)})
|
||||||
|
</span>
|
||||||
|
<span className="font-medium text-primary">
|
||||||
|
{t("information.pixels", {
|
||||||
|
ns: "common",
|
||||||
|
area: attributeAreaPx,
|
||||||
|
})}{" "}
|
||||||
|
· {attributeAreaPct}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{item.data?.zones && item.data.zones.length > 0 && (
|
{item.data?.zones && item.data.zones.length > 0 && (
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="mt-1 flex flex-wrap items-center gap-2">
|
||||||
{item.data.zones.map((zone, zidx) => {
|
{item.data.zones.map((zone, zidx) => {
|
||||||
const color = getZoneColor(zone)?.join(",") ?? "0,0,0";
|
const color = getZoneColor(zone)?.join(",") ?? "0,0,0";
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -16,6 +16,7 @@ export type TrackingDetailsSequence = {
|
|||||||
data: {
|
data: {
|
||||||
camera: string;
|
camera: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
score: number;
|
||||||
sub_label: string;
|
sub_label: string;
|
||||||
box?: [number, number, number, number];
|
box?: [number, number, number, number];
|
||||||
region: [number, number, number, number];
|
region: [number, number, number, number];
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user