mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-05 13:07:44 +03:00
Support creating a new face
This commit is contained in:
parent
e9409e9070
commit
13d7eec965
@ -125,10 +125,13 @@ def train_face(request: Request, name: str, body: dict = None):
|
|||||||
sanitized_name = sanitize_filename(name)
|
sanitized_name = sanitize_filename(name)
|
||||||
rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
||||||
new_name = f"{sanitized_name}-{rand_id}.webp"
|
new_name = f"{sanitized_name}-{rand_id}.webp"
|
||||||
new_file = os.path.join(FACE_DIR, f"{sanitized_name}/{new_name}")
|
new_file_folder = os.path.join(FACE_DIR, f"{sanitized_name}")
|
||||||
|
|
||||||
|
if not os.path.exists(new_file_folder):
|
||||||
|
os.mkdir(new_file_folder)
|
||||||
|
|
||||||
if training_file_name:
|
if training_file_name:
|
||||||
shutil.move(training_file, new_file)
|
shutil.move(training_file, os.path.join(new_file_folder, new_name))
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
event: Event = Event.get(Event.id == event_id)
|
event: Event = Event.get(Event.id == event_id)
|
||||||
@ -155,7 +158,7 @@ def train_face(request: Request, name: str, body: dict = None):
|
|||||||
x2 = x1 + int(face_box[2] * detect_config.width) - 4
|
x2 = x1 + int(face_box[2] * detect_config.width) - 4
|
||||||
y2 = y1 + int(face_box[3] * detect_config.height) - 4
|
y2 = y1 + int(face_box[3] * detect_config.height) - 4
|
||||||
face = snapshot[y1:y2, x1:x2]
|
face = snapshot[y1:y2, x1:x2]
|
||||||
cv2.imwrite(new_file, face)
|
cv2.imwrite(os.path.join(new_file_folder, new_name), face)
|
||||||
|
|
||||||
context: EmbeddingsContext = request.app.embeddings
|
context: EmbeddingsContext = request.app.embeddings
|
||||||
context.clear_face_classifier()
|
context.clear_face_classifier()
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import TimeAgo from "@/components/dynamic/TimeAgo";
|
|||||||
import AddFaceIcon from "@/components/icons/AddFaceIcon";
|
import AddFaceIcon from "@/components/icons/AddFaceIcon";
|
||||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||||
import CreateFaceWizardDialog from "@/components/overlay/detail/FaceCreateWizardDialog";
|
import CreateFaceWizardDialog from "@/components/overlay/detail/FaceCreateWizardDialog";
|
||||||
|
import TextEntryDialog from "@/components/overlay/dialog/TextEntryDialog";
|
||||||
import UploadImageDialog from "@/components/overlay/dialog/UploadImageDialog";
|
import UploadImageDialog from "@/components/overlay/dialog/UploadImageDialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@ -42,6 +43,7 @@ import { isDesktop, isMobile } from "react-device-detect";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
LuImagePlus,
|
LuImagePlus,
|
||||||
|
LuPlus,
|
||||||
LuRefreshCw,
|
LuRefreshCw,
|
||||||
LuScanFace,
|
LuScanFace,
|
||||||
LuSearch,
|
LuSearch,
|
||||||
@ -605,6 +607,8 @@ function FaceAttempt({
|
|||||||
|
|
||||||
// interaction
|
// interaction
|
||||||
|
|
||||||
|
const [newFace, setNewFace] = useState(false);
|
||||||
|
|
||||||
const imgRef = useRef<HTMLImageElement | null>(null);
|
const imgRef = useRef<HTMLImageElement | null>(null);
|
||||||
|
|
||||||
useContextMenu(imgRef, () => {
|
useContextMenu(imgRef, () => {
|
||||||
@ -663,76 +667,99 @@ function FaceAttempt({
|
|||||||
}, [data, onRefresh, t]);
|
}, [data, onRefresh, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
className={cn(
|
{newFace && (
|
||||||
"relative flex cursor-pointer flex-col rounded-lg outline outline-[3px]",
|
<TextEntryDialog
|
||||||
selected
|
open={true}
|
||||||
? "shadow-selected outline-selected"
|
setOpen={setNewFace}
|
||||||
: "outline-transparent duration-500",
|
title="Create New Face"
|
||||||
)}
|
onSave={(newName) => onTrainAttempt(newName)}
|
||||||
>
|
|
||||||
<div className="relative w-full overflow-hidden rounded-lg *:text-card-foreground">
|
|
||||||
<img
|
|
||||||
ref={imgRef}
|
|
||||||
className={cn("size-44", isMobile && "w-full")}
|
|
||||||
src={`${baseUrl}clips/faces/train/${data.filename}`}
|
|
||||||
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">
|
)}
|
||||||
<TimeAgo className="text-white" time={data.timestamp * 1000} dense />
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-pointer flex-col rounded-lg outline outline-[3px]",
|
||||||
|
selected
|
||||||
|
? "shadow-selected outline-selected"
|
||||||
|
: "outline-transparent duration-500",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="relative w-full overflow-hidden rounded-lg *:text-card-foreground">
|
||||||
|
<img
|
||||||
|
ref={imgRef}
|
||||||
|
className={cn("size-44", isMobile && "w-full")}
|
||||||
|
src={`${baseUrl}clips/faces/train/${data.filename}`}
|
||||||
|
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">
|
||||||
|
<TimeAgo
|
||||||
|
className="text-white"
|
||||||
|
time={data.timestamp * 1000}
|
||||||
|
dense
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="p-2">
|
||||||
<div className="p-2">
|
<div className="flex w-full flex-row items-center justify-between gap-2">
|
||||||
<div className="flex w-full flex-row items-center justify-between gap-2">
|
<div className="flex flex-col items-start text-xs text-primary-variant">
|
||||||
<div className="flex flex-col items-start text-xs text-primary-variant">
|
<div className="capitalize">{data.name}</div>
|
||||||
<div className="capitalize">{data.name}</div>
|
<div
|
||||||
<div
|
className={cn(
|
||||||
className={cn(
|
"",
|
||||||
"",
|
scoreStatus == "match" && "text-success",
|
||||||
scoreStatus == "match" && "text-success",
|
scoreStatus == "potential" && "text-orange-400",
|
||||||
scoreStatus == "potential" && "text-orange-400",
|
scoreStatus == "unknown" && "text-danger",
|
||||||
scoreStatus == "unknown" && "text-danger",
|
)}
|
||||||
)}
|
>
|
||||||
>
|
{Math.round(data.score * 100)}%
|
||||||
{Math.round(data.score * 100)}%
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row items-start justify-end gap-5 md:gap-4">
|
||||||
|
<Tooltip>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<AddFaceIcon className="size-5 cursor-pointer text-primary-variant hover:text-primary" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuLabel>{t("trainFaceAs")}</DropdownMenuLabel>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="flex cursor-pointer gap-2 capitalize"
|
||||||
|
onClick={() => setNewFace(true)}
|
||||||
|
>
|
||||||
|
<LuPlus />
|
||||||
|
Create New Face
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{faceNames.map((faceName) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={faceName}
|
||||||
|
className="flex cursor-pointer gap-2 capitalize"
|
||||||
|
onClick={() => onTrainAttempt(faceName)}
|
||||||
|
>
|
||||||
|
<LuScanFace />
|
||||||
|
{faceName}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
<TooltipContent>{t("trainFace")}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<LuRefreshCw
|
||||||
|
className="size-5 cursor-pointer text-primary-variant hover:text-primary"
|
||||||
|
onClick={() => onReprocess()}
|
||||||
|
/>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{t("button.reprocessFace")}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-start justify-end gap-5 md:gap-4">
|
|
||||||
<Tooltip>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<AddFaceIcon className="size-5 cursor-pointer text-primary-variant hover:text-primary" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent>
|
|
||||||
<DropdownMenuLabel>{t("trainFaceAs")}</DropdownMenuLabel>
|
|
||||||
{faceNames.map((faceName) => (
|
|
||||||
<DropdownMenuItem
|
|
||||||
key={faceName}
|
|
||||||
className="cursor-pointer capitalize"
|
|
||||||
onClick={() => onTrainAttempt(faceName)}
|
|
||||||
>
|
|
||||||
{faceName}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
))}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
<TooltipContent>{t("trainFace")}</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<LuRefreshCw
|
|
||||||
className="size-5 cursor-pointer text-primary-variant hover:text-primary"
|
|
||||||
onClick={() => onReprocess()}
|
|
||||||
/>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>{t("button.reprocessFace")}</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user