mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-08-02 17:12:16 +03:00
Masks and zones improvements (#22163)
* migrator and runtime config changes * component changes to use rasterized_mask * frontend * convert none to empty string for config save * i18n * update tests * add enabled config to zones * zones frontend * i18n * docs * tweaks * use dashed stroke to indicate disabled * allow toggle from icon * use filelock to ensure atomic config updates from endpoint * enforce atomic config update in the frontend * toggle via mqtt * fix global object masks * correctly handle global object masks in dispatcher * ws hooks * render masks and zones based on ws enabled state * use enabled_in_config for zones and masks * frontend for enabled_in_config * tweaks * i18n * publish websocket on config save * i18n tweaks * pydantic title and description * i18n generation * tweaks * fix typing
This commit is contained in:
@@ -34,7 +34,6 @@ import { useSearchEffect } from "@/hooks/use-overlay-state";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useDocDomain } from "@/hooks/use-doc-domain";
|
||||
import { getTranslatedLabel } from "@/utils/i18n";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type MasksAndZoneViewProps = {
|
||||
@@ -54,6 +53,9 @@ export default function MasksAndZonesView({
|
||||
const [allPolygons, setAllPolygons] = useState<Polygon[]>([]);
|
||||
const [editingPolygons, setEditingPolygons] = useState<Polygon[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [loadingPolygonIndex, setLoadingPolygonIndex] = useState<
|
||||
number | undefined
|
||||
>(undefined);
|
||||
const [activePolygonIndex, setActivePolygonIndex] = useState<
|
||||
number | undefined
|
||||
>(undefined);
|
||||
@@ -170,6 +172,7 @@ export default function MasksAndZonesView({
|
||||
objects: [],
|
||||
camera: selectedCamera,
|
||||
color: polygonColor,
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
};
|
||||
@@ -231,6 +234,8 @@ export default function MasksAndZonesView({
|
||||
camera: cameraConfig.name,
|
||||
name,
|
||||
friendly_name: zoneData.friendly_name,
|
||||
enabled: zoneData.enabled,
|
||||
enabled_in_config: zoneData.enabled_in_config,
|
||||
objects: zoneData.objects,
|
||||
points: interpolatePoints(
|
||||
parseCoordinates(zoneData.coordinates),
|
||||
@@ -250,102 +255,93 @@ export default function MasksAndZonesView({
|
||||
let globalObjectMasks: Polygon[] = [];
|
||||
let objectMasks: Polygon[] = [];
|
||||
|
||||
// this can be an array or a string
|
||||
motionMasks = (
|
||||
Array.isArray(cameraConfig.motion.mask)
|
||||
? cameraConfig.motion.mask
|
||||
: cameraConfig.motion.mask
|
||||
? [cameraConfig.motion.mask]
|
||||
: []
|
||||
).map((maskData, index) => ({
|
||||
type: "motion_mask" as PolygonType,
|
||||
typeIndex: index,
|
||||
camera: cameraConfig.name,
|
||||
name: t("masksAndZones.motionMaskLabel", {
|
||||
number: index + 1,
|
||||
// Motion masks are a dict with mask_id as key
|
||||
motionMasks = Object.entries(cameraConfig.motion.mask || {}).map(
|
||||
([maskId, maskData], index) => ({
|
||||
type: "motion_mask" as PolygonType,
|
||||
typeIndex: index,
|
||||
camera: cameraConfig.name,
|
||||
name: maskId,
|
||||
friendly_name: maskData.friendly_name,
|
||||
enabled: maskData.enabled,
|
||||
enabled_in_config: maskData.enabled_in_config,
|
||||
objects: [],
|
||||
points: interpolatePoints(
|
||||
parseCoordinates(maskData.coordinates),
|
||||
1,
|
||||
1,
|
||||
scaledWidth,
|
||||
scaledHeight,
|
||||
),
|
||||
distances: [],
|
||||
isFinished: true,
|
||||
color: [0, 0, 255],
|
||||
}),
|
||||
objects: [],
|
||||
points: interpolatePoints(
|
||||
parseCoordinates(maskData),
|
||||
1,
|
||||
1,
|
||||
scaledWidth,
|
||||
scaledHeight,
|
||||
),
|
||||
distances: [],
|
||||
isFinished: true,
|
||||
color: [0, 0, 255],
|
||||
}));
|
||||
);
|
||||
|
||||
const globalObjectMasksArray = Array.isArray(cameraConfig.objects.mask)
|
||||
? cameraConfig.objects.mask
|
||||
: cameraConfig.objects.mask
|
||||
? [cameraConfig.objects.mask]
|
||||
: [];
|
||||
|
||||
globalObjectMasks = globalObjectMasksArray.map((maskData, index) => ({
|
||||
type: "object_mask" as PolygonType,
|
||||
typeIndex: index,
|
||||
camera: cameraConfig.name,
|
||||
name: t("masksAndZones.objectMaskLabel", {
|
||||
number: index + 1,
|
||||
label: t("masksAndZones.zones.allObjects"),
|
||||
// Global object masks are a dict with mask_id as key
|
||||
globalObjectMasks = Object.entries(cameraConfig.objects.mask || {}).map(
|
||||
([maskId, maskData], index) => ({
|
||||
type: "object_mask" as PolygonType,
|
||||
typeIndex: index,
|
||||
camera: cameraConfig.name,
|
||||
name: maskId,
|
||||
friendly_name: maskData.friendly_name,
|
||||
enabled: maskData.enabled,
|
||||
enabled_in_config: maskData.enabled_in_config,
|
||||
objects: [],
|
||||
points: interpolatePoints(
|
||||
parseCoordinates(maskData.coordinates),
|
||||
1,
|
||||
1,
|
||||
scaledWidth,
|
||||
scaledHeight,
|
||||
),
|
||||
distances: [],
|
||||
isFinished: true,
|
||||
color: [128, 128, 128],
|
||||
}),
|
||||
objects: [],
|
||||
points: interpolatePoints(
|
||||
parseCoordinates(maskData),
|
||||
1,
|
||||
1,
|
||||
scaledWidth,
|
||||
scaledHeight,
|
||||
),
|
||||
distances: [],
|
||||
isFinished: true,
|
||||
color: [128, 128, 128],
|
||||
}));
|
||||
);
|
||||
|
||||
const globalObjectMasksCount = globalObjectMasks.length;
|
||||
let index = 0;
|
||||
let objectMaskIndex = globalObjectMasks.length;
|
||||
|
||||
objectMasks = Object.entries(cameraConfig.objects.filters)
|
||||
.filter(([, { mask }]) => mask || Array.isArray(mask))
|
||||
.flatMap(([objectName, { mask }]): Polygon[] => {
|
||||
const maskArray = Array.isArray(mask) ? mask : mask ? [mask] : [];
|
||||
return maskArray.flatMap((maskItem, subIndex) => {
|
||||
const maskItemString = maskItem;
|
||||
const newMask = {
|
||||
type: "object_mask" as PolygonType,
|
||||
typeIndex: subIndex,
|
||||
camera: cameraConfig.name,
|
||||
name: t("masksAndZones.objectMaskLabel", {
|
||||
number: globalObjectMasksCount + index + 1,
|
||||
label: getTranslatedLabel(objectName),
|
||||
}),
|
||||
objects: [objectName],
|
||||
points: interpolatePoints(
|
||||
parseCoordinates(maskItem),
|
||||
1,
|
||||
1,
|
||||
scaledWidth,
|
||||
scaledHeight,
|
||||
),
|
||||
distances: [],
|
||||
isFinished: true,
|
||||
color: [128, 128, 128],
|
||||
};
|
||||
index++;
|
||||
.filter(
|
||||
([, filterConfig]) =>
|
||||
filterConfig.mask && Object.keys(filterConfig.mask).length > 0,
|
||||
)
|
||||
.flatMap(([objectName, filterConfig]): Polygon[] => {
|
||||
return Object.entries(filterConfig.mask || {}).flatMap(
|
||||
([maskId, maskData]) => {
|
||||
// Skip if this mask is a global mask (prefixed with "global_")
|
||||
if (maskId.startsWith("global_")) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (
|
||||
globalObjectMasksArray.some(
|
||||
(globalMask) => globalMask === maskItemString,
|
||||
)
|
||||
) {
|
||||
index--;
|
||||
return [];
|
||||
} else {
|
||||
const newMask = {
|
||||
type: "object_mask" as PolygonType,
|
||||
typeIndex: objectMaskIndex,
|
||||
camera: cameraConfig.name,
|
||||
name: maskId,
|
||||
friendly_name: maskData.friendly_name,
|
||||
enabled: maskData.enabled,
|
||||
enabled_in_config: maskData.enabled_in_config,
|
||||
objects: [objectName],
|
||||
points: interpolatePoints(
|
||||
parseCoordinates(maskData.coordinates),
|
||||
1,
|
||||
1,
|
||||
scaledWidth,
|
||||
scaledHeight,
|
||||
),
|
||||
distances: [],
|
||||
isFinished: true,
|
||||
color: [128, 128, 128],
|
||||
};
|
||||
objectMaskIndex++;
|
||||
return [newMask];
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
setAllPolygons([
|
||||
@@ -548,6 +544,10 @@ export default function MasksAndZonesView({
|
||||
setActivePolygonIndex={setActivePolygonIndex}
|
||||
setEditPane={setEditPane}
|
||||
handleCopyCoordinates={handleCopyCoordinates}
|
||||
isLoading={isLoading}
|
||||
setIsLoading={setIsLoading}
|
||||
loadingPolygonIndex={loadingPolygonIndex}
|
||||
setLoadingPolygonIndex={setLoadingPolygonIndex}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -618,6 +618,10 @@ export default function MasksAndZonesView({
|
||||
setActivePolygonIndex={setActivePolygonIndex}
|
||||
setEditPane={setEditPane}
|
||||
handleCopyCoordinates={handleCopyCoordinates}
|
||||
isLoading={isLoading}
|
||||
setIsLoading={setIsLoading}
|
||||
loadingPolygonIndex={loadingPolygonIndex}
|
||||
setLoadingPolygonIndex={setLoadingPolygonIndex}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -688,6 +692,10 @@ export default function MasksAndZonesView({
|
||||
setActivePolygonIndex={setActivePolygonIndex}
|
||||
setEditPane={setEditPane}
|
||||
handleCopyCoordinates={handleCopyCoordinates}
|
||||
isLoading={isLoading}
|
||||
setIsLoading={setIsLoading}
|
||||
loadingPolygonIndex={loadingPolygonIndex}
|
||||
setLoadingPolygonIndex={setLoadingPolygonIndex}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user