working motion masks

This commit is contained in:
Josh Hawkins 2024-04-16 16:03:25 -05:00
parent 1982fa3461
commit 93b206d6b4
7 changed files with 275 additions and 67 deletions

View File

@ -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}
/>

View File

@ -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>

View File

@ -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>

View File

@ -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,

View File

@ -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">

View File

@ -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;
};

View File

@ -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], []);
};