mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-31 12:24:55 +03:00
Design optimizations
This commit is contained in:
parent
f3b1769dd8
commit
4f83f6a021
@ -52,7 +52,6 @@
|
|||||||
"categorizeImage": "Classify Image",
|
"categorizeImage": "Classify Image",
|
||||||
"wizard": {
|
"wizard": {
|
||||||
"title": "Create New Classification",
|
"title": "Create New Classification",
|
||||||
"description": "Create a new state or object classification model.",
|
|
||||||
"steps": {
|
"steps": {
|
||||||
"nameAndDefine": "Name & Define",
|
"nameAndDefine": "Name & Define",
|
||||||
"stateArea": "State Area",
|
"stateArea": "State Area",
|
||||||
@ -60,6 +59,7 @@
|
|||||||
"train": "Train"
|
"train": "Train"
|
||||||
},
|
},
|
||||||
"step1": {
|
"step1": {
|
||||||
|
"description": "Create a new state or object classification model.",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"namePlaceholder": "Enter model name...",
|
"namePlaceholder": "Enter model name...",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
|
|||||||
@ -150,8 +150,16 @@ export default function ClassificationModelWizardDialog({
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t("wizard.title")}</DialogTitle>
|
<DialogTitle>{t("wizard.title")}</DialogTitle>
|
||||||
{wizardState.currentStep === 0 && (
|
{wizardState.currentStep === 0 && (
|
||||||
<DialogDescription>{t("wizard.description")}</DialogDescription>
|
<DialogDescription>
|
||||||
|
{t("wizard.step1.description")}
|
||||||
|
</DialogDescription>
|
||||||
)}
|
)}
|
||||||
|
{wizardState.currentStep === 1 &&
|
||||||
|
wizardState.step1Data?.modelType === "state" && (
|
||||||
|
<DialogDescription>
|
||||||
|
{t("wizard.step2.description")}
|
||||||
|
</DialogDescription>
|
||||||
|
)}
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="pb-4">
|
<div className="pb-4">
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import Konva from "konva";
|
|||||||
import { useResizeObserver } from "@/hooks/resize-observer";
|
import { useResizeObserver } from "@/hooks/resize-observer";
|
||||||
import { useApiHost } from "@/api";
|
import { useApiHost } from "@/api";
|
||||||
import { resolveCameraName } from "@/hooks/use-camera-friendly-name";
|
import { resolveCameraName } from "@/hooks/use-camera-friendly-name";
|
||||||
|
import Heading from "@/components/ui/heading";
|
||||||
|
|
||||||
export type CameraAreaConfig = {
|
export type CameraAreaConfig = {
|
||||||
camera: string;
|
camera: string;
|
||||||
@ -45,6 +46,7 @@ export default function Step2StateArea({
|
|||||||
);
|
);
|
||||||
const [selectedCameraIndex, setSelectedCameraIndex] = useState<number>(0);
|
const [selectedCameraIndex, setSelectedCameraIndex] = useState<number>(0);
|
||||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||||
|
const [imageLoaded, setImageLoaded] = useState(false);
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const imageRef = useRef<HTMLImageElement>(null);
|
const imageRef = useRef<HTMLImageElement>(null);
|
||||||
@ -108,15 +110,39 @@ export default function Step2StateArea({
|
|||||||
|
|
||||||
const handleAddCamera = useCallback(
|
const handleAddCamera = useCallback(
|
||||||
(cameraName: string) => {
|
(cameraName: string) => {
|
||||||
|
// Calculate a square crop in pixel space
|
||||||
|
const camera = config?.cameras[cameraName];
|
||||||
|
if (!camera) return;
|
||||||
|
|
||||||
|
const cameraAspect = camera.detect.width / camera.detect.height;
|
||||||
|
const cropSize = 0.3;
|
||||||
|
let x1, y1, x2, y2;
|
||||||
|
|
||||||
|
if (cameraAspect >= 1) {
|
||||||
|
const pixelSize = cropSize * camera.detect.height;
|
||||||
|
const normalizedWidth = pixelSize / camera.detect.width;
|
||||||
|
x1 = (1 - normalizedWidth) / 2;
|
||||||
|
y1 = (1 - cropSize) / 2;
|
||||||
|
x2 = x1 + normalizedWidth;
|
||||||
|
y2 = y1 + cropSize;
|
||||||
|
} else {
|
||||||
|
const pixelSize = cropSize * camera.detect.width;
|
||||||
|
const normalizedHeight = pixelSize / camera.detect.height;
|
||||||
|
x1 = (1 - cropSize) / 2;
|
||||||
|
y1 = (1 - normalizedHeight) / 2;
|
||||||
|
x2 = x1 + cropSize;
|
||||||
|
y2 = y1 + normalizedHeight;
|
||||||
|
}
|
||||||
|
|
||||||
const newArea: CameraAreaConfig = {
|
const newArea: CameraAreaConfig = {
|
||||||
camera: cameraName,
|
camera: cameraName,
|
||||||
crop: [0.385, 0.385, 0.535, 0.535],
|
crop: [x1, y1, x2, y2],
|
||||||
};
|
};
|
||||||
setCameraAreas([...cameraAreas, newArea]);
|
setCameraAreas([...cameraAreas, newArea]);
|
||||||
setSelectedCameraIndex(cameraAreas.length);
|
setSelectedCameraIndex(cameraAreas.length);
|
||||||
setIsPopoverOpen(false);
|
setIsPopoverOpen(false);
|
||||||
},
|
},
|
||||||
[cameraAreas],
|
[cameraAreas, config],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleRemoveCamera = useCallback(
|
const handleRemoveCamera = useCallback(
|
||||||
@ -142,17 +168,27 @@ export default function Step2StateArea({
|
|||||||
[cameraAreas, selectedCameraIndex],
|
[cameraAreas, selectedCameraIndex],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setImageLoaded(false);
|
||||||
|
}, [selectedCamera]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const rect = rectRef.current;
|
const rect = rectRef.current;
|
||||||
const transformer = transformerRef.current;
|
const transformer = transformerRef.current;
|
||||||
|
|
||||||
if (rect && transformer) {
|
if (
|
||||||
|
rect &&
|
||||||
|
transformer &&
|
||||||
|
selectedCamera &&
|
||||||
|
imageSize.width > 0 &&
|
||||||
|
imageLoaded
|
||||||
|
) {
|
||||||
rect.scaleX(1);
|
rect.scaleX(1);
|
||||||
rect.scaleY(1);
|
rect.scaleY(1);
|
||||||
transformer.nodes([rect]);
|
transformer.nodes([rect]);
|
||||||
transformer.getLayer()?.batchDraw();
|
transformer.getLayer()?.batchDraw();
|
||||||
}
|
}
|
||||||
}, [selectedCamera, imageSize]);
|
}, [selectedCamera, imageSize, imageLoaded]);
|
||||||
|
|
||||||
const handleRectChange = useCallback(() => {
|
const handleRectChange = useCallback(() => {
|
||||||
const rect = rectRef.current;
|
const rect = rectRef.current;
|
||||||
@ -186,10 +222,6 @@ export default function Step2StateArea({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
{t("wizard.step2.description")}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-4 overflow-hidden">
|
<div className="flex gap-4 overflow-hidden">
|
||||||
<div className="flex w-64 flex-shrink-0 flex-col gap-2 overflow-y-auto rounded-lg bg-secondary p-4">
|
<div className="flex w-64 flex-shrink-0 flex-col gap-2 overflow-y-auto rounded-lg bg-secondary p-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@ -205,7 +237,7 @@ export default function Step2StateArea({
|
|||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-6 w-6 p-0"
|
className="size-6 p-0"
|
||||||
aria-label="Add camera"
|
aria-label="Add camera"
|
||||||
>
|
>
|
||||||
<MdAddBox className="size-6 text-primary" />
|
<MdAddBox className="size-6 text-primary" />
|
||||||
@ -218,17 +250,17 @@ export default function Step2StateArea({
|
|||||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h4 className="text-sm font-medium">
|
<Heading as="h4" className="text-sm font-medium">
|
||||||
{t("wizard.step2.selectCamera")}
|
{t("wizard.step2.selectCamera")}
|
||||||
</h4>
|
</Heading>
|
||||||
<div className="scrollbar-container flex max-h-64 flex-col gap-1 overflow-y-auto">
|
<div className="scrollbar-container flex max-h-[30vh] flex-col gap-1 overflow-y-auto">
|
||||||
{availableCameras.map((cam) => (
|
{availableCameras.map((cam) => (
|
||||||
<Button
|
<Button
|
||||||
key={cam.name}
|
key={cam.name}
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="capit h-auto justify-start px-3 py-2 capitalize"
|
className="h-auto justify-start p-2 capitalize text-primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleAddCamera(cam.name);
|
handleAddCamera(cam.name);
|
||||||
}}
|
}}
|
||||||
@ -286,130 +318,129 @@ export default function Step2StateArea({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-1 items-center justify-center overflow-hidden rounded-lg p-4">
|
<div className="flex flex-1 items-center justify-center overflow-hidden rounded-lg p-4">
|
||||||
{selectedCamera && selectedCameraConfig ? (
|
<div
|
||||||
<div
|
ref={containerRef}
|
||||||
ref={containerRef}
|
className="flex items-center justify-center"
|
||||||
className="flex items-center justify-center"
|
style={{
|
||||||
style={{
|
width: "100%",
|
||||||
width: "100%",
|
aspectRatio: "16 / 9",
|
||||||
aspectRatio: "16 / 9",
|
maxHeight: "100%",
|
||||||
maxHeight: "100%",
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{selectedCamera && selectedCameraConfig && imageSize.width > 0 ? (
|
||||||
{imageSize.width > 0 && (
|
<div
|
||||||
<div
|
style={{
|
||||||
style={{
|
width: imageSize.width,
|
||||||
width: imageSize.width,
|
height: imageSize.height,
|
||||||
height: imageSize.height,
|
position: "relative",
|
||||||
position: "relative",
|
}}
|
||||||
}}
|
>
|
||||||
|
<img
|
||||||
|
ref={imageRef}
|
||||||
|
src={`${apiHost}api/${selectedCamera.camera}/latest.jpg?h=500`}
|
||||||
|
alt={resolveCameraName(config, selectedCamera.camera)}
|
||||||
|
className="h-full w-full object-contain"
|
||||||
|
onLoad={() => setImageLoaded(true)}
|
||||||
|
/>
|
||||||
|
<Stage
|
||||||
|
ref={stageRef}
|
||||||
|
width={imageSize.width}
|
||||||
|
height={imageSize.height}
|
||||||
|
className="absolute inset-0"
|
||||||
>
|
>
|
||||||
<img
|
<Layer>
|
||||||
ref={imageRef}
|
<Rect
|
||||||
src={`${apiHost}api/${selectedCamera.camera}/latest.jpg?h=500`}
|
ref={rectRef}
|
||||||
alt={resolveCameraName(config, selectedCamera.camera)}
|
x={selectedCamera.crop[0] * imageSize.width}
|
||||||
className="h-full w-full object-contain"
|
y={selectedCamera.crop[1] * imageSize.height}
|
||||||
/>
|
width={
|
||||||
<Stage
|
(selectedCamera.crop[2] - selectedCamera.crop[0]) *
|
||||||
ref={stageRef}
|
imageSize.width
|
||||||
width={imageSize.width}
|
}
|
||||||
height={imageSize.height}
|
height={
|
||||||
className="absolute inset-0"
|
(selectedCamera.crop[3] - selectedCamera.crop[1]) *
|
||||||
>
|
imageSize.height
|
||||||
<Layer>
|
}
|
||||||
<Rect
|
stroke="#3b82f6"
|
||||||
ref={rectRef}
|
strokeWidth={2}
|
||||||
x={selectedCamera.crop[0] * imageSize.width}
|
fill="rgba(59, 130, 246, 0.1)"
|
||||||
y={selectedCamera.crop[1] * imageSize.height}
|
draggable
|
||||||
width={
|
dragBoundFunc={(pos) => {
|
||||||
(selectedCamera.crop[2] - selectedCamera.crop[0]) *
|
const rect = rectRef.current;
|
||||||
imageSize.width
|
if (!rect) return pos;
|
||||||
}
|
|
||||||
height={
|
|
||||||
(selectedCamera.crop[3] - selectedCamera.crop[1]) *
|
|
||||||
imageSize.height
|
|
||||||
}
|
|
||||||
stroke="#3b82f6"
|
|
||||||
strokeWidth={2}
|
|
||||||
fill="rgba(59, 130, 246, 0.1)"
|
|
||||||
draggable
|
|
||||||
dragBoundFunc={(pos) => {
|
|
||||||
const rect = rectRef.current;
|
|
||||||
if (!rect) return pos;
|
|
||||||
|
|
||||||
const size = rect.width();
|
const size = rect.width();
|
||||||
const x = Math.max(
|
const x = Math.max(
|
||||||
0,
|
0,
|
||||||
Math.min(pos.x, imageSize.width - size),
|
Math.min(pos.x, imageSize.width - size),
|
||||||
);
|
);
|
||||||
const y = Math.max(
|
const y = Math.max(
|
||||||
0,
|
0,
|
||||||
Math.min(pos.y, imageSize.height - size),
|
Math.min(pos.y, imageSize.height - size),
|
||||||
);
|
);
|
||||||
|
|
||||||
return { x, y };
|
return { x, y };
|
||||||
}}
|
}}
|
||||||
onDragEnd={handleRectChange}
|
onDragEnd={handleRectChange}
|
||||||
onTransformEnd={handleRectChange}
|
onTransformEnd={handleRectChange}
|
||||||
/>
|
/>
|
||||||
<Transformer
|
<Transformer
|
||||||
ref={transformerRef}
|
ref={transformerRef}
|
||||||
rotateEnabled={false}
|
rotateEnabled={false}
|
||||||
enabledAnchors={[
|
enabledAnchors={[
|
||||||
"top-left",
|
"top-left",
|
||||||
"top-right",
|
"top-right",
|
||||||
"bottom-left",
|
"bottom-left",
|
||||||
"bottom-right",
|
"bottom-right",
|
||||||
]}
|
]}
|
||||||
boundBoxFunc={(_oldBox, newBox) => {
|
boundBoxFunc={(_oldBox, newBox) => {
|
||||||
const minSize = 50;
|
const minSize = 50;
|
||||||
const maxSize = Math.min(
|
const maxSize = Math.min(
|
||||||
imageSize.width,
|
imageSize.width,
|
||||||
imageSize.height,
|
imageSize.height,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Clamp dimensions to stage bounds first
|
// Clamp dimensions to stage bounds first
|
||||||
const clampedWidth = Math.max(
|
const clampedWidth = Math.max(
|
||||||
minSize,
|
minSize,
|
||||||
Math.min(newBox.width, maxSize),
|
Math.min(newBox.width, maxSize),
|
||||||
);
|
);
|
||||||
const clampedHeight = Math.max(
|
const clampedHeight = Math.max(
|
||||||
minSize,
|
minSize,
|
||||||
Math.min(newBox.height, maxSize),
|
Math.min(newBox.height, maxSize),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Enforce square using average
|
// Enforce square using average
|
||||||
const size = (clampedWidth + clampedHeight) / 2;
|
const size = (clampedWidth + clampedHeight) / 2;
|
||||||
|
|
||||||
// Clamp position to keep square within bounds
|
// Clamp position to keep square within bounds
|
||||||
const x = Math.max(
|
const x = Math.max(
|
||||||
0,
|
0,
|
||||||
Math.min(newBox.x, imageSize.width - size),
|
Math.min(newBox.x, imageSize.width - size),
|
||||||
);
|
);
|
||||||
const y = Math.max(
|
const y = Math.max(
|
||||||
0,
|
0,
|
||||||
Math.min(newBox.y, imageSize.height - size),
|
Math.min(newBox.y, imageSize.height - size),
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...newBox,
|
...newBox,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Layer>
|
</Layer>
|
||||||
</Stage>
|
</Stage>
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : (
|
||||||
</div>
|
<div className="flex items-center justify-center text-muted-foreground">
|
||||||
) : (
|
{t("wizard.step2.selectCameraPrompt")}
|
||||||
<div className="flex h-full items-center justify-center text-muted-foreground">
|
</div>
|
||||||
{t("wizard.step2.selectCameraPrompt")}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user