mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-20 23:28:23 +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,
|
"record": CameraConfigUpdateEnum.record,
|
||||||
"review": CameraConfigUpdateEnum.review,
|
"review": CameraConfigUpdateEnum.review,
|
||||||
"snapshots": CameraConfigUpdateEnum.snapshots,
|
"snapshots": CameraConfigUpdateEnum.snapshots,
|
||||||
|
"zones": CameraConfigUpdateEnum.zones,
|
||||||
}
|
}
|
||||||
|
|
||||||
PERSISTENCE_FILE = Path(CONFIG_DIR) / ".active_profile"
|
PERSISTENCE_FILE = Path(CONFIG_DIR) / ".active_profile"
|
||||||
@ -60,14 +61,36 @@ class ProfileManager:
|
|||||||
self._base_enabled[cam_name] = cam_config.enabled
|
self._base_enabled[cam_name] = cam_config.enabled
|
||||||
self._base_zones[cam_name] = copy.deepcopy(cam_config.zones)
|
self._base_zones[cam_name] = copy.deepcopy(cam_config.zones)
|
||||||
for section in PROFILE_SECTION_UPDATES:
|
for section in PROFILE_SECTION_UPDATES:
|
||||||
section_config = getattr(cam_config, section, None)
|
section_value = getattr(cam_config, section, None)
|
||||||
if section_config is not 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] = (
|
self._base_configs[cam_name][section] = (
|
||||||
section_config.model_dump()
|
section_value.model_dump()
|
||||||
)
|
)
|
||||||
self._base_api_configs[cam_name][section] = (
|
self._base_api_configs[cam_name][section] = (
|
||||||
section_config.model_dump(
|
section_value.model_dump(
|
||||||
mode="json", warnings="none", exclude_none=True
|
mode="json",
|
||||||
|
warnings="none",
|
||||||
|
exclude_none=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -154,9 +177,11 @@ class ProfileManager:
|
|||||||
cam_config.zones = copy.deepcopy(base_zones)
|
cam_config.zones = copy.deepcopy(base_zones)
|
||||||
changed.setdefault(cam_name, set()).add("zones")
|
changed.setdefault(cam_name, set()).add("zones")
|
||||||
|
|
||||||
# Restore section configs
|
# Restore section configs (zones handled above)
|
||||||
base = self._base_configs.get(cam_name, {})
|
base = self._base_configs.get(cam_name, {})
|
||||||
for section in PROFILE_SECTION_UPDATES:
|
for section in PROFILE_SECTION_UPDATES:
|
||||||
|
if section == "zones":
|
||||||
|
continue
|
||||||
base_data = base.get(section)
|
base_data = base.get(section)
|
||||||
if base_data is None:
|
if base_data is None:
|
||||||
continue
|
continue
|
||||||
@ -190,12 +215,23 @@ class ProfileManager:
|
|||||||
base_zones = self._base_zones.get(cam_name, {})
|
base_zones = self._base_zones.get(cam_name, {})
|
||||||
merged_zones = copy.deepcopy(base_zones)
|
merged_zones = copy.deepcopy(base_zones)
|
||||||
merged_zones.update(profile.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
|
cam_config.zones = merged_zones
|
||||||
changed.setdefault(cam_name, set()).add("zones")
|
changed.setdefault(cam_name, set()).add("zones")
|
||||||
|
|
||||||
base = self._base_configs.get(cam_name, {})
|
base = self._base_configs.get(cam_name, {})
|
||||||
|
|
||||||
for section in PROFILE_SECTION_UPDATES:
|
for section in PROFILE_SECTION_UPDATES:
|
||||||
|
if section == "zones":
|
||||||
|
continue
|
||||||
profile_section = getattr(profile, section, None)
|
profile_section = getattr(profile, section, None)
|
||||||
if profile_section is None:
|
if profile_section is None:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@ -249,17 +249,26 @@ export default function MasksAndZonesView({
|
|||||||
? cameraConfig.profiles?.[currentEditingProfile]
|
? cameraConfig.profiles?.[currentEditingProfile]
|
||||||
: undefined;
|
: 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
|
// 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 profileZoneNames = new Set(Object.keys(profileData?.zones ?? {}));
|
||||||
const baseMotionMaskNames = new Set(
|
const baseMotionMaskNames = new Set(Object.keys(baseMotion.mask || {}));
|
||||||
Object.keys(cameraConfig.motion.mask || {}),
|
|
||||||
);
|
|
||||||
const profileMotionMaskNames = new Set(
|
const profileMotionMaskNames = new Set(
|
||||||
Object.keys(profileData?.motion?.mask ?? {}),
|
Object.keys(profileData?.motion?.mask ?? {}),
|
||||||
);
|
);
|
||||||
const baseGlobalObjectMaskNames = new Set(
|
const baseGlobalObjectMaskNames = new Set(
|
||||||
Object.keys(cameraConfig.objects.mask || {}),
|
Object.keys(baseObjects.mask || {}),
|
||||||
);
|
);
|
||||||
const profileGlobalObjectMaskNames = new Set(
|
const profileGlobalObjectMaskNames = new Set(
|
||||||
Object.keys(profileData?.objects?.mask ?? {}),
|
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)) {
|
if (currentEditingProfile && profileZoneNames.has(name)) {
|
||||||
// Profile overrides this base zone
|
// Profile overrides this base zone
|
||||||
mergedZones.set(name, {
|
mergedZones.set(name, {
|
||||||
@ -302,7 +311,8 @@ export default function MasksAndZonesView({
|
|||||||
const zones: Polygon[] = [];
|
const zones: Polygon[] = [];
|
||||||
for (const [name, { data: zoneData, source }] of mergedZones) {
|
for (const [name, { data: zoneData, source }] of mergedZones) {
|
||||||
const isBase = source === "base" && !!currentEditingProfile;
|
const isBase = source === "base" && !!currentEditingProfile;
|
||||||
const baseColor = zoneData.color ?? [128, 128, 0];
|
const baseColor =
|
||||||
|
zoneData.color ?? baseZones[name]?.color ?? [128, 128, 0];
|
||||||
zones.push({
|
zones.push({
|
||||||
type: "zone" as PolygonType,
|
type: "zone" as PolygonType,
|
||||||
typeIndex: zoneIndex,
|
typeIndex: zoneIndex,
|
||||||
@ -339,9 +349,7 @@ export default function MasksAndZonesView({
|
|||||||
}
|
}
|
||||||
>();
|
>();
|
||||||
|
|
||||||
for (const [maskId, maskData] of Object.entries(
|
for (const [maskId, maskData] of Object.entries(baseMotion.mask || {})) {
|
||||||
cameraConfig.motion.mask || {},
|
|
||||||
)) {
|
|
||||||
if (currentEditingProfile && profileMotionMaskNames.has(maskId)) {
|
if (currentEditingProfile && profileMotionMaskNames.has(maskId)) {
|
||||||
mergedMotionMasks.set(maskId, {
|
mergedMotionMasks.set(maskId, {
|
||||||
data: profileData!.motion!.mask![maskId],
|
data: profileData!.motion!.mask![maskId],
|
||||||
@ -406,9 +414,7 @@ export default function MasksAndZonesView({
|
|||||||
}
|
}
|
||||||
>();
|
>();
|
||||||
|
|
||||||
for (const [maskId, maskData] of Object.entries(
|
for (const [maskId, maskData] of Object.entries(baseObjects.mask || {})) {
|
||||||
cameraConfig.objects.mask || {},
|
|
||||||
)) {
|
|
||||||
if (currentEditingProfile && profileGlobalObjectMaskNames.has(maskId)) {
|
if (currentEditingProfile && profileGlobalObjectMaskNames.has(maskId)) {
|
||||||
mergedGlobalObjectMasks.set(maskId, {
|
mergedGlobalObjectMasks.set(maskId, {
|
||||||
data: profileData!.objects!.mask![maskId],
|
data: profileData!.objects!.mask![maskId],
|
||||||
@ -472,7 +478,7 @@ export default function MasksAndZonesView({
|
|||||||
// Build per-object filter mask names for profile tracking
|
// Build per-object filter mask names for profile tracking
|
||||||
const baseFilterMaskNames = new Set<string>();
|
const baseFilterMaskNames = new Set<string>();
|
||||||
for (const [, filterConfig] of Object.entries(
|
for (const [, filterConfig] of Object.entries(
|
||||||
cameraConfig.objects.filters,
|
baseObjects.filters || {},
|
||||||
)) {
|
)) {
|
||||||
for (const maskId of Object.keys(filterConfig.mask || {})) {
|
for (const maskId of Object.keys(filterConfig.mask || {})) {
|
||||||
if (!maskId.startsWith("global_")) {
|
if (!maskId.startsWith("global_")) {
|
||||||
@ -495,9 +501,7 @@ export default function MasksAndZonesView({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Per-object filter masks (base)
|
// Per-object filter masks (base)
|
||||||
const objectMasks: Polygon[] = Object.entries(
|
const objectMasks: Polygon[] = Object.entries(baseObjects.filters || {})
|
||||||
cameraConfig.objects.filters,
|
|
||||||
)
|
|
||||||
.filter(
|
.filter(
|
||||||
([, filterConfig]) =>
|
([, filterConfig]) =>
|
||||||
filterConfig.mask && Object.keys(filterConfig.mask).length > 0,
|
filterConfig.mask && Object.keys(filterConfig.mask).length > 0,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user