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)
|
||||
rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
||||
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:
|
||||
shutil.move(training_file, new_file)
|
||||
shutil.move(training_file, os.path.join(new_file_folder, new_name))
|
||||
else:
|
||||
try:
|
||||
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
|
||||
y2 = y1 + int(face_box[3] * detect_config.height) - 4
|
||||
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.clear_face_classifier()
|
||||
|
||||
@ -3,6 +3,7 @@ import TimeAgo from "@/components/dynamic/TimeAgo";
|
||||
import AddFaceIcon from "@/components/icons/AddFaceIcon";
|
||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||
import CreateFaceWizardDialog from "@/components/overlay/detail/FaceCreateWizardDialog";
|
||||
import TextEntryDialog from "@/components/overlay/dialog/TextEntryDialog";
|
||||
import UploadImageDialog from "@/components/overlay/dialog/UploadImageDialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@ -42,6 +43,7 @@ import { isDesktop, isMobile } from "react-device-detect";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
LuImagePlus,
|
||||
LuPlus,
|
||||
LuRefreshCw,
|
||||
LuScanFace,
|
||||
LuSearch,
|
||||
@ -605,6 +607,8 @@ function FaceAttempt({
|
||||
|
||||
// interaction
|
||||
|
||||
const [newFace, setNewFace] = useState(false);
|
||||
|
||||
const imgRef = useRef<HTMLImageElement | null>(null);
|
||||
|
||||
useContextMenu(imgRef, () => {
|
||||
@ -663,76 +667,99 @@ function FaceAttempt({
|
||||
}, [data, onRefresh, t]);
|
||||
|
||||
return (
|
||||
<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)}
|
||||
<>
|
||||
{newFace && (
|
||||
<TextEntryDialog
|
||||
open={true}
|
||||
setOpen={setNewFace}
|
||||
title="Create New Face"
|
||||
onSave={(newName) => onTrainAttempt(newName)}
|
||||
/>
|
||||
<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 className="p-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="capitalize">{data.name}</div>
|
||||
<div
|
||||
className={cn(
|
||||
"",
|
||||
scoreStatus == "match" && "text-success",
|
||||
scoreStatus == "potential" && "text-orange-400",
|
||||
scoreStatus == "unknown" && "text-danger",
|
||||
)}
|
||||
>
|
||||
{Math.round(data.score * 100)}%
|
||||
<div className="p-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="capitalize">{data.name}</div>
|
||||
<div
|
||||
className={cn(
|
||||
"",
|
||||
scoreStatus == "match" && "text-success",
|
||||
scoreStatus == "potential" && "text-orange-400",
|
||||
scoreStatus == "unknown" && "text-danger",
|
||||
)}
|
||||
>
|
||||
{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 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user