mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-07 05:55:27 +03:00
Section faces by event id
This commit is contained in:
parent
3f1b4438e4
commit
c89bb12a4f
@ -32,6 +32,7 @@ import { useFormattedTimestamp } from "@/hooks/use-date-utils";
|
|||||||
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
||||||
import useOptimisticState from "@/hooks/use-optimistic-state";
|
import useOptimisticState from "@/hooks/use-optimistic-state";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Event } from "@/types/event";
|
||||||
import { FaceLibraryData, RecognizedFaceData } from "@/types/face";
|
import { FaceLibraryData, RecognizedFaceData } from "@/types/face";
|
||||||
import { FaceRecognitionConfig, FrigateConfig } from "@/types/frigateConfig";
|
import { FaceRecognitionConfig, FrigateConfig } from "@/types/frigateConfig";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
@ -395,6 +396,41 @@ function TrainingGrid({
|
|||||||
|
|
||||||
// face data
|
// face data
|
||||||
|
|
||||||
|
const faceGroups = useMemo(() => {
|
||||||
|
const groups: { [eventId: string]: RecognizedFaceData[] } = {};
|
||||||
|
|
||||||
|
attemptImages.forEach((image) => {
|
||||||
|
const parts = image.split("-");
|
||||||
|
const data = {
|
||||||
|
filename: image,
|
||||||
|
timestamp: Number.parseFloat(parts[0]),
|
||||||
|
eventId: `${parts[0]}-${parts[1]}`,
|
||||||
|
name: parts[2],
|
||||||
|
score: Number.parseFloat(parts[3]),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (groups[data.eventId]) {
|
||||||
|
groups[data.eventId].push(data);
|
||||||
|
} else {
|
||||||
|
groups[data.eventId] = [data];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}, [attemptImages]);
|
||||||
|
|
||||||
|
const eventIdsQuery = useMemo(
|
||||||
|
() => Object.keys(faceGroups).join(","),
|
||||||
|
[faceGroups],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: events } = useSWR<Event[]>([
|
||||||
|
"event_ids",
|
||||||
|
{ ids: eventIdsQuery },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// selection
|
||||||
|
|
||||||
const [selectedEvent, setSelectedEvent] = useState<RecognizedFaceData>();
|
const [selectedEvent, setSelectedEvent] = useState<RecognizedFaceData>();
|
||||||
|
|
||||||
const formattedDate = useFormattedTimestamp(
|
const formattedDate = useFormattedTimestamp(
|
||||||
@ -405,6 +441,10 @@ function TrainingGrid({
|
|||||||
config?.ui.timezone,
|
config?.ui.timezone,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!events) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog
|
<Dialog
|
||||||
@ -446,30 +486,49 @@ function TrainingGrid({
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<div className="scrollbar-container flex flex-wrap gap-2 overflow-y-scroll p-1">
|
<div className="scrollbar-container flex flex-wrap gap-2 overflow-y-scroll p-1">
|
||||||
{attemptImages.map((image: string) => (
|
{Object.entries(faceGroups).map(([key, group]) => {
|
||||||
<FaceAttempt
|
const event = events.find((ev) => ev.id == key);
|
||||||
key={image}
|
|
||||||
image={image}
|
if (!event) {
|
||||||
faceNames={faceNames}
|
return null;
|
||||||
recognitionConfig={config.face_recognition}
|
}
|
||||||
selected={selectedFaces.includes(image)}
|
|
||||||
onClick={(data, meta) => {
|
return (
|
||||||
if (meta) {
|
<div className="flex flex-col gap-2 rounded-lg bg-card p-4">
|
||||||
onClickFace(image, meta);
|
<div className="capitalize">
|
||||||
} else {
|
{event.label} ({Math.round(event.data.top_score * 100)}%){" "}
|
||||||
setSelectedEvent(data);
|
{event.sub_label &&
|
||||||
}
|
`${event.sub_label} (${Math.round((event.data.sub_label_score || 0) * 100)}%)`}
|
||||||
}}
|
</div>
|
||||||
onRefresh={onRefresh}
|
<div className="flex flex-row">
|
||||||
/>
|
{group.map((data: RecognizedFaceData) => (
|
||||||
))}
|
<FaceAttempt
|
||||||
|
key={data.timestamp}
|
||||||
|
data={data}
|
||||||
|
faceNames={faceNames}
|
||||||
|
recognitionConfig={config.face_recognition}
|
||||||
|
selected={selectedFaces.includes(data.filename)}
|
||||||
|
onClick={(data, meta) => {
|
||||||
|
if (meta) {
|
||||||
|
onClickFace(data.filename, meta);
|
||||||
|
} else {
|
||||||
|
setSelectedEvent(data);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type FaceAttemptProps = {
|
type FaceAttemptProps = {
|
||||||
image: string;
|
data: RecognizedFaceData;
|
||||||
faceNames: string[];
|
faceNames: string[];
|
||||||
recognitionConfig: FaceRecognitionConfig;
|
recognitionConfig: FaceRecognitionConfig;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
@ -477,7 +536,7 @@ type FaceAttemptProps = {
|
|||||||
onRefresh: () => void;
|
onRefresh: () => void;
|
||||||
};
|
};
|
||||||
function FaceAttempt({
|
function FaceAttempt({
|
||||||
image,
|
data,
|
||||||
faceNames,
|
faceNames,
|
||||||
recognitionConfig,
|
recognitionConfig,
|
||||||
selected,
|
selected,
|
||||||
@ -485,16 +544,6 @@ function FaceAttempt({
|
|||||||
onRefresh,
|
onRefresh,
|
||||||
}: FaceAttemptProps) {
|
}: FaceAttemptProps) {
|
||||||
const { t } = useTranslation(["views/faceLibrary"]);
|
const { t } = useTranslation(["views/faceLibrary"]);
|
||||||
const data = useMemo<RecognizedFaceData>(() => {
|
|
||||||
const parts = image.split("-");
|
|
||||||
|
|
||||||
return {
|
|
||||||
timestamp: Number.parseFloat(parts[0]),
|
|
||||||
eventId: `${parts[0]}-${parts[1]}`,
|
|
||||||
name: parts[2],
|
|
||||||
score: Number.parseFloat(parts[3]),
|
|
||||||
};
|
|
||||||
}, [image]);
|
|
||||||
|
|
||||||
const scoreStatus = useMemo(() => {
|
const scoreStatus = useMemo(() => {
|
||||||
if (data.score >= recognitionConfig.recognition_threshold) {
|
if (data.score >= recognitionConfig.recognition_threshold) {
|
||||||
@ -519,7 +568,9 @@ function FaceAttempt({
|
|||||||
const onTrainAttempt = useCallback(
|
const onTrainAttempt = useCallback(
|
||||||
(trainName: string) => {
|
(trainName: string) => {
|
||||||
axios
|
axios
|
||||||
.post(`/faces/train/${trainName}/classify`, { training_file: image })
|
.post(`/faces/train/${trainName}/classify`, {
|
||||||
|
training_file: data.eventId,
|
||||||
|
})
|
||||||
.then((resp) => {
|
.then((resp) => {
|
||||||
if (resp.status == 200) {
|
if (resp.status == 200) {
|
||||||
toast.success(t("toast.success.trainedFace"), {
|
toast.success(t("toast.success.trainedFace"), {
|
||||||
@ -538,12 +589,12 @@ function FaceAttempt({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[image, onRefresh, t],
|
[data, onRefresh, t],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onReprocess = useCallback(() => {
|
const onReprocess = useCallback(() => {
|
||||||
axios
|
axios
|
||||||
.post(`/faces/reprocess`, { training_file: image })
|
.post(`/faces/reprocess`, { training_file: data.filename })
|
||||||
.then((resp) => {
|
.then((resp) => {
|
||||||
if (resp.status == 200) {
|
if (resp.status == 200) {
|
||||||
toast.success(t("toast.success.updatedFaceScore"), {
|
toast.success(t("toast.success.updatedFaceScore"), {
|
||||||
@ -561,7 +612,7 @@ function FaceAttempt({
|
|||||||
position: "top-center",
|
position: "top-center",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, [image, onRefresh, t]);
|
}, [data, onRefresh, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -576,7 +627,7 @@ function FaceAttempt({
|
|||||||
<img
|
<img
|
||||||
ref={imgRef}
|
ref={imgRef}
|
||||||
className="size-44"
|
className="size-44"
|
||||||
src={`${baseUrl}clips/faces/train/${image}`}
|
src={`${baseUrl}clips/faces/train/${data.filename}`}
|
||||||
onClick={(e) => onClick(data, e.metaKey || e.ctrlKey)}
|
onClick={(e) => onClick(data, e.metaKey || e.ctrlKey)}
|
||||||
/>
|
/>
|
||||||
<div className="absolute bottom-1 right-1 z-10 rounded-lg bg-black/50 px-2 py-1 text-xs text-white">
|
<div className="absolute bottom-1 right-1 z-10 rounded-lg bg-black/50 px-2 py-1 text-xs text-white">
|
||||||
|
|||||||
@ -3,6 +3,7 @@ export type FaceLibraryData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type RecognizedFaceData = {
|
export type RecognizedFaceData = {
|
||||||
|
filename: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
eventId: string;
|
eventId: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user