mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-11 13:45:25 +03:00
working motion masks
This commit is contained in:
parent
1982fa3461
commit
93b206d6b4
@ -4,7 +4,7 @@ import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { PolygonCanvas } from "./PolygonCanvas";
|
||||
import { Polygon, PolygonType } from "@/types/canvas";
|
||||
import { interpolatePoints } from "@/utils/canvasUtil";
|
||||
import { interpolatePoints, parseCoordinates } from "@/utils/canvasUtil";
|
||||
import { Skeleton } from "../ui/skeleton";
|
||||
import { useResizeObserver } from "@/hooks/resize-observer";
|
||||
import { LuExternalLink, LuInfo, LuPlus } from "react-icons/lu";
|
||||
@ -25,19 +25,6 @@ import ObjectMaskEditPane from "./ObjectMaskEditPane";
|
||||
import PolygonItem from "./PolygonItem";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const parseCoordinates = (coordinatesString: string) => {
|
||||
const coordinates = coordinatesString.split(",");
|
||||
const points = [];
|
||||
|
||||
for (let i = 0; i < coordinates.length; i += 2) {
|
||||
const x = parseFloat(coordinates[i]);
|
||||
const y = parseFloat(coordinates[i + 1]);
|
||||
points.push([x, y]);
|
||||
}
|
||||
|
||||
return points;
|
||||
};
|
||||
|
||||
// export type ZoneObjects = {
|
||||
// camera: string;
|
||||
// zoneName: string;
|
||||
@ -299,8 +286,9 @@ export default function MasksAndZones({
|
||||
useEffect(() => {
|
||||
if (cameraConfig && containerRef.current && scaledWidth && scaledHeight) {
|
||||
const zones = Object.entries(cameraConfig.zones).map(
|
||||
([name, zoneData]) => ({
|
||||
([name, zoneData], index) => ({
|
||||
type: "zone" as PolygonType,
|
||||
typeIndex: index,
|
||||
camera: cameraConfig.name,
|
||||
name,
|
||||
objects: zoneData.objects,
|
||||
@ -317,28 +305,32 @@ export default function MasksAndZones({
|
||||
}),
|
||||
);
|
||||
|
||||
const motionMasks = Object.entries(cameraConfig.motion.mask).map(
|
||||
([, maskData], index) => ({
|
||||
type: "motion_mask" as PolygonType,
|
||||
camera: cameraConfig.name,
|
||||
name: `Motion Mask ${index + 1}`,
|
||||
objects: [],
|
||||
points: interpolatePoints(
|
||||
parseCoordinates(maskData),
|
||||
1,
|
||||
1,
|
||||
scaledWidth,
|
||||
scaledHeight,
|
||||
),
|
||||
isFinished: true,
|
||||
// isUnsaved: false,
|
||||
color: [0, 0, 255],
|
||||
}),
|
||||
);
|
||||
// this can be an array or a string
|
||||
const motionMasks = Object.entries(
|
||||
Array.isArray(cameraConfig.motion.mask)
|
||||
? cameraConfig.motion.mask
|
||||
: [cameraConfig.motion.mask],
|
||||
).map(([, maskData], index) => ({
|
||||
type: "motion_mask" as PolygonType,
|
||||
typeIndex: index,
|
||||
camera: cameraConfig.name,
|
||||
name: `Motion Mask ${index + 1}`,
|
||||
objects: [],
|
||||
points: interpolatePoints(
|
||||
parseCoordinates(maskData),
|
||||
1,
|
||||
1,
|
||||
scaledWidth,
|
||||
scaledHeight,
|
||||
),
|
||||
isFinished: true,
|
||||
color: [0, 0, 255],
|
||||
}));
|
||||
|
||||
const globalObjectMasks = Object.entries(cameraConfig.objects.mask).map(
|
||||
([, maskData], index) => ({
|
||||
type: "object_mask" as PolygonType,
|
||||
typeIndex: index,
|
||||
camera: cameraConfig.name,
|
||||
name: `Object Mask ${index + 1} (all objects)`,
|
||||
objects: [],
|
||||
@ -365,6 +357,7 @@ export default function MasksAndZones({
|
||||
? [
|
||||
{
|
||||
type: "object_mask" as PolygonType,
|
||||
typeIndex: subIndex,
|
||||
camera: cameraConfig.name,
|
||||
name: `Object Mask ${globalObjectMasksCount + subIndex + 1} (${objectName})`,
|
||||
objects: [objectName],
|
||||
@ -411,6 +404,10 @@ export default function MasksAndZones({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [cameraConfig, containerRef, scaledHeight, scaledWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("editing polygons changed:", editingPolygons);
|
||||
}, [editingPolygons]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editPane === undefined) {
|
||||
setEditingPolygons([...allPolygons]);
|
||||
@ -469,6 +466,10 @@ export default function MasksAndZones({
|
||||
polygons={editingPolygons}
|
||||
setPolygons={setEditingPolygons}
|
||||
activePolygonIndex={activePolygonIndex}
|
||||
scaledWidth={scaledWidth}
|
||||
scaledHeight={scaledHeight}
|
||||
isLoading={isLoading}
|
||||
setIsLoading={setIsLoading}
|
||||
onCancel={handleCancel}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
@ -478,6 +479,10 @@ export default function MasksAndZones({
|
||||
polygons={editingPolygons}
|
||||
setPolygons={setEditingPolygons}
|
||||
activePolygonIndex={activePolygonIndex}
|
||||
scaledWidth={scaledWidth}
|
||||
scaledHeight={scaledHeight}
|
||||
isLoading={isLoading}
|
||||
setIsLoading={setIsLoading}
|
||||
onCancel={handleCancel}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
|
||||
@ -2,18 +2,32 @@ import Heading from "../ui/heading";
|
||||
import { Separator } from "../ui/separator";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Form, FormField, FormItem, FormMessage } from "@/components/ui/form";
|
||||
import { useMemo } from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { Polygon } from "@/types/canvas";
|
||||
import PolygonEditControls from "./PolygonEditControls";
|
||||
import { FaCheckCircle } from "react-icons/fa";
|
||||
import { Polygon } from "@/types/canvas";
|
||||
import useSWR from "swr";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import {
|
||||
flattenPoints,
|
||||
interpolatePoints,
|
||||
parseCoordinates,
|
||||
} from "@/utils/canvasUtil";
|
||||
import axios from "axios";
|
||||
import { toast } from "sonner";
|
||||
import { Toaster } from "../ui/sonner";
|
||||
|
||||
type MotionMaskEditPaneProps = {
|
||||
polygons?: Polygon[];
|
||||
setPolygons: React.Dispatch<React.SetStateAction<Polygon[]>>;
|
||||
activePolygonIndex?: number;
|
||||
scaledWidth?: number;
|
||||
scaledHeight?: number;
|
||||
isLoading: boolean;
|
||||
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
onSave?: () => void;
|
||||
onCancel?: () => void;
|
||||
};
|
||||
@ -22,9 +36,16 @@ export default function MotionMaskEditPane({
|
||||
polygons,
|
||||
setPolygons,
|
||||
activePolygonIndex,
|
||||
scaledWidth,
|
||||
scaledHeight,
|
||||
isLoading,
|
||||
setIsLoading,
|
||||
onSave,
|
||||
onCancel,
|
||||
}: MotionMaskEditPaneProps) {
|
||||
const { data: config, mutate: updateConfig } =
|
||||
useSWR<FrigateConfig>("config");
|
||||
|
||||
const polygon = useMemo(() => {
|
||||
if (polygons && activePolygonIndex !== undefined) {
|
||||
return polygons[activePolygonIndex];
|
||||
@ -33,6 +54,12 @@ export default function MotionMaskEditPane({
|
||||
}
|
||||
}, [polygons, activePolygonIndex]);
|
||||
|
||||
const cameraConfig = useMemo(() => {
|
||||
if (polygon?.camera && config) {
|
||||
return config.cameras[polygon.camera];
|
||||
}
|
||||
}, [polygon, config]);
|
||||
|
||||
const defaultName = useMemo(() => {
|
||||
if (!polygons) {
|
||||
return;
|
||||
@ -60,20 +87,121 @@ export default function MotionMaskEditPane({
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
// console.log("form values", values);
|
||||
// if (activePolygonIndex === undefined || !polygons) {
|
||||
// return;
|
||||
// }
|
||||
const saveToConfig = useCallback(async () => {
|
||||
if (!scaledWidth || !scaledHeight || !polygon || !cameraConfig) {
|
||||
return;
|
||||
}
|
||||
// console.log("loitering time", loitering_time);
|
||||
// const alertsZones = config?.cameras[camera]?.review.alerts.required_zones;
|
||||
|
||||
// const updatedPolygons = [...polygons];
|
||||
// const activePolygon = updatedPolygons[activePolygonIndex];
|
||||
// updatedPolygons[activePolygonIndex] = {
|
||||
// ...activePolygon,
|
||||
// name: defaultName ?? "foo",
|
||||
// };
|
||||
// setPolygons(updatedPolygons);
|
||||
// const detectionsZones =
|
||||
// config?.cameras[camera]?.review.detections.required_zones;
|
||||
|
||||
// console.log("out of try except", mutatedConfig);
|
||||
|
||||
const coordinates = flattenPoints(
|
||||
interpolatePoints(polygon.points, scaledWidth, scaledHeight, 1, 1),
|
||||
).join(",");
|
||||
|
||||
let index = Array.isArray(cameraConfig.motion.mask)
|
||||
? cameraConfig.motion.mask.length
|
||||
: 1;
|
||||
|
||||
console.log("are we an array?", Array.isArray(cameraConfig.motion.mask));
|
||||
console.log("index", index);
|
||||
const editingMask = polygon.name.length > 0;
|
||||
|
||||
// editing existing mask, not creating a new one
|
||||
if (editingMask) {
|
||||
index = polygon.typeIndex;
|
||||
if (polygon.name) {
|
||||
const match = polygon.name.match(/\d+/);
|
||||
if (match) {
|
||||
// index = parseInt(match[0]) - 1;
|
||||
console.log("editing, index", index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const filteredMask = Array.isArray(cameraConfig.motion.mask)
|
||||
? cameraConfig.motion.mask
|
||||
: [cameraConfig.motion.mask].filter(
|
||||
(_, currentIndex) => currentIndex !== index,
|
||||
);
|
||||
console.log("filtered", filteredMask);
|
||||
|
||||
// if (editingMask) {
|
||||
// if (index != null) {
|
||||
|
||||
// }
|
||||
// }
|
||||
filteredMask.splice(index, 0, coordinates);
|
||||
console.log("filtered after splice", filteredMask);
|
||||
|
||||
const queryString = filteredMask
|
||||
.map((pointsArray) => {
|
||||
const coordinates = flattenPoints(parseCoordinates(pointsArray)).join(
|
||||
",",
|
||||
);
|
||||
return `cameras.${polygon?.camera}.motion.mask=${coordinates}&`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
console.log("polygon", polygon);
|
||||
console.log(queryString);
|
||||
|
||||
// console.log(
|
||||
// `config/set?cameras.${polygon?.camera}.motion.mask=${coordinates}&${queryString}`,
|
||||
// );
|
||||
console.log("motion masks", cameraConfig.motion.mask);
|
||||
console.log("new coords", coordinates);
|
||||
// return;
|
||||
|
||||
axios
|
||||
.put(`config/set?${queryString}`, {
|
||||
requires_restart: 0,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.status === 200) {
|
||||
toast.success(`Zone ${name} saved.`, {
|
||||
position: "top-center",
|
||||
});
|
||||
// setChangedValue(false);
|
||||
updateConfig();
|
||||
} else {
|
||||
toast.error(`Failed to save config changes: ${res.statusText}`, {
|
||||
position: "top-center",
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(
|
||||
`Failed to save config changes: ${error.response.data.message}`,
|
||||
{ position: "top-center" },
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, [updateConfig, polygon, scaledWidth, scaledHeight, setIsLoading]);
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
if (activePolygonIndex === undefined || !values || !polygons) {
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
// polygons[activePolygonIndex].name = values.name;
|
||||
// console.log("form values", values);
|
||||
// console.log(
|
||||
// "string",
|
||||
|
||||
// flattenPoints(
|
||||
// interpolatePoints(polygon.points, scaledWidth, scaledHeight, 1, 1),
|
||||
// ).join(","),
|
||||
// );
|
||||
// console.log("active polygon", polygons[activePolygonIndex]);
|
||||
|
||||
saveToConfig();
|
||||
if (onSave) {
|
||||
onSave();
|
||||
}
|
||||
@ -85,6 +213,7 @@ export default function MotionMaskEditPane({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toaster position="top-center" />
|
||||
<Heading as="h3" className="my-2">
|
||||
{polygon.name.length ? "Edit" : "New"} Motion Mask
|
||||
</Heading>
|
||||
|
||||
@ -20,7 +20,11 @@ import { FaDrawPolygon, FaObjectGroup } from "react-icons/fa";
|
||||
import { BsPersonBoundingBox } from "react-icons/bs";
|
||||
import { HiOutlineDotsVertical, HiTrash } from "react-icons/hi";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import { toRGBColorString } from "@/utils/canvasUtil";
|
||||
import {
|
||||
flattenPoints,
|
||||
parseCoordinates,
|
||||
toRGBColorString,
|
||||
} from "@/utils/canvasUtil";
|
||||
import { Polygon, PolygonType } from "@/types/canvas";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import axios from "axios";
|
||||
@ -33,6 +37,7 @@ import { reviewQueries } from "@/utils/zoneEdutUtil";
|
||||
type PolygonItemProps = {
|
||||
polygon: Polygon;
|
||||
setAllPolygons: React.Dispatch<React.SetStateAction<Polygon[]>>;
|
||||
setReindexPolygons: React.Dispatch<React.SetStateAction<Polygon[]>>;
|
||||
index: number;
|
||||
activePolygonIndex: number | undefined;
|
||||
hoveredPolygonIndex: number | null;
|
||||
@ -86,13 +91,40 @@ export default function PolygonItem({
|
||||
cameraConfig?.review.alerts.required_zones || [],
|
||||
cameraConfig?.review.detections.required_zones || [],
|
||||
);
|
||||
url = `config/set?cameras.${polygon.camera}.zones.${polygon.name}${alertQueries}${detectionQueries}`;
|
||||
url = `cameras.${polygon.camera}.zones.${polygon.name}${alertQueries}${detectionQueries}`;
|
||||
}
|
||||
if (polygon.type == "motion_mask") {
|
||||
url = `config/set?cameras.${polygon.camera}.motion.mask`;
|
||||
console.log("deleting", polygon.typeIndex);
|
||||
if (polygon.name) {
|
||||
const match = polygon.name.match(/\d+/);
|
||||
if (match) {
|
||||
// index = parseInt(match[0]) - 1;
|
||||
console.log("deleting, index", polygon.typeIndex);
|
||||
}
|
||||
}
|
||||
|
||||
const filteredMask = (
|
||||
Array.isArray(cameraConfig.motion.mask)
|
||||
? cameraConfig.motion.mask
|
||||
: [cameraConfig.motion.mask]
|
||||
).filter((_, currentIndex) => currentIndex !== polygon.typeIndex);
|
||||
console.log(filteredMask);
|
||||
|
||||
url = filteredMask
|
||||
.map((pointsArray) => {
|
||||
const coordinates = flattenPoints(
|
||||
parseCoordinates(pointsArray),
|
||||
).join(",");
|
||||
return `cameras.${polygon?.camera}.motion.mask=${coordinates}&`;
|
||||
})
|
||||
.join("");
|
||||
console.log(url);
|
||||
|
||||
// return;
|
||||
// url = `config/set?cameras.${polygon.camera}.motion.mask`;
|
||||
}
|
||||
axios
|
||||
.put(url, { requires_restart: 0 })
|
||||
await axios
|
||||
.put(`config/set?${url}`, { requires_restart: 0 })
|
||||
.then((res) => {
|
||||
if (res.status === 200) {
|
||||
toast.success(`${polygon?.name} has been deleted.`, {
|
||||
@ -116,12 +148,35 @@ export default function PolygonItem({
|
||||
// setIsLoading(false);
|
||||
});
|
||||
},
|
||||
[updateConfig],
|
||||
[updateConfig, cameraConfig],
|
||||
);
|
||||
|
||||
const handleDelete = (index: number) => {
|
||||
const reindexPolygons = (arr: Polygon[]): Polygon[] => {
|
||||
const typeCounters: { [type: string]: number } = {};
|
||||
|
||||
return arr.map((obj) => {
|
||||
if (!typeCounters[obj.type]) {
|
||||
typeCounters[obj.type] = 0;
|
||||
}
|
||||
|
||||
const newObj: Polygon = {
|
||||
...obj,
|
||||
typeIndex: typeCounters[obj.type],
|
||||
};
|
||||
typeCounters[obj.type]++;
|
||||
return newObj;
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = (type: string, typeIndex: number) => {
|
||||
setAllPolygons((oldPolygons) => {
|
||||
return oldPolygons.filter((_, i) => i !== index);
|
||||
const filteredPolygons = oldPolygons.filter(
|
||||
(polygon) =>
|
||||
!(polygon.type === type && polygon.typeIndex === typeIndex),
|
||||
);
|
||||
console.log("filtered", filteredPolygons);
|
||||
// console.log("reindexed", reindexPolygons(filteredPolygons));
|
||||
return filteredPolygons;
|
||||
});
|
||||
setActivePolygonIndex(undefined);
|
||||
saveToConfig(polygon);
|
||||
@ -176,7 +231,9 @@ export default function PolygonItem({
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={() => handleDelete(index)}>
|
||||
<AlertDialogAction
|
||||
onClick={() => handleDelete(polygon.type, polygon.typeIndex)}
|
||||
>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
|
||||
@ -18,7 +18,7 @@ import { isMobile } from "react-device-detect";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { Polygon } from "@/types/canvas";
|
||||
import { FormValuesType, Polygon } from "@/types/canvas";
|
||||
import { reviewQueries } from "@/utils/zoneEdutUtil";
|
||||
import { Switch } from "../ui/switch";
|
||||
import { Label } from "../ui/label";
|
||||
@ -158,15 +158,6 @@ export default function ZoneEditPane({
|
||||
});
|
||||
|
||||
// const [changedValue, setChangedValue] = useState(false);
|
||||
type FormValuesType = {
|
||||
name: string;
|
||||
inertia: number;
|
||||
loitering_time: number;
|
||||
isFinished: boolean;
|
||||
objects: string[];
|
||||
review_alerts: boolean;
|
||||
review_detections: boolean;
|
||||
};
|
||||
|
||||
// const requiredDetectionZones = useMemo(
|
||||
// () => cameraConfig?.review.detections.required_zones,
|
||||
|
||||
@ -56,9 +56,11 @@ export default function Settings() {
|
||||
|
||||
useEffect(() => {
|
||||
if (cameras) {
|
||||
// TODO: fixme
|
||||
setSelectedCamera(cameras[0].name);
|
||||
console.log("setting selected cam");
|
||||
}
|
||||
}, [cameras]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="size-full p-2 flex flex-col">
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
export type PolygonType = "zone" | "motion_mask" | "object_mask";
|
||||
|
||||
export type Polygon = {
|
||||
typeIndex: number;
|
||||
camera: string;
|
||||
name: string;
|
||||
type: PolygonType;
|
||||
@ -10,3 +11,13 @@ export type Polygon = {
|
||||
// isUnsaved: boolean;
|
||||
color: number[];
|
||||
};
|
||||
|
||||
export type FormValuesType = {
|
||||
name: string;
|
||||
inertia: number;
|
||||
loitering_time: number;
|
||||
isFinished: boolean;
|
||||
objects: string[];
|
||||
review_alerts: boolean;
|
||||
review_detections: boolean;
|
||||
};
|
||||
|
||||
@ -64,6 +64,19 @@ export const interpolatePoints = (
|
||||
return newPoints;
|
||||
};
|
||||
|
||||
export const parseCoordinates = (coordinatesString: string) => {
|
||||
const coordinates = coordinatesString.split(",");
|
||||
const points = [];
|
||||
|
||||
for (let i = 0; i < coordinates.length; i += 2) {
|
||||
const x = parseFloat(coordinates[i]);
|
||||
const y = parseFloat(coordinates[i + 1]);
|
||||
points.push([x, y]);
|
||||
}
|
||||
|
||||
return points;
|
||||
};
|
||||
|
||||
export const flattenPoints = (points: number[][]): number[] => {
|
||||
return points.reduce((acc, point) => [...acc, ...point], []);
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user