mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-18 14:18:21 +03:00
migrator and runtime config changes
This commit is contained in:
parent
fa1f9a1fa4
commit
105e7ca4fd
73
frigate/config/camera/mask.py
Normal file
73
frigate/config/camera/mask.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
"""Mask configuration for motion and object masks."""
|
||||||
|
|
||||||
|
from typing import Any, Optional, Union
|
||||||
|
|
||||||
|
from pydantic import Field, field_serializer
|
||||||
|
|
||||||
|
from ..base import FrigateBaseModel
|
||||||
|
|
||||||
|
__all__ = ["MotionMaskConfig", "ObjectMaskConfig"]
|
||||||
|
|
||||||
|
|
||||||
|
class MotionMaskConfig(FrigateBaseModel):
|
||||||
|
"""Configuration for a single motion mask."""
|
||||||
|
|
||||||
|
friendly_name: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
title="Motion mask friendly name used in the Frigate UI.",
|
||||||
|
)
|
||||||
|
enabled: bool = Field(
|
||||||
|
default=True,
|
||||||
|
title="Enable this motion mask.",
|
||||||
|
)
|
||||||
|
coordinates: Union[str, list[str]] = Field(
|
||||||
|
default="",
|
||||||
|
title="Coordinates polygon for the motion mask.",
|
||||||
|
)
|
||||||
|
raw_coordinates: Union[str, list[str]] = ""
|
||||||
|
|
||||||
|
def get_formatted_name(self, mask_id: str) -> str:
|
||||||
|
"""Return the friendly name if set, otherwise return a formatted version of the mask ID."""
|
||||||
|
if self.friendly_name:
|
||||||
|
return self.friendly_name
|
||||||
|
return mask_id.replace("_", " ").title()
|
||||||
|
|
||||||
|
@field_serializer("coordinates", when_used="json")
|
||||||
|
def serialize_coordinates(self, value: Any, info):
|
||||||
|
return self.raw_coordinates
|
||||||
|
|
||||||
|
@field_serializer("raw_coordinates", when_used="json")
|
||||||
|
def serialize_raw_coordinates(self, value: Any, info):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectMaskConfig(FrigateBaseModel):
|
||||||
|
"""Configuration for a single object mask."""
|
||||||
|
|
||||||
|
friendly_name: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
title="Object mask friendly name used in the Frigate UI.",
|
||||||
|
)
|
||||||
|
enabled: bool = Field(
|
||||||
|
default=True,
|
||||||
|
title="Enable this object mask.",
|
||||||
|
)
|
||||||
|
coordinates: Union[str, list[str]] = Field(
|
||||||
|
default="",
|
||||||
|
title="Coordinates polygon for the object mask.",
|
||||||
|
)
|
||||||
|
raw_coordinates: Union[str, list[str]] = ""
|
||||||
|
|
||||||
|
def get_formatted_name(self, mask_id: str) -> str:
|
||||||
|
"""Return the friendly name if set, otherwise return a formatted version of the mask ID."""
|
||||||
|
if self.friendly_name:
|
||||||
|
return self.friendly_name
|
||||||
|
return mask_id.replace("_", " ").title()
|
||||||
|
|
||||||
|
@field_serializer("coordinates", when_used="json")
|
||||||
|
def serialize_coordinates(self, value: Any, info):
|
||||||
|
return self.raw_coordinates
|
||||||
|
|
||||||
|
@field_serializer("raw_coordinates", when_used="json")
|
||||||
|
def serialize_raw_coordinates(self, value: Any, info):
|
||||||
|
return None
|
||||||
@ -1,8 +1,9 @@
|
|||||||
from typing import Any, Optional, Union
|
from typing import Any, Optional
|
||||||
|
|
||||||
from pydantic import Field, field_serializer
|
from pydantic import Field, field_serializer
|
||||||
|
|
||||||
from ..base import FrigateBaseModel
|
from ..base import FrigateBaseModel
|
||||||
|
from .mask import MotionMaskConfig
|
||||||
|
|
||||||
__all__ = ["MotionConfig"]
|
__all__ = ["MotionConfig"]
|
||||||
|
|
||||||
@ -52,8 +53,8 @@ class MotionConfig(FrigateBaseModel):
|
|||||||
title="Frame height",
|
title="Frame height",
|
||||||
description="Height in pixels to scale frames to when computing motion.",
|
description="Height in pixels to scale frames to when computing motion.",
|
||||||
)
|
)
|
||||||
mask: Union[str, list[str]] = Field(
|
mask: dict[str, Optional[MotionMaskConfig]] = Field(
|
||||||
default="",
|
default_factory=dict,
|
||||||
title="Mask coordinates",
|
title="Mask coordinates",
|
||||||
description="Ordered x,y coordinates defining the motion mask polygon used to include/exclude areas.",
|
description="Ordered x,y coordinates defining the motion mask polygon used to include/exclude areas.",
|
||||||
)
|
)
|
||||||
@ -67,11 +68,15 @@ class MotionConfig(FrigateBaseModel):
|
|||||||
title="Original motion state",
|
title="Original motion state",
|
||||||
description="Indicates whether motion detection was enabled in the original static configuration.",
|
description="Indicates whether motion detection was enabled in the original static configuration.",
|
||||||
)
|
)
|
||||||
raw_mask: Union[str, list[str]] = ""
|
raw_mask: dict[str, Optional[MotionMaskConfig]] = Field(
|
||||||
|
default_factory=dict, exclude=True
|
||||||
|
)
|
||||||
|
|
||||||
@field_serializer("mask", when_used="json")
|
@field_serializer("mask", when_used="json")
|
||||||
def serialize_mask(self, value: Any, info):
|
def serialize_mask(self, value: Any, info):
|
||||||
return self.raw_mask
|
if self.raw_mask:
|
||||||
|
return self.raw_mask
|
||||||
|
return value
|
||||||
|
|
||||||
@field_serializer("raw_mask", when_used="json")
|
@field_serializer("raw_mask", when_used="json")
|
||||||
def serialize_raw_mask(self, value: Any, info):
|
def serialize_raw_mask(self, value: Any, info):
|
||||||
|
|||||||
@ -3,6 +3,7 @@ from typing import Any, Optional, Union
|
|||||||
from pydantic import Field, PrivateAttr, field_serializer, field_validator
|
from pydantic import Field, PrivateAttr, field_serializer, field_validator
|
||||||
|
|
||||||
from ..base import FrigateBaseModel
|
from ..base import FrigateBaseModel
|
||||||
|
from .mask import ObjectMaskConfig
|
||||||
|
|
||||||
__all__ = ["ObjectConfig", "GenAIObjectConfig", "FilterConfig"]
|
__all__ = ["ObjectConfig", "GenAIObjectConfig", "FilterConfig"]
|
||||||
|
|
||||||
@ -41,16 +42,20 @@ class FilterConfig(FrigateBaseModel):
|
|||||||
title="Minimum confidence",
|
title="Minimum confidence",
|
||||||
description="Minimum single-frame detection confidence required for the object to be counted.",
|
description="Minimum single-frame detection confidence required for the object to be counted.",
|
||||||
)
|
)
|
||||||
mask: Optional[Union[str, list[str]]] = Field(
|
mask: dict[str, Optional[ObjectMaskConfig]] = Field(
|
||||||
default=None,
|
default_factory=dict,
|
||||||
title="Filter mask",
|
title="Filter mask",
|
||||||
description="Polygon coordinates defining where this filter applies within the frame.",
|
description="Polygon coordinates defining where this filter applies within the frame.",
|
||||||
)
|
)
|
||||||
raw_mask: Union[str, list[str]] = ""
|
raw_mask: dict[str, Optional[ObjectMaskConfig]] = Field(
|
||||||
|
default_factory=dict, exclude=True
|
||||||
|
)
|
||||||
|
|
||||||
@field_serializer("mask", when_used="json")
|
@field_serializer("mask", when_used="json")
|
||||||
def serialize_mask(self, value: Any, info):
|
def serialize_mask(self, value: Any, info):
|
||||||
return self.raw_mask
|
if self.raw_mask:
|
||||||
|
return self.raw_mask
|
||||||
|
return value
|
||||||
|
|
||||||
@field_serializer("raw_mask", when_used="json")
|
@field_serializer("raw_mask", when_used="json")
|
||||||
def serialize_raw_mask(self, value: Any, info):
|
def serialize_raw_mask(self, value: Any, info):
|
||||||
@ -139,11 +144,14 @@ class ObjectConfig(FrigateBaseModel):
|
|||||||
title="Object filters",
|
title="Object filters",
|
||||||
description="Filters applied to detected objects to reduce false positives (area, ratio, confidence).",
|
description="Filters applied to detected objects to reduce false positives (area, ratio, confidence).",
|
||||||
)
|
)
|
||||||
mask: Union[str, list[str]] = Field(
|
mask: dict[str, Optional[ObjectMaskConfig]] = Field(
|
||||||
default="",
|
default_factory=dict,
|
||||||
title="Object mask",
|
title="Object mask",
|
||||||
description="Mask polygon used to prevent object detection in specified areas.",
|
description="Mask polygon used to prevent object detection in specified areas.",
|
||||||
)
|
)
|
||||||
|
raw_mask: dict[str, Optional[ObjectMaskConfig]] = Field(
|
||||||
|
default_factory=dict, exclude=True
|
||||||
|
)
|
||||||
genai: GenAIObjectConfig = Field(
|
genai: GenAIObjectConfig = Field(
|
||||||
default_factory=GenAIObjectConfig,
|
default_factory=GenAIObjectConfig,
|
||||||
title="GenAI object config",
|
title="GenAI object config",
|
||||||
@ -166,3 +174,13 @@ class ObjectConfig(FrigateBaseModel):
|
|||||||
enabled_labels.update(camera.objects.track)
|
enabled_labels.update(camera.objects.track)
|
||||||
|
|
||||||
self._all_objects = list(enabled_labels)
|
self._all_objects = list(enabled_labels)
|
||||||
|
|
||||||
|
@field_serializer("mask", when_used="json")
|
||||||
|
def serialize_mask(self, value: Any, info):
|
||||||
|
if self.raw_mask:
|
||||||
|
return self.raw_mask
|
||||||
|
return value
|
||||||
|
|
||||||
|
@field_serializer("raw_mask", when_used="json")
|
||||||
|
def serialize_raw_mask(self, value: Any, info):
|
||||||
|
return None
|
||||||
|
|||||||
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from typing import Any, Dict, List, Optional, Union
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pydantic import (
|
from pydantic import (
|
||||||
@ -93,54 +93,111 @@ stream_info_retriever = StreamInfoRetriever()
|
|||||||
|
|
||||||
|
|
||||||
class RuntimeMotionConfig(MotionConfig):
|
class RuntimeMotionConfig(MotionConfig):
|
||||||
raw_mask: Union[str, List[str]] = ""
|
"""Runtime version of MotionConfig with rasterized masks."""
|
||||||
mask: np.ndarray = None
|
|
||||||
|
# The rasterized numpy mask (combination of all enabled masks)
|
||||||
|
rasterized_mask: np.ndarray = None
|
||||||
|
|
||||||
def __init__(self, **config):
|
def __init__(self, **config):
|
||||||
frame_shape = config.get("frame_shape", (1, 1))
|
frame_shape = config.get("frame_shape", (1, 1))
|
||||||
|
|
||||||
mask = get_relative_coordinates(config.get("mask", ""), frame_shape)
|
# Store original mask dict for serialization
|
||||||
config["raw_mask"] = mask
|
original_mask = config.get("mask", {})
|
||||||
|
if isinstance(original_mask, dict):
|
||||||
if mask:
|
# Process the new dict format - update raw_coordinates for each mask
|
||||||
config["mask"] = create_mask(frame_shape, mask)
|
processed_mask = {}
|
||||||
else:
|
for mask_id, mask_config in original_mask.items():
|
||||||
empty_mask = np.zeros(frame_shape, np.uint8)
|
if isinstance(mask_config, dict):
|
||||||
empty_mask[:] = 255
|
coords = mask_config.get("coordinates", "")
|
||||||
config["mask"] = empty_mask
|
relative_coords = get_relative_coordinates(coords, frame_shape)
|
||||||
|
mask_config_copy = mask_config.copy()
|
||||||
|
mask_config_copy["raw_coordinates"] = (
|
||||||
|
relative_coords if relative_coords else coords
|
||||||
|
)
|
||||||
|
mask_config_copy["coordinates"] = (
|
||||||
|
relative_coords if relative_coords else coords
|
||||||
|
)
|
||||||
|
processed_mask[mask_id] = mask_config_copy
|
||||||
|
else:
|
||||||
|
processed_mask[mask_id] = mask_config
|
||||||
|
config["mask"] = processed_mask
|
||||||
|
config["raw_mask"] = processed_mask
|
||||||
|
|
||||||
super().__init__(**config)
|
super().__init__(**config)
|
||||||
|
|
||||||
|
# Rasterize only enabled masks
|
||||||
|
enabled_coords = []
|
||||||
|
for mask_config in self.mask.values():
|
||||||
|
if mask_config.enabled and mask_config.coordinates:
|
||||||
|
coords = mask_config.coordinates
|
||||||
|
if isinstance(coords, list):
|
||||||
|
enabled_coords.extend(coords)
|
||||||
|
else:
|
||||||
|
enabled_coords.append(coords)
|
||||||
|
|
||||||
|
if enabled_coords:
|
||||||
|
self.rasterized_mask = create_mask(frame_shape, enabled_coords)
|
||||||
|
else:
|
||||||
|
empty_mask = np.zeros(frame_shape, np.uint8)
|
||||||
|
empty_mask[:] = 255
|
||||||
|
self.rasterized_mask = empty_mask
|
||||||
|
|
||||||
def dict(self, **kwargs):
|
def dict(self, **kwargs):
|
||||||
ret = super().model_dump(**kwargs)
|
ret = super().model_dump(**kwargs)
|
||||||
if "mask" in ret:
|
if "rasterized_mask" in ret:
|
||||||
ret["mask"] = ret["raw_mask"]
|
ret.pop("rasterized_mask")
|
||||||
ret.pop("raw_mask")
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@field_serializer("mask", when_used="json")
|
@field_serializer("rasterized_mask", when_used="json")
|
||||||
def serialize_mask(self, value: Any, info):
|
def serialize_rasterized_mask(self, value: Any, info):
|
||||||
return self.raw_mask
|
|
||||||
|
|
||||||
@field_serializer("raw_mask", when_used="json")
|
|
||||||
def serialize_raw_mask(self, value: Any, info):
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
model_config = ConfigDict(arbitrary_types_allowed=True, extra="ignore")
|
model_config = ConfigDict(arbitrary_types_allowed=True, extra="ignore")
|
||||||
|
|
||||||
|
|
||||||
class RuntimeFilterConfig(FilterConfig):
|
class RuntimeFilterConfig(FilterConfig):
|
||||||
mask: Optional[np.ndarray] = None
|
"""Runtime version of FilterConfig with rasterized masks."""
|
||||||
raw_mask: Optional[Union[str, List[str]]] = None
|
|
||||||
|
# The rasterized numpy mask (combination of all enabled masks)
|
||||||
|
rasterized_mask: Optional[np.ndarray] = None
|
||||||
|
|
||||||
def __init__(self, **config):
|
def __init__(self, **config):
|
||||||
frame_shape = config.get("frame_shape", (1, 1))
|
frame_shape = config.get("frame_shape", (1, 1))
|
||||||
mask = get_relative_coordinates(config.get("mask"), frame_shape)
|
|
||||||
|
|
||||||
config["raw_mask"] = mask
|
# Store original mask dict for serialization
|
||||||
|
original_mask = config.get("mask", {})
|
||||||
if mask is not None:
|
if isinstance(original_mask, dict):
|
||||||
config["mask"] = create_mask(frame_shape, mask)
|
# Process the new dict format - update raw_coordinates for each mask
|
||||||
|
processed_mask = {}
|
||||||
|
for mask_id, mask_config in original_mask.items():
|
||||||
|
# Handle both dict and ObjectMaskConfig formats
|
||||||
|
if hasattr(mask_config, "model_dump"):
|
||||||
|
# It's an ObjectMaskConfig object
|
||||||
|
mask_dict = mask_config.model_dump()
|
||||||
|
coords = mask_dict.get("coordinates", "")
|
||||||
|
relative_coords = get_relative_coordinates(coords, frame_shape)
|
||||||
|
mask_dict["raw_coordinates"] = (
|
||||||
|
relative_coords if relative_coords else coords
|
||||||
|
)
|
||||||
|
mask_dict["coordinates"] = (
|
||||||
|
relative_coords if relative_coords else coords
|
||||||
|
)
|
||||||
|
processed_mask[mask_id] = mask_dict
|
||||||
|
elif isinstance(mask_config, dict):
|
||||||
|
coords = mask_config.get("coordinates", "")
|
||||||
|
relative_coords = get_relative_coordinates(coords, frame_shape)
|
||||||
|
mask_config_copy = mask_config.copy()
|
||||||
|
mask_config_copy["raw_coordinates"] = (
|
||||||
|
relative_coords if relative_coords else coords
|
||||||
|
)
|
||||||
|
mask_config_copy["coordinates"] = (
|
||||||
|
relative_coords if relative_coords else coords
|
||||||
|
)
|
||||||
|
processed_mask[mask_id] = mask_config_copy
|
||||||
|
else:
|
||||||
|
processed_mask[mask_id] = mask_config
|
||||||
|
config["mask"] = processed_mask
|
||||||
|
config["raw_mask"] = processed_mask
|
||||||
|
|
||||||
# Convert min_area and max_area to pixels if they're percentages
|
# Convert min_area and max_area to pixels if they're percentages
|
||||||
if "min_area" in config:
|
if "min_area" in config:
|
||||||
@ -151,13 +208,31 @@ class RuntimeFilterConfig(FilterConfig):
|
|||||||
|
|
||||||
super().__init__(**config)
|
super().__init__(**config)
|
||||||
|
|
||||||
|
# Rasterize only enabled masks
|
||||||
|
enabled_coords = []
|
||||||
|
for mask_config in self.mask.values():
|
||||||
|
if mask_config.enabled and mask_config.coordinates:
|
||||||
|
coords = mask_config.coordinates
|
||||||
|
if isinstance(coords, list):
|
||||||
|
enabled_coords.extend(coords)
|
||||||
|
else:
|
||||||
|
enabled_coords.append(coords)
|
||||||
|
|
||||||
|
if enabled_coords:
|
||||||
|
self.rasterized_mask = create_mask(frame_shape, enabled_coords)
|
||||||
|
else:
|
||||||
|
self.rasterized_mask = None
|
||||||
|
|
||||||
def dict(self, **kwargs):
|
def dict(self, **kwargs):
|
||||||
ret = super().model_dump(**kwargs)
|
ret = super().model_dump(**kwargs)
|
||||||
if "mask" in ret:
|
if "rasterized_mask" in ret:
|
||||||
ret["mask"] = ret["raw_mask"]
|
ret.pop("rasterized_mask")
|
||||||
ret.pop("raw_mask")
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
@field_serializer("rasterized_mask", when_used="json")
|
||||||
|
def serialize_rasterized_mask(self, value: Any, info):
|
||||||
|
return None
|
||||||
|
|
||||||
model_config = ConfigDict(arbitrary_types_allowed=True, extra="ignore")
|
model_config = ConfigDict(arbitrary_types_allowed=True, extra="ignore")
|
||||||
|
|
||||||
|
|
||||||
@ -715,31 +790,23 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
|
|
||||||
# Apply global object masks and convert masks to numpy array
|
# Apply global object masks and convert masks to numpy array
|
||||||
for object, filter in camera_config.objects.filters.items():
|
for object, filter in camera_config.objects.filters.items():
|
||||||
|
# Merge global object masks with per-object filter masks
|
||||||
|
merged_mask = dict(filter.mask) # Copy filter-specific masks
|
||||||
|
|
||||||
|
# Add global object masks if they exist
|
||||||
if camera_config.objects.mask:
|
if camera_config.objects.mask:
|
||||||
filter_mask = []
|
for mask_id, mask_config in camera_config.objects.mask.items():
|
||||||
if filter.mask is not None:
|
# Use a global prefix to avoid key collisions
|
||||||
filter_mask = (
|
global_mask_id = f"global_{mask_id}"
|
||||||
filter.mask
|
merged_mask[global_mask_id] = mask_config
|
||||||
if isinstance(filter.mask, list)
|
|
||||||
else [filter.mask]
|
|
||||||
)
|
|
||||||
object_mask = (
|
|
||||||
get_relative_coordinates(
|
|
||||||
(
|
|
||||||
camera_config.objects.mask
|
|
||||||
if isinstance(camera_config.objects.mask, list)
|
|
||||||
else [camera_config.objects.mask]
|
|
||||||
),
|
|
||||||
camera_config.frame_shape,
|
|
||||||
)
|
|
||||||
or []
|
|
||||||
)
|
|
||||||
filter.mask = filter_mask + object_mask
|
|
||||||
|
|
||||||
# Set runtime filter to create masks
|
# Set runtime filter to create masks
|
||||||
camera_config.objects.filters[object] = RuntimeFilterConfig(
|
camera_config.objects.filters[object] = RuntimeFilterConfig(
|
||||||
frame_shape=camera_config.frame_shape,
|
frame_shape=camera_config.frame_shape,
|
||||||
**filter.model_dump(exclude_unset=True),
|
mask=merged_mask,
|
||||||
|
**filter.model_dump(
|
||||||
|
exclude_unset=True, exclude={"mask", "raw_mask"}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Convert motion configuration
|
# Convert motion configuration
|
||||||
@ -750,7 +817,6 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
else:
|
else:
|
||||||
camera_config.motion = RuntimeMotionConfig(
|
camera_config.motion = RuntimeMotionConfig(
|
||||||
frame_shape=camera_config.frame_shape,
|
frame_shape=camera_config.frame_shape,
|
||||||
raw_mask=camera_config.motion.mask,
|
|
||||||
**camera_config.motion.model_dump(exclude_unset=True),
|
**camera_config.motion.model_dump(exclude_unset=True),
|
||||||
)
|
)
|
||||||
camera_config.motion.enabled_in_config = camera_config.motion.enabled
|
camera_config.motion.enabled_in_config = camera_config.motion.enabled
|
||||||
|
|||||||
@ -434,6 +434,55 @@ def migrate_017_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]
|
|||||||
return new_config
|
return new_config
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_legacy_mask_to_dict(
|
||||||
|
mask: Optional[Union[str, list]], mask_type: str = "motion_mask", label: str = ""
|
||||||
|
) -> dict[str, dict[str, Any]]:
|
||||||
|
"""Convert legacy mask format (str or list[str]) to new dict format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mask: Legacy mask format (string or list of strings)
|
||||||
|
mask_type: Type of mask for naming ("motion_mask" or "object_mask")
|
||||||
|
label: Optional label for object masks (e.g., "person")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with mask_id as key and mask config as value
|
||||||
|
"""
|
||||||
|
if not mask:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
if isinstance(mask, str):
|
||||||
|
if mask:
|
||||||
|
mask_id = f"{mask_type}_1"
|
||||||
|
friendly_name = (
|
||||||
|
f"Object Mask 1 ({label})"
|
||||||
|
if label
|
||||||
|
else f"{mask_type.replace('_', ' ').title()} 1"
|
||||||
|
)
|
||||||
|
result[mask_id] = {
|
||||||
|
"friendly_name": friendly_name,
|
||||||
|
"enabled": True,
|
||||||
|
"coordinates": mask,
|
||||||
|
}
|
||||||
|
elif isinstance(mask, list):
|
||||||
|
for i, coords in enumerate(mask):
|
||||||
|
if coords:
|
||||||
|
mask_id = f"{mask_type}_{i + 1}"
|
||||||
|
friendly_name = (
|
||||||
|
f"Object Mask {i + 1} ({label})"
|
||||||
|
if label
|
||||||
|
else f"{mask_type.replace('_', ' ').title()} {i + 1}"
|
||||||
|
)
|
||||||
|
result[mask_id] = {
|
||||||
|
"friendly_name": friendly_name,
|
||||||
|
"enabled": True,
|
||||||
|
"coordinates": coords,
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def migrate_018_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]:
|
def migrate_018_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]:
|
||||||
"""Handle migrating frigate config to 0.18-0"""
|
"""Handle migrating frigate config to 0.18-0"""
|
||||||
new_config = config.copy()
|
new_config = config.copy()
|
||||||
@ -459,7 +508,35 @@ def migrate_018_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]
|
|||||||
if not new_config.get("record"):
|
if not new_config.get("record"):
|
||||||
del new_config["record"]
|
del new_config["record"]
|
||||||
|
|
||||||
# Remove deprecated sync_recordings and timelapse_args from camera-specific record configs
|
# Migrate global motion masks
|
||||||
|
global_motion = new_config.get("motion", {})
|
||||||
|
if global_motion and "mask" in global_motion:
|
||||||
|
mask = global_motion.get("mask")
|
||||||
|
if mask is not None and not isinstance(mask, dict):
|
||||||
|
new_config["motion"]["mask"] = _convert_legacy_mask_to_dict(
|
||||||
|
mask, "motion_mask"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Migrate global object masks
|
||||||
|
global_objects = new_config.get("objects", {})
|
||||||
|
if global_objects and "mask" in global_objects:
|
||||||
|
mask = global_objects.get("mask")
|
||||||
|
if mask is not None and not isinstance(mask, dict):
|
||||||
|
new_config["objects"]["mask"] = _convert_legacy_mask_to_dict(
|
||||||
|
mask, "object_mask"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Migrate global object filters masks
|
||||||
|
if global_objects and "filters" in global_objects:
|
||||||
|
for obj_name, filter_config in global_objects.get("filters", {}).items():
|
||||||
|
if isinstance(filter_config, dict) and "mask" in filter_config:
|
||||||
|
mask = filter_config.get("mask")
|
||||||
|
if mask is not None and not isinstance(mask, dict):
|
||||||
|
new_config["objects"]["filters"][obj_name]["mask"] = (
|
||||||
|
_convert_legacy_mask_to_dict(mask, "object_mask", obj_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Remove deprecated sync_recordings and migrate masks for camera-specific configs
|
||||||
for name, camera in config.get("cameras", {}).items():
|
for name, camera in config.get("cameras", {}).items():
|
||||||
camera_config: dict[str, dict[str, Any]] = camera.copy()
|
camera_config: dict[str, dict[str, Any]] = camera.copy()
|
||||||
|
|
||||||
@ -478,6 +555,34 @@ def migrate_018_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]
|
|||||||
if not camera_config.get("record"):
|
if not camera_config.get("record"):
|
||||||
del camera_config["record"]
|
del camera_config["record"]
|
||||||
|
|
||||||
|
# Migrate camera motion masks
|
||||||
|
camera_motion = camera_config.get("motion", {})
|
||||||
|
if camera_motion and "mask" in camera_motion:
|
||||||
|
mask = camera_motion.get("mask")
|
||||||
|
if mask is not None and not isinstance(mask, dict):
|
||||||
|
camera_config["motion"]["mask"] = _convert_legacy_mask_to_dict(
|
||||||
|
mask, "motion_mask"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Migrate camera global object masks
|
||||||
|
camera_objects = camera_config.get("objects", {})
|
||||||
|
if camera_objects and "mask" in camera_objects:
|
||||||
|
mask = camera_objects.get("mask")
|
||||||
|
if mask is not None and not isinstance(mask, dict):
|
||||||
|
camera_config["objects"]["mask"] = _convert_legacy_mask_to_dict(
|
||||||
|
mask, "object_mask"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Migrate camera object filter masks
|
||||||
|
if camera_objects and "filters" in camera_objects:
|
||||||
|
for obj_name, filter_config in camera_objects.get("filters", {}).items():
|
||||||
|
if isinstance(filter_config, dict) and "mask" in filter_config:
|
||||||
|
mask = filter_config.get("mask")
|
||||||
|
if mask is not None and not isinstance(mask, dict):
|
||||||
|
camera_config["objects"]["filters"][obj_name]["mask"] = (
|
||||||
|
_convert_legacy_mask_to_dict(mask, "object_mask", obj_name)
|
||||||
|
)
|
||||||
|
|
||||||
new_config["cameras"][name] = camera_config
|
new_config["cameras"][name] = camera_config
|
||||||
|
|
||||||
new_config["version"] = "0.18-0"
|
new_config["version"] = "0.18-0"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user