Skip to content

Commit 03e25b3

Browse files
authored
Improve config validation for zones and object masks (#11022)
* Add verification for required zone names * Make global object masks use relative coordinates as well * Ensure event image cleanup doesn't fail * Return passed value
1 parent fb721ad commit 03e25b3

File tree

3 files changed

+95
-81
lines changed

3 files changed

+95
-81
lines changed

frigate/config.py

Lines changed: 27 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
get_ffmpeg_arg_list,
4747
load_config_with_no_duplicates,
4848
)
49+
from frigate.util.config import get_relative_coordinates
4950
from frigate.util.image import create_mask
5051
from frigate.util.services import auto_detect_hwaccel, get_video_properties
5152

@@ -348,35 +349,7 @@ class RuntimeMotionConfig(MotionConfig):
348349
def __init__(self, **config):
349350
frame_shape = config.get("frame_shape", (1, 1))
350351

351-
mask = config.get("mask", "")
352-
353-
# masks and zones are saved as relative coordinates
354-
# we know if any points are > 1 then it is using the
355-
# old native resolution coordinates
356-
if mask:
357-
if isinstance(mask, list) and any(x > "1.0" for x in mask[0].split(",")):
358-
relative_masks = []
359-
for m in mask:
360-
points = m.split(",")
361-
relative_masks.append(
362-
",".join(
363-
[
364-
f"{round(int(points[i]) / frame_shape[1], 3)},{round(int(points[i + 1]) / frame_shape[0], 3)}"
365-
for i in range(0, len(points), 2)
366-
]
367-
)
368-
)
369-
370-
mask = relative_masks
371-
elif isinstance(mask, str) and any(x > "1.0" for x in mask.split(",")):
372-
points = mask.split(",")
373-
mask = ",".join(
374-
[
375-
f"{round(int(points[i]) / frame_shape[1], 3)},{round(int(points[i + 1]) / frame_shape[0], 3)}"
376-
for i in range(0, len(points), 2)
377-
]
378-
)
379-
352+
mask = get_relative_coordinates(config.get("mask", ""), frame_shape)
380353
config["raw_mask"] = mask
381354

382355
if mask:
@@ -508,34 +481,7 @@ class RuntimeFilterConfig(FilterConfig):
508481

509482
def __init__(self, **config):
510483
frame_shape = config.get("frame_shape", (1, 1))
511-
mask = config.get("mask")
512-
513-
# masks and zones are saved as relative coordinates
514-
# we know if any points are > 1 then it is using the
515-
# old native resolution coordinates
516-
if mask:
517-
if isinstance(mask, list) and any(x > "1.0" for x in mask[0].split(",")):
518-
relative_masks = []
519-
for m in mask:
520-
points = m.split(",")
521-
relative_masks.append(
522-
",".join(
523-
[
524-
f"{round(int(points[i]) / frame_shape[1], 3)},{round(int(points[i + 1]) / frame_shape[0], 3)}"
525-
for i in range(0, len(points), 2)
526-
]
527-
)
528-
)
529-
530-
mask = relative_masks
531-
elif isinstance(mask, str) and any(x > "1.0" for x in mask.split(",")):
532-
points = mask.split(",")
533-
mask = ",".join(
534-
[
535-
f"{round(int(points[i]) / frame_shape[1], 3)},{round(int(points[i + 1]) / frame_shape[0], 3)}"
536-
for i in range(0, len(points), 2)
537-
]
538-
)
484+
mask = get_relative_coordinates(config.get("mask"), frame_shape)
539485

540486
config["raw_mask"] = mask
541487

@@ -1231,6 +1177,20 @@ def verify_zone_objects_are_tracked(camera_config: CameraConfig) -> None:
12311177
)
12321178

12331179

1180+
def verify_required_zones_exist(camera_config: CameraConfig) -> None:
1181+
for det_zone in camera_config.review.detections.required_zones:
1182+
if det_zone not in camera_config.zones.keys():
1183+
raise ValueError(
1184+
f"Camera {camera_config.name} has a required zone for detections {det_zone} that is not defined."
1185+
)
1186+
1187+
for det_zone in camera_config.review.alerts.required_zones:
1188+
if det_zone not in camera_config.zones.keys():
1189+
raise ValueError(
1190+
f"Camera {camera_config.name} has a required zone for alerts {det_zone} that is not defined."
1191+
)
1192+
1193+
12341194
def verify_autotrack_zones(camera_config: CameraConfig) -> ValueError | None:
12351195
"""Verify that required_zones are specified when autotracking is enabled."""
12361196
if (
@@ -1456,9 +1416,15 @@ def runtime_config(self, plus_api: PlusApi = None) -> FrigateConfig:
14561416
else [filter.mask]
14571417
)
14581418
object_mask = (
1459-
camera_config.objects.mask
1460-
if isinstance(camera_config.objects.mask, list)
1461-
else [camera_config.objects.mask]
1419+
get_relative_coordinates(
1420+
(
1421+
camera_config.objects.mask
1422+
if isinstance(camera_config.objects.mask, list)
1423+
else [camera_config.objects.mask]
1424+
),
1425+
camera_config.frame_shape,
1426+
)
1427+
or []
14621428
)
14631429
filter.mask = filter_mask + object_mask
14641430

@@ -1495,6 +1461,7 @@ def runtime_config(self, plus_api: PlusApi = None) -> FrigateConfig:
14951461
verify_recording_retention(camera_config)
14961462
verify_recording_segments_setup_with_reasonable_time(camera_config)
14971463
verify_zone_objects_are_tracked(camera_config)
1464+
verify_required_zones_exist(camera_config)
14981465
verify_autotrack_zones(camera_config)
14991466
verify_motion_and_detect(camera_config)
15001467

frigate/events/cleanup.py

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def expire(self, media_type: EventCleanupType) -> list[str]:
8383
datetime.datetime.now() - datetime.timedelta(days=expire_days)
8484
).timestamp()
8585
# grab all events after specific time
86-
expired_events = (
86+
expired_events: list[Event] = (
8787
Event.select(
8888
Event.id,
8989
Event.camera,
@@ -103,12 +103,16 @@ def expire(self, media_type: EventCleanupType) -> list[str]:
103103
media_path = Path(
104104
f"{os.path.join(CLIPS_DIR, media_name)}.{file_extension}"
105105
)
106-
media_path.unlink(missing_ok=True)
107-
if file_extension == "jpg":
108-
media_path = Path(
109-
f"{os.path.join(CLIPS_DIR, media_name)}-clean.png"
110-
)
106+
107+
try:
111108
media_path.unlink(missing_ok=True)
109+
if file_extension == "jpg":
110+
media_path = Path(
111+
f"{os.path.join(CLIPS_DIR, media_name)}-clean.png"
112+
)
113+
media_path.unlink(missing_ok=True)
114+
except OSError as e:
115+
logger.warning(f"Unable to delete event images: {e}")
112116

113117
# update the clips attribute for the db entry
114118
update_query = Event.update(update_params).where(
@@ -163,15 +167,18 @@ def expire(self, media_type: EventCleanupType) -> list[str]:
163167
events_to_update.append(event.id)
164168

165169
if media_type == EventCleanupType.snapshots:
166-
media_name = f"{event.camera}-{event.id}"
167-
media_path = Path(
168-
f"{os.path.join(CLIPS_DIR, media_name)}.{file_extension}"
169-
)
170-
media_path.unlink(missing_ok=True)
171-
media_path = Path(
172-
f"{os.path.join(CLIPS_DIR, media_name)}-clean.png"
173-
)
174-
media_path.unlink(missing_ok=True)
170+
try:
171+
media_name = f"{event.camera}-{event.id}"
172+
media_path = Path(
173+
f"{os.path.join(CLIPS_DIR, media_name)}.{file_extension}"
174+
)
175+
media_path.unlink(missing_ok=True)
176+
media_path = Path(
177+
f"{os.path.join(CLIPS_DIR, media_name)}-clean.png"
178+
)
179+
media_path.unlink(missing_ok=True)
180+
except OSError as e:
181+
logger.warning(f"Unable to delete event images: {e}")
175182

176183
# update the clips attribute for the db entry
177184
Event.update(update_params).where(Event.id << events_to_update).execute()
@@ -195,14 +202,18 @@ def purge_duplicates(self) -> None:
195202
select distinct id, camera, has_snapshot, has_clip from grouped_events
196203
where copy_number > 1 and end_time not null;"""
197204

198-
duplicate_events = Event.raw(duplicate_query)
205+
duplicate_events: list[Event] = Event.raw(duplicate_query)
199206
for event in duplicate_events:
200207
logger.debug(f"Removing duplicate: {event.id}")
201-
media_name = f"{event.camera}-{event.id}"
202-
media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}.jpg")
203-
media_path.unlink(missing_ok=True)
204-
media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}-clean.png")
205-
media_path.unlink(missing_ok=True)
208+
209+
try:
210+
media_name = f"{event.camera}-{event.id}"
211+
media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}.jpg")
212+
media_path.unlink(missing_ok=True)
213+
media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}-clean.png")
214+
media_path.unlink(missing_ok=True)
215+
except OSError as e:
216+
logger.warning(f"Unable to delete event images: {e}")
206217

207218
(
208219
Event.delete()

frigate/util/config.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import logging
44
import os
55
import shutil
6+
from typing import Optional, Union
67

78
from ruamel.yaml import YAML
89

@@ -141,3 +142,38 @@ def migrate_014(config: dict[str, dict[str, any]]) -> dict[str, dict[str, any]]:
141142
new_config["cameras"][name] = camera_config
142143

143144
return new_config
145+
146+
147+
def get_relative_coordinates(
148+
mask: Optional[Union[str, list]], frame_shape: tuple[int, int]
149+
) -> Union[str, list]:
150+
# masks and zones are saved as relative coordinates
151+
# we know if any points are > 1 then it is using the
152+
# old native resolution coordinates
153+
if mask:
154+
if isinstance(mask, list) and any(x > "1.0" for x in mask[0].split(",")):
155+
relative_masks = []
156+
for m in mask:
157+
points = m.split(",")
158+
relative_masks.append(
159+
",".join(
160+
[
161+
f"{round(int(points[i]) / frame_shape[1], 3)},{round(int(points[i + 1]) / frame_shape[0], 3)}"
162+
for i in range(0, len(points), 2)
163+
]
164+
)
165+
)
166+
167+
mask = relative_masks
168+
elif isinstance(mask, str) and any(x > "1.0" for x in mask.split(",")):
169+
points = mask.split(",")
170+
mask = ",".join(
171+
[
172+
f"{round(int(points[i]) / frame_shape[1], 3)},{round(int(points[i + 1]) / frame_shape[0], 3)}"
173+
for i in range(0, len(points), 2)
174+
]
175+
)
176+
177+
return mask
178+
179+
return mask

0 commit comments

Comments
 (0)