diff --git a/web/src/components/overlay/DebugDrawingLayer.tsx b/web/src/components/overlay/DebugDrawingLayer.tsx new file mode 100644 index 000000000..b45ef3f81 --- /dev/null +++ b/web/src/components/overlay/DebugDrawingLayer.tsx @@ -0,0 +1,177 @@ +import React, { useState, useRef, useCallback, useMemo } from "react"; +import { Stage, Layer, Rect } from "react-konva"; +import { KonvaEventObject } from "konva/lib/Node"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import Konva from "konva"; +import { useResizeObserver } from "@/hooks/resize-observer"; + +type DebugDrawingLayerProps = { + containerRef: React.RefObject; + cameraWidth: number; + cameraHeight: number; +}; + +function DebugDrawingLayer({ + containerRef, + cameraWidth, + cameraHeight, +}: DebugDrawingLayerProps) { + const [rectangle, setRectangle] = useState<{ + x: number; + y: number; + width: number; + height: number; + } | null>(null); + const [isDrawing, setIsDrawing] = useState(false); + const [showPopover, setShowPopover] = useState(false); + const stageRef = useRef(null); + + const [{ width: containerWidth }] = useResizeObserver(containerRef); + + const imageSize = useMemo(() => { + const aspectRatio = cameraWidth / cameraHeight; + const imageWidth = containerWidth; + const imageHeight = imageWidth / aspectRatio; + return { width: imageWidth, height: imageHeight }; + }, [containerWidth, cameraWidth, cameraHeight]); + + const handleMouseDown = (e: KonvaEventObject) => { + const pos = e.target.getStage()?.getPointerPosition(); + if (pos) { + setIsDrawing(true); + setRectangle({ x: pos.x, y: pos.y, width: 0, height: 0 }); + } + }; + + const handleMouseMove = (e: KonvaEventObject) => { + if (!isDrawing) return; + + const pos = e.target.getStage()?.getPointerPosition(); + if (pos && rectangle) { + setRectangle({ + ...rectangle, + width: pos.x - rectangle.x, + height: pos.y - rectangle.y, + }); + } + }; + + const handleMouseUp = () => { + setIsDrawing(false); + if (rectangle) { + setShowPopover(true); + } + }; + + const convertToRealCoordinates = useCallback( + (x: number, y: number, width: number, height: number) => { + const scaleX = cameraWidth / imageSize.width; + const scaleY = cameraHeight / imageSize.height; + return { + x: x * scaleX, + y: y * scaleY, + width: width * scaleX, + height: height * scaleY, + }; + }, + [cameraWidth, cameraHeight, imageSize.width, imageSize.height], + ); + + const calculateArea = useCallback(() => { + if (!rectangle) return 0; + const { width, height } = convertToRealCoordinates( + 0, + 0, + Math.abs(rectangle.width), + Math.abs(rectangle.height), + ); + return width * height; + }, [rectangle, convertToRealCoordinates]); + + const calculateAreaPercentage = useCallback(() => { + if (!rectangle) return 0; + const { width, height } = convertToRealCoordinates( + 0, + 0, + Math.abs(rectangle.width), + Math.abs(rectangle.height), + ); + return (width * height) / (cameraWidth * cameraHeight); + }, [rectangle, convertToRealCoordinates, cameraWidth, cameraHeight]); + + const calculateRatio = useCallback(() => { + if (!rectangle) return 0; + const { width, height } = convertToRealCoordinates( + 0, + 0, + Math.abs(rectangle.width), + Math.abs(rectangle.height), + ); + return width / height; + }, [rectangle, convertToRealCoordinates]); + + return ( +
+ + + {rectangle && ( + + )} + + + {showPopover && rectangle && ( + + +
+ + +
+
+ Area:{" "} + + px: {calculateArea().toFixed(0)} + + + %: {calculateAreaPercentage().toFixed(4)} + +
+
+ Ratio:{" "} + + {" "} + {calculateRatio().toFixed(2)} + +
+
+
+ + )} +
+ ); +} + +export default DebugDrawingLayer; diff --git a/web/src/views/settings/ObjectSettingsView.tsx b/web/src/views/settings/ObjectSettingsView.tsx index 0784f2301..ea1083ec1 100644 --- a/web/src/views/settings/ObjectSettingsView.tsx +++ b/web/src/views/settings/ObjectSettingsView.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import AutoUpdatingCameraImage from "@/components/camera/AutoUpdatingCameraImage"; import { CameraConfig, FrigateConfig } from "@/types/frigateConfig"; @@ -23,6 +23,9 @@ import { getIconForLabel } from "@/utils/iconUtil"; import { capitalizeFirstLetter } from "@/utils/stringUtil"; import { LuExternalLink, LuInfo } from "react-icons/lu"; import { Link } from "react-router-dom"; +import DebugDrawingLayer from "@/components/overlay/DebugDrawingLayer"; +import { Separator } from "@/components/ui/separator"; +import { isDesktop } from "react-device-detect"; type ObjectSettingsViewProps = { selectedCamera?: string; @@ -37,6 +40,8 @@ export default function ObjectSettingsView({ }: ObjectSettingsViewProps) { const { data: config } = useSWR("config"); + const containerRef = useRef(null); + const DEBUG_OPTIONS = [ { param: "bbox", @@ -130,6 +135,12 @@ export default function ObjectSettingsView({ [options, setOptions], ); + const [debugDraw, setDebugDraw] = useState(false); + + useEffect(() => { + setDebugDraw(false); + }, [selectedCamera]); + const cameraConfig = useMemo(() => { if (config && selectedCamera) { return config.cameras[selectedCamera]; @@ -234,7 +245,7 @@ export default function ObjectSettingsView({ Info
- + {info} @@ -256,6 +267,62 @@ export default function ObjectSettingsView({ ))} + {isDesktop && ( + <> + +
+
+
+ + + + +
+ + Info +
+
+ + Enable this option to draw a rectangle on the + camera image to show its area and ratio. These + values can then be used to set object shape filter + parameters in your config. +
+ + Read the documentation{" "} + + +
+
+
+
+
+ Draw a rectangle on the image to view area and ratio + details +
+
+ { + setDebugDraw(isChecked); + }} + /> +
+ + )} @@ -267,7 +334,7 @@ export default function ObjectSettingsView({ {cameraConfig ? (
-
+
+ {debugDraw && ( + + )}
) : (