mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-17 13:48:21 +03:00
fix zones
- Fix zone colors not matching across profiles by falling back to base zone color when profile zone data lacks a color field - Use base_config for base-layer values in masks/zones view so profile-merged values don't pollute the base config editing view - Handle zones separately in profile manager snapshot/restore since ZoneConfig requires special serialization (color as private attr, contour generation) - Inherit base zone color and generate contours for profile zone overrides in profile manager
This commit is contained in:
parent
d41d328a9b
commit
3a08b3d54b
@ -29,6 +29,7 @@ PROFILE_SECTION_UPDATES: dict[str, CameraConfigUpdateEnum] = {
|
||||
"record": CameraConfigUpdateEnum.record,
|
||||
"review": CameraConfigUpdateEnum.review,
|
||||
"snapshots": CameraConfigUpdateEnum.snapshots,
|
||||
"zones": CameraConfigUpdateEnum.zones,
|
||||
}
|
||||
|
||||
PERSISTENCE_FILE = Path(CONFIG_DIR) / ".active_profile"
|
||||
@ -60,14 +61,36 @@ class ProfileManager:
|
||||
self._base_enabled[cam_name] = cam_config.enabled
|
||||
self._base_zones[cam_name] = copy.deepcopy(cam_config.zones)
|
||||
for section in PROFILE_SECTION_UPDATES:
|
||||
section_config = getattr(cam_config, section, None)
|
||||
if section_config is not None:
|
||||
section_value = getattr(cam_config, section, None)
|
||||
if section_value is None:
|
||||
continue
|
||||
|
||||
if section == "zones":
|
||||
# zones is a dict of ZoneConfig models
|
||||
self._base_configs[cam_name][section] = {
|
||||
name: zone.model_dump()
|
||||
for name, zone in section_value.items()
|
||||
}
|
||||
self._base_api_configs[cam_name][section] = {
|
||||
name: {
|
||||
**zone.model_dump(
|
||||
mode="json",
|
||||
warnings="none",
|
||||
exclude_none=True,
|
||||
),
|
||||
"color": zone.color,
|
||||
}
|
||||
for name, zone in section_value.items()
|
||||
}
|
||||
else:
|
||||
self._base_configs[cam_name][section] = (
|
||||
section_config.model_dump()
|
||||
section_value.model_dump()
|
||||
)
|
||||
self._base_api_configs[cam_name][section] = (
|
||||
section_config.model_dump(
|
||||
mode="json", warnings="none", exclude_none=True
|
||||
section_value.model_dump(
|
||||
mode="json",
|
||||
warnings="none",
|
||||
exclude_none=True,
|
||||
)
|
||||
)
|
||||
|
||||
@ -154,9 +177,11 @@ class ProfileManager:
|
||||
cam_config.zones = copy.deepcopy(base_zones)
|
||||
changed.setdefault(cam_name, set()).add("zones")
|
||||
|
||||
# Restore section configs
|
||||
# Restore section configs (zones handled above)
|
||||
base = self._base_configs.get(cam_name, {})
|
||||
for section in PROFILE_SECTION_UPDATES:
|
||||
if section == "zones":
|
||||
continue
|
||||
base_data = base.get(section)
|
||||
if base_data is None:
|
||||
continue
|
||||
@ -190,12 +215,23 @@ class ProfileManager:
|
||||
base_zones = self._base_zones.get(cam_name, {})
|
||||
merged_zones = copy.deepcopy(base_zones)
|
||||
merged_zones.update(profile.zones)
|
||||
# Profile zone objects are parsed without colors or contours
|
||||
# (those are set during CameraConfig init / post-validation).
|
||||
# Inherit the base zone's color when available, and ensure
|
||||
# every zone has a valid contour for rendering.
|
||||
for name, zone in merged_zones.items():
|
||||
if zone.contour.size == 0:
|
||||
zone.generate_contour(cam_config.frame_shape)
|
||||
if zone.color == (0, 0, 0) and name in base_zones:
|
||||
zone._color = base_zones[name].color
|
||||
cam_config.zones = merged_zones
|
||||
changed.setdefault(cam_name, set()).add("zones")
|
||||
|
||||
base = self._base_configs.get(cam_name, {})
|
||||
|
||||
for section in PROFILE_SECTION_UPDATES:
|
||||
if section == "zones":
|
||||
continue
|
||||
profile_section = getattr(profile, section, None)
|
||||
if profile_section is None:
|
||||
continue
|
||||
|
||||
@ -249,17 +249,26 @@ export default function MasksAndZonesView({
|
||||
? cameraConfig.profiles?.[currentEditingProfile]
|
||||
: undefined;
|
||||
|
||||
// When a profile is active, the top-level sections contain
|
||||
// effective (profile-merged) values. Use base_config for the
|
||||
// original base values so the "Base Config" view is accurate and
|
||||
// the base layer for profile merging is correct.
|
||||
const baseMotion = (cameraConfig.base_config?.motion ??
|
||||
cameraConfig.motion) as typeof cameraConfig.motion;
|
||||
const baseObjects = (cameraConfig.base_config?.objects ??
|
||||
cameraConfig.objects) as typeof cameraConfig.objects;
|
||||
const baseZones = (cameraConfig.base_config?.zones ??
|
||||
cameraConfig.zones) as typeof cameraConfig.zones;
|
||||
|
||||
// Build base zone names set for source tracking
|
||||
const baseZoneNames = new Set(Object.keys(cameraConfig.zones));
|
||||
const baseZoneNames = new Set(Object.keys(baseZones));
|
||||
const profileZoneNames = new Set(Object.keys(profileData?.zones ?? {}));
|
||||
const baseMotionMaskNames = new Set(
|
||||
Object.keys(cameraConfig.motion.mask || {}),
|
||||
);
|
||||
const baseMotionMaskNames = new Set(Object.keys(baseMotion.mask || {}));
|
||||
const profileMotionMaskNames = new Set(
|
||||
Object.keys(profileData?.motion?.mask ?? {}),
|
||||
);
|
||||
const baseGlobalObjectMaskNames = new Set(
|
||||
Object.keys(cameraConfig.objects.mask || {}),
|
||||
Object.keys(baseObjects.mask || {}),
|
||||
);
|
||||
const profileGlobalObjectMaskNames = new Set(
|
||||
Object.keys(profileData?.objects?.mask ?? {}),
|
||||
@ -274,7 +283,7 @@ export default function MasksAndZonesView({
|
||||
}
|
||||
>();
|
||||
|
||||
for (const [name, zoneData] of Object.entries(cameraConfig.zones)) {
|
||||
for (const [name, zoneData] of Object.entries(baseZones)) {
|
||||
if (currentEditingProfile && profileZoneNames.has(name)) {
|
||||
// Profile overrides this base zone
|
||||
mergedZones.set(name, {
|
||||
@ -302,7 +311,8 @@ export default function MasksAndZonesView({
|
||||
const zones: Polygon[] = [];
|
||||
for (const [name, { data: zoneData, source }] of mergedZones) {
|
||||
const isBase = source === "base" && !!currentEditingProfile;
|
||||
const baseColor = zoneData.color ?? [128, 128, 0];
|
||||
const baseColor =
|
||||
zoneData.color ?? baseZones[name]?.color ?? [128, 128, 0];
|
||||
zones.push({
|
||||
type: "zone" as PolygonType,
|
||||
typeIndex: zoneIndex,
|
||||
@ -339,9 +349,7 @@ export default function MasksAndZonesView({
|
||||
}
|
||||
>();
|
||||
|
||||
for (const [maskId, maskData] of Object.entries(
|
||||
cameraConfig.motion.mask || {},
|
||||
)) {
|
||||
for (const [maskId, maskData] of Object.entries(baseMotion.mask || {})) {
|
||||
if (currentEditingProfile && profileMotionMaskNames.has(maskId)) {
|
||||
mergedMotionMasks.set(maskId, {
|
||||
data: profileData!.motion!.mask![maskId],
|
||||
@ -406,9 +414,7 @@ export default function MasksAndZonesView({
|
||||
}
|
||||
>();
|
||||
|
||||
for (const [maskId, maskData] of Object.entries(
|
||||
cameraConfig.objects.mask || {},
|
||||
)) {
|
||||
for (const [maskId, maskData] of Object.entries(baseObjects.mask || {})) {
|
||||
if (currentEditingProfile && profileGlobalObjectMaskNames.has(maskId)) {
|
||||
mergedGlobalObjectMasks.set(maskId, {
|
||||
data: profileData!.objects!.mask![maskId],
|
||||
@ -472,7 +478,7 @@ export default function MasksAndZonesView({
|
||||
// Build per-object filter mask names for profile tracking
|
||||
const baseFilterMaskNames = new Set<string>();
|
||||
for (const [, filterConfig] of Object.entries(
|
||||
cameraConfig.objects.filters,
|
||||
baseObjects.filters || {},
|
||||
)) {
|
||||
for (const maskId of Object.keys(filterConfig.mask || {})) {
|
||||
if (!maskId.startsWith("global_")) {
|
||||
@ -495,9 +501,7 @@ export default function MasksAndZonesView({
|
||||
}
|
||||
|
||||
// Per-object filter masks (base)
|
||||
const objectMasks: Polygon[] = Object.entries(
|
||||
cameraConfig.objects.filters,
|
||||
)
|
||||
const objectMasks: Polygon[] = Object.entries(baseObjects.filters || {})
|
||||
.filter(
|
||||
([, filterConfig]) =>
|
||||
filterConfig.mask && Object.keys(filterConfig.mask).length > 0,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user