mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
Implement config migration and restructure config for new review format (#10961)
* Update reference config to reflect new config * Migrate 0.13 config to match 0.14 config style * Overwrite existing config * Adjust config schema to remove events required zones and include reviews * Update object config to check correct required zones config * Refactor reviews to remove motion and only create review segments in expected circumstances * Cleanup * Formatting * Fix update ordering * Update pydantic * Remove rtmp references as part of migration * Catch file not found for alert frame
This commit is contained in:
parent
11dc407b36
commit
3788df5bc6
@ -12,7 +12,7 @@ pandas == 2.2.*
|
|||||||
peewee == 3.17.*
|
peewee == 3.17.*
|
||||||
peewee_migrate == 1.12.*
|
peewee_migrate == 1.12.*
|
||||||
psutil == 5.9.*
|
psutil == 5.9.*
|
||||||
pydantic == 2.6.*
|
pydantic == 2.7.*
|
||||||
git+https://github.com/fbcotter/py3nvml#egg=py3nvml
|
git+https://github.com/fbcotter/py3nvml#egg=py3nvml
|
||||||
PyYAML == 6.0.*
|
PyYAML == 6.0.*
|
||||||
pytz == 2024.1
|
pytz == 2024.1
|
||||||
|
@ -257,6 +257,28 @@ objects:
|
|||||||
# Checks based on the bottom center of the bounding box of the object
|
# Checks based on the bottom center of the bounding box of the object
|
||||||
mask: 0,0,1000,0,1000,200,0,200
|
mask: 0,0,1000,0,1000,200,0,200
|
||||||
|
|
||||||
|
# Optional: Review configuration
|
||||||
|
# NOTE: Can be overridden at the camera level
|
||||||
|
review:
|
||||||
|
# Optional: alerts configuration
|
||||||
|
alerts:
|
||||||
|
# Optional: labels that qualify as an alert (default: shown below)
|
||||||
|
labels:
|
||||||
|
- car
|
||||||
|
- person
|
||||||
|
# Optional: required zones for an object to be marked as an alert (default: none)
|
||||||
|
required_zones:
|
||||||
|
- driveway
|
||||||
|
# Optional: detections configuration
|
||||||
|
detections:
|
||||||
|
# Optional: labels that qualify as a detection (default: all labels that are tracked / listened to)
|
||||||
|
labels:
|
||||||
|
- car
|
||||||
|
- person
|
||||||
|
# Optional: required zones for an object to be marked as a detection (default: none)
|
||||||
|
required_zones:
|
||||||
|
- driveway
|
||||||
|
|
||||||
# Optional: Motion configuration
|
# Optional: Motion configuration
|
||||||
# NOTE: Can be overridden at the camera level
|
# NOTE: Can be overridden at the camera level
|
||||||
motion:
|
motion:
|
||||||
@ -345,8 +367,6 @@ record:
|
|||||||
# Optional: Objects to save recordings for. (default: all tracked objects)
|
# Optional: Objects to save recordings for. (default: all tracked objects)
|
||||||
objects:
|
objects:
|
||||||
- person
|
- person
|
||||||
# Optional: Restrict recordings to objects that entered any of the listed zones (default: no required zones)
|
|
||||||
required_zones: []
|
|
||||||
# Optional: Retention settings for recordings of events
|
# Optional: Retention settings for recordings of events
|
||||||
retain:
|
retain:
|
||||||
# Required: Default retention days (default: shown below)
|
# Required: Default retention days (default: shown below)
|
||||||
|
@ -63,6 +63,7 @@ from frigate.storage import StorageMaintainer
|
|||||||
from frigate.timeline import TimelineProcessor
|
from frigate.timeline import TimelineProcessor
|
||||||
from frigate.types import CameraMetricsTypes, PTZMetricsTypes
|
from frigate.types import CameraMetricsTypes, PTZMetricsTypes
|
||||||
from frigate.util.builtin import save_default_config
|
from frigate.util.builtin import save_default_config
|
||||||
|
from frigate.util.config import migrate_frigate_config
|
||||||
from frigate.util.object import get_camera_regions_grid
|
from frigate.util.object import get_camera_regions_grid
|
||||||
from frigate.version import VERSION
|
from frigate.version import VERSION
|
||||||
from frigate.video import capture_camera, track_camera
|
from frigate.video import capture_camera, track_camera
|
||||||
@ -126,6 +127,9 @@ class FrigateApp:
|
|||||||
config_file = config_file_yaml
|
config_file = config_file_yaml
|
||||||
save_default_config(config_file)
|
save_default_config(config_file)
|
||||||
|
|
||||||
|
# check if the config file needs to be migrated
|
||||||
|
migrate_frigate_config(config_file)
|
||||||
|
|
||||||
user_config = FrigateConfig.parse_file(config_file)
|
user_config = FrigateConfig.parse_file(config_file)
|
||||||
self.config = user_config.runtime_config(self.plus_api)
|
self.config = user_config.runtime_config(self.plus_api)
|
||||||
|
|
||||||
|
@ -245,10 +245,6 @@ class EventsConfig(FrigateBaseModel):
|
|||||||
default=5, title="Seconds to retain before event starts.", le=MAX_PRE_CAPTURE
|
default=5, title="Seconds to retain before event starts.", le=MAX_PRE_CAPTURE
|
||||||
)
|
)
|
||||||
post_capture: int = Field(default=5, title="Seconds to retain after event ends.")
|
post_capture: int = Field(default=5, title="Seconds to retain after event ends.")
|
||||||
required_zones: List[str] = Field(
|
|
||||||
default_factory=list,
|
|
||||||
title="List of required zones to be entered in order to save the event.",
|
|
||||||
)
|
|
||||||
objects: Optional[List[str]] = Field(
|
objects: Optional[List[str]] = Field(
|
||||||
None,
|
None,
|
||||||
title="List of objects to be detected in order to save the event.",
|
title="List of objects to be detected in order to save the event.",
|
||||||
@ -657,13 +653,45 @@ class ZoneConfig(BaseModel):
|
|||||||
|
|
||||||
class ObjectConfig(FrigateBaseModel):
|
class ObjectConfig(FrigateBaseModel):
|
||||||
track: List[str] = Field(default=DEFAULT_TRACKED_OBJECTS, title="Objects to track.")
|
track: List[str] = Field(default=DEFAULT_TRACKED_OBJECTS, title="Objects to track.")
|
||||||
alert: List[str] = Field(
|
|
||||||
default=DEFAULT_ALERT_OBJECTS, title="Objects to create alerts for."
|
|
||||||
)
|
|
||||||
filters: Dict[str, FilterConfig] = Field(default={}, title="Object filters.")
|
filters: Dict[str, FilterConfig] = Field(default={}, title="Object filters.")
|
||||||
mask: Union[str, List[str]] = Field(default="", title="Object mask.")
|
mask: Union[str, List[str]] = Field(default="", title="Object mask.")
|
||||||
|
|
||||||
|
|
||||||
|
class AlertsConfig(FrigateBaseModel):
|
||||||
|
"""Configure alerts"""
|
||||||
|
|
||||||
|
labels: List[str] = Field(
|
||||||
|
default=DEFAULT_ALERT_OBJECTS, title="Labels to create alerts for."
|
||||||
|
)
|
||||||
|
required_zones: List[str] = Field(
|
||||||
|
default_factory=list,
|
||||||
|
title="List of required zones to be entered in order to save the event as an alert.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DetectionsConfig(FrigateBaseModel):
|
||||||
|
"""Configure detections"""
|
||||||
|
|
||||||
|
labels: Optional[List[str]] = Field(
|
||||||
|
default=None, title="Labels to create detections for."
|
||||||
|
)
|
||||||
|
required_zones: List[str] = Field(
|
||||||
|
default_factory=list,
|
||||||
|
title="List of required zones to be entered in order to save the event as a detection.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewConfig(FrigateBaseModel):
|
||||||
|
"""Configure reviews"""
|
||||||
|
|
||||||
|
alerts: AlertsConfig = Field(
|
||||||
|
default_factory=AlertsConfig, title="Review alerts config."
|
||||||
|
)
|
||||||
|
detections: DetectionsConfig = Field(
|
||||||
|
default_factory=DetectionsConfig, title="Review detections config."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AudioConfig(FrigateBaseModel):
|
class AudioConfig(FrigateBaseModel):
|
||||||
enabled: bool = Field(default=False, title="Enable audio events.")
|
enabled: bool = Field(default=False, title="Enable audio events.")
|
||||||
max_not_heard: int = Field(
|
max_not_heard: int = Field(
|
||||||
@ -942,6 +970,9 @@ class CameraConfig(FrigateBaseModel):
|
|||||||
objects: ObjectConfig = Field(
|
objects: ObjectConfig = Field(
|
||||||
default_factory=ObjectConfig, title="Object configuration."
|
default_factory=ObjectConfig, title="Object configuration."
|
||||||
)
|
)
|
||||||
|
review: ReviewConfig = Field(
|
||||||
|
default_factory=ReviewConfig, title="Review configuration."
|
||||||
|
)
|
||||||
audio: AudioConfig = Field(
|
audio: AudioConfig = Field(
|
||||||
default_factory=AudioConfig, title="Audio events configuration."
|
default_factory=AudioConfig, title="Audio events configuration."
|
||||||
)
|
)
|
||||||
@ -1263,6 +1294,9 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
objects: ObjectConfig = Field(
|
objects: ObjectConfig = Field(
|
||||||
default_factory=ObjectConfig, title="Global object configuration."
|
default_factory=ObjectConfig, title="Global object configuration."
|
||||||
)
|
)
|
||||||
|
review: ReviewConfig = Field(
|
||||||
|
default_factory=ReviewConfig, title="Review configuration."
|
||||||
|
)
|
||||||
audio: AudioConfig = Field(
|
audio: AudioConfig = Field(
|
||||||
default_factory=AudioConfig, title="Global Audio events configuration."
|
default_factory=AudioConfig, title="Global Audio events configuration."
|
||||||
)
|
)
|
||||||
@ -1310,6 +1344,7 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
"snapshots": ...,
|
"snapshots": ...,
|
||||||
"live": ...,
|
"live": ...,
|
||||||
"objects": ...,
|
"objects": ...,
|
||||||
|
"review": ...,
|
||||||
"motion": ...,
|
"motion": ...,
|
||||||
"detect": ...,
|
"detect": ...,
|
||||||
"ffmpeg": ...,
|
"ffmpeg": ...,
|
||||||
|
@ -1007,7 +1007,7 @@ class TrackedObjectProcessor(threading.Thread):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def should_retain_recording(self, camera, obj: TrackedObject):
|
def should_retain_recording(self, camera: str, obj: TrackedObject):
|
||||||
if obj.false_positive:
|
if obj.false_positive:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -1022,7 +1022,11 @@ class TrackedObjectProcessor(threading.Thread):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# If there are required zones and there is no overlap
|
# If there are required zones and there is no overlap
|
||||||
required_zones = record_config.events.required_zones
|
review_config = self.config.cameras[camera].review
|
||||||
|
required_zones = (
|
||||||
|
review_config.alerts.required_zones
|
||||||
|
+ review_config.detections.required_zones
|
||||||
|
)
|
||||||
if len(required_zones) > 0 and not set(obj.entered_zones) & set(required_zones):
|
if len(required_zones) > 0 and not set(obj.entered_zones) & set(required_zones):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Not creating clip for {obj.obj_data['id']} because it did not enter required zones"
|
f"Not creating clip for {obj.obj_data['id']} because it did not enter required zones"
|
||||||
|
@ -32,13 +32,11 @@ THUMB_WIDTH = 320
|
|||||||
|
|
||||||
THRESHOLD_ALERT_ACTIVITY = 120
|
THRESHOLD_ALERT_ACTIVITY = 120
|
||||||
THRESHOLD_DETECTION_ACTIVITY = 30
|
THRESHOLD_DETECTION_ACTIVITY = 30
|
||||||
THRESHOLD_MOTION_ACTIVITY = 30
|
|
||||||
|
|
||||||
|
|
||||||
class SeverityEnum(str, Enum):
|
class SeverityEnum(str, Enum):
|
||||||
alert = "alert"
|
alert = "alert"
|
||||||
detection = "detection"
|
detection = "detection"
|
||||||
signification_motion = "significant_motion"
|
|
||||||
|
|
||||||
|
|
||||||
class PendingReviewSegment:
|
class PendingReviewSegment:
|
||||||
@ -50,7 +48,6 @@ class PendingReviewSegment:
|
|||||||
detections: dict[str, str],
|
detections: dict[str, str],
|
||||||
zones: set[str] = set(),
|
zones: set[str] = set(),
|
||||||
audio: set[str] = set(),
|
audio: set[str] = set(),
|
||||||
motion: list[int] = [],
|
|
||||||
):
|
):
|
||||||
rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
||||||
self.id = f"{frame_time}-{rand_id}"
|
self.id = f"{frame_time}-{rand_id}"
|
||||||
@ -60,7 +57,6 @@ class PendingReviewSegment:
|
|||||||
self.detections = detections
|
self.detections = detections
|
||||||
self.zones = zones
|
self.zones = zones
|
||||||
self.audio = audio
|
self.audio = audio
|
||||||
self.sig_motion_areas = motion
|
|
||||||
self.last_update = frame_time
|
self.last_update = frame_time
|
||||||
|
|
||||||
# thumbnail
|
# thumbnail
|
||||||
@ -117,7 +113,6 @@ class PendingReviewSegment:
|
|||||||
"objects": list(set(self.detections.values())),
|
"objects": list(set(self.detections.values())),
|
||||||
"zones": list(self.zones),
|
"zones": list(self.zones),
|
||||||
"audio": list(self.audio),
|
"audio": list(self.audio),
|
||||||
"significant_motion_areas": self.sig_motion_areas,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,7 +165,6 @@ class ReviewSegmentMaintainer(threading.Thread):
|
|||||||
segment: PendingReviewSegment,
|
segment: PendingReviewSegment,
|
||||||
frame_time: float,
|
frame_time: float,
|
||||||
objects: list[TrackedObject],
|
objects: list[TrackedObject],
|
||||||
motion: list,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Validate if existing review segment should continue."""
|
"""Validate if existing review segment should continue."""
|
||||||
camera_config = self.config.cameras[segment.camera]
|
camera_config = self.config.cameras[segment.camera]
|
||||||
@ -180,19 +174,6 @@ class ReviewSegmentMaintainer(threading.Thread):
|
|||||||
if frame_time > segment.last_update:
|
if frame_time > segment.last_update:
|
||||||
segment.last_update = frame_time
|
segment.last_update = frame_time
|
||||||
|
|
||||||
# update type for this segment now that active objects are detected
|
|
||||||
if segment.severity == SeverityEnum.signification_motion:
|
|
||||||
segment.severity = SeverityEnum.detection
|
|
||||||
|
|
||||||
if len(active_objects) > segment.frame_active_count:
|
|
||||||
frame_id = f"{camera_config.name}{frame_time}"
|
|
||||||
yuv_frame = self.frame_manager.get(
|
|
||||||
frame_id, camera_config.frame_shape_yuv
|
|
||||||
)
|
|
||||||
segment.update_frame(camera_config, yuv_frame, active_objects)
|
|
||||||
self.frame_manager.close(frame_id)
|
|
||||||
self.update_segment(segment)
|
|
||||||
|
|
||||||
for object in active_objects:
|
for object in active_objects:
|
||||||
if not object["sub_label"]:
|
if not object["sub_label"]:
|
||||||
segment.detections[object["id"]] = object["label"]
|
segment.detections[object["id"]] = object["label"]
|
||||||
@ -201,24 +182,38 @@ class ReviewSegmentMaintainer(threading.Thread):
|
|||||||
else:
|
else:
|
||||||
segment.detections[object["id"]] = f'{object["label"]}-verified'
|
segment.detections[object["id"]] = f'{object["label"]}-verified'
|
||||||
|
|
||||||
# if object is alert label and has qualified for recording
|
# if object is alert label
|
||||||
|
# and has entered required zones or required zones is not set
|
||||||
# mark this review as alert
|
# mark this review as alert
|
||||||
if (
|
if (
|
||||||
segment.severity == SeverityEnum.detection
|
segment.severity != SeverityEnum.alert
|
||||||
and object["has_clip"]
|
and object["label"] in camera_config.review.alerts.labels
|
||||||
and object["label"] in camera_config.objects.alert
|
and (
|
||||||
|
not camera_config.review.alerts.required_zones
|
||||||
|
or (
|
||||||
|
len(object["current_zones"]) > 0
|
||||||
|
and set(object["current_zones"])
|
||||||
|
& set(camera_config.review.alerts.required_zones)
|
||||||
|
)
|
||||||
|
)
|
||||||
):
|
):
|
||||||
segment.severity = SeverityEnum.alert
|
segment.severity = SeverityEnum.alert
|
||||||
|
|
||||||
# keep zones up to date
|
# keep zones up to date
|
||||||
if len(object["current_zones"]) > 0:
|
if len(object["current_zones"]) > 0:
|
||||||
segment.zones.update(object["current_zones"])
|
segment.zones.update(object["current_zones"])
|
||||||
elif (
|
|
||||||
segment.severity == SeverityEnum.signification_motion
|
if len(active_objects) > segment.frame_active_count:
|
||||||
and len(motion) >= THRESHOLD_MOTION_ACTIVITY
|
try:
|
||||||
):
|
frame_id = f"{camera_config.name}{frame_time}"
|
||||||
if frame_time > segment.last_update:
|
yuv_frame = self.frame_manager.get(
|
||||||
segment.last_update = frame_time
|
frame_id, camera_config.frame_shape_yuv
|
||||||
|
)
|
||||||
|
segment.update_frame(camera_config, yuv_frame, active_objects)
|
||||||
|
self.frame_manager.close(frame_id)
|
||||||
|
self.update_segment(segment)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
if segment.severity == SeverityEnum.alert and frame_time > (
|
if segment.severity == SeverityEnum.alert and frame_time > (
|
||||||
segment.last_update + THRESHOLD_ALERT_ACTIVITY
|
segment.last_update + THRESHOLD_ALERT_ACTIVITY
|
||||||
@ -232,7 +227,6 @@ class ReviewSegmentMaintainer(threading.Thread):
|
|||||||
camera: str,
|
camera: str,
|
||||||
frame_time: float,
|
frame_time: float,
|
||||||
objects: list[TrackedObject],
|
objects: list[TrackedObject],
|
||||||
motion: list,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Check if a new review segment should be created."""
|
"""Check if a new review segment should be created."""
|
||||||
camera_config = self.config.cameras[camera]
|
camera_config = self.config.cameras[camera]
|
||||||
@ -242,15 +236,9 @@ class ReviewSegmentMaintainer(threading.Thread):
|
|||||||
has_sig_object = False
|
has_sig_object = False
|
||||||
detections: dict[str, str] = {}
|
detections: dict[str, str] = {}
|
||||||
zones: set = set()
|
zones: set = set()
|
||||||
|
severity = None
|
||||||
|
|
||||||
for object in active_objects:
|
for object in active_objects:
|
||||||
if (
|
|
||||||
not has_sig_object
|
|
||||||
and object["has_clip"]
|
|
||||||
and object["label"] in camera_config.objects.alert
|
|
||||||
):
|
|
||||||
has_sig_object = True
|
|
||||||
|
|
||||||
if not object["sub_label"]:
|
if not object["sub_label"]:
|
||||||
detections[object["id"]] = object["label"]
|
detections[object["id"]] = object["label"]
|
||||||
elif object["sub_label"][0] in ALL_ATTRIBUTE_LABELS:
|
elif object["sub_label"][0] in ALL_ATTRIBUTE_LABELS:
|
||||||
@ -258,35 +246,68 @@ class ReviewSegmentMaintainer(threading.Thread):
|
|||||||
else:
|
else:
|
||||||
detections[object["id"]] = f'{object["label"]}-verified'
|
detections[object["id"]] = f'{object["label"]}-verified'
|
||||||
|
|
||||||
|
# if object is alert label
|
||||||
|
# and has entered required zones or required zones is not set
|
||||||
|
# mark this review as alert
|
||||||
|
if (
|
||||||
|
severity != SeverityEnum.alert
|
||||||
|
and object["label"] in camera_config.review.alerts.labels
|
||||||
|
and (
|
||||||
|
not camera_config.review.alerts.required_zones
|
||||||
|
or (
|
||||||
|
len(object["current_zones"]) > 0
|
||||||
|
and set(object["current_zones"])
|
||||||
|
& set(camera_config.review.alerts.required_zones)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
severity = SeverityEnum.alert
|
||||||
|
|
||||||
|
# if object is detection label
|
||||||
|
# and review is not already a detection or alert
|
||||||
|
# and has entered required zones or required zones is not set
|
||||||
|
# mark this review as alert
|
||||||
|
if (
|
||||||
|
not severity
|
||||||
|
and (
|
||||||
|
not camera_config.review.detections.labels
|
||||||
|
or object["label"] in (camera_config.review.detections.labels)
|
||||||
|
)
|
||||||
|
and (
|
||||||
|
not camera_config.review.detections.required_zones
|
||||||
|
or (
|
||||||
|
len(object["current_zones"]) > 0
|
||||||
|
and set(object["current_zones"])
|
||||||
|
& set(camera_config.review.detections.required_zones)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
severity = SeverityEnum.detection
|
||||||
|
|
||||||
zones.update(object["current_zones"])
|
zones.update(object["current_zones"])
|
||||||
|
|
||||||
self.active_review_segments[camera] = PendingReviewSegment(
|
if severity:
|
||||||
camera,
|
self.active_review_segments[camera] = PendingReviewSegment(
|
||||||
frame_time,
|
camera,
|
||||||
SeverityEnum.alert if has_sig_object else SeverityEnum.detection,
|
frame_time,
|
||||||
detections,
|
SeverityEnum.alert if has_sig_object else SeverityEnum.detection,
|
||||||
audio=set(),
|
detections,
|
||||||
zones=zones,
|
audio=set(),
|
||||||
motion=[],
|
zones=zones,
|
||||||
)
|
)
|
||||||
|
|
||||||
frame_id = f"{camera_config.name}{frame_time}"
|
try:
|
||||||
yuv_frame = self.frame_manager.get(frame_id, camera_config.frame_shape_yuv)
|
frame_id = f"{camera_config.name}{frame_time}"
|
||||||
self.active_review_segments[camera].update_frame(
|
yuv_frame = self.frame_manager.get(
|
||||||
camera_config, yuv_frame, active_objects
|
frame_id, camera_config.frame_shape_yuv
|
||||||
)
|
)
|
||||||
self.frame_manager.close(frame_id)
|
self.active_review_segments[camera].update_frame(
|
||||||
self.update_segment(self.active_review_segments[camera])
|
camera_config, yuv_frame, active_objects
|
||||||
elif len(motion) >= 20:
|
)
|
||||||
self.active_review_segments[camera] = PendingReviewSegment(
|
self.frame_manager.close(frame_id)
|
||||||
camera,
|
self.update_segment(self.active_review_segments[camera])
|
||||||
frame_time,
|
except FileNotFoundError:
|
||||||
SeverityEnum.signification_motion,
|
return
|
||||||
detections={},
|
|
||||||
audio=set(),
|
|
||||||
motion=motion,
|
|
||||||
zones=set(),
|
|
||||||
)
|
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
while not self.stop_event.is_set():
|
while not self.stop_event.is_set():
|
||||||
@ -344,13 +365,22 @@ class ReviewSegmentMaintainer(threading.Thread):
|
|||||||
current_segment,
|
current_segment,
|
||||||
frame_time,
|
frame_time,
|
||||||
current_tracked_objects,
|
current_tracked_objects,
|
||||||
motion_boxes,
|
|
||||||
)
|
)
|
||||||
elif topic == DetectionTypeEnum.audio and len(audio_detections) > 0:
|
elif topic == DetectionTypeEnum.audio and len(audio_detections) > 0:
|
||||||
|
camera_config = self.config.cameras[camera]
|
||||||
|
|
||||||
if frame_time > current_segment.last_update:
|
if frame_time > current_segment.last_update:
|
||||||
current_segment.last_update = frame_time
|
current_segment.last_update = frame_time
|
||||||
|
|
||||||
current_segment.audio.update(audio_detections)
|
for audio in audio_detections:
|
||||||
|
if audio in camera_config.review.alerts.labels:
|
||||||
|
current_segment.audio.add(audio)
|
||||||
|
current_segment.severity = SeverityEnum.alert
|
||||||
|
elif (
|
||||||
|
not camera_config.review.detections.labels
|
||||||
|
or audio in camera_config.review.detections.labels
|
||||||
|
):
|
||||||
|
current_segment.audio.add(audio)
|
||||||
elif topic == DetectionTypeEnum.api:
|
elif topic == DetectionTypeEnum.api:
|
||||||
if manual_info["state"] == ManualEventState.complete:
|
if manual_info["state"] == ManualEventState.complete:
|
||||||
current_segment.detections[manual_info["event_id"]] = (
|
current_segment.detections[manual_info["event_id"]] = (
|
||||||
@ -378,18 +408,35 @@ class ReviewSegmentMaintainer(threading.Thread):
|
|||||||
camera,
|
camera,
|
||||||
frame_time,
|
frame_time,
|
||||||
current_tracked_objects,
|
current_tracked_objects,
|
||||||
motion_boxes,
|
|
||||||
)
|
)
|
||||||
elif topic == DetectionTypeEnum.audio and len(audio_detections) > 0:
|
elif topic == DetectionTypeEnum.audio and len(audio_detections) > 0:
|
||||||
self.active_review_segments[camera] = PendingReviewSegment(
|
severity = None
|
||||||
camera,
|
|
||||||
frame_time,
|
camera_config = self.config.cameras[camera]
|
||||||
SeverityEnum.detection,
|
detections = set()
|
||||||
{},
|
|
||||||
set(),
|
for audio in audio_detections:
|
||||||
set(audio_detections),
|
if audio in camera_config.review.alerts.labels:
|
||||||
[],
|
detections.add(audio)
|
||||||
)
|
severity = SeverityEnum.alert
|
||||||
|
elif (
|
||||||
|
not camera_config.review.detections.labels
|
||||||
|
or audio in camera_config.review.detections.labels
|
||||||
|
):
|
||||||
|
detections.add(audio)
|
||||||
|
|
||||||
|
if not severity:
|
||||||
|
severity = SeverityEnum.detection
|
||||||
|
|
||||||
|
if severity:
|
||||||
|
self.active_review_segments[camera] = PendingReviewSegment(
|
||||||
|
camera,
|
||||||
|
frame_time,
|
||||||
|
severity,
|
||||||
|
{},
|
||||||
|
set(),
|
||||||
|
detections,
|
||||||
|
)
|
||||||
elif topic == DetectionTypeEnum.api:
|
elif topic == DetectionTypeEnum.api:
|
||||||
self.active_review_segments[camera] = PendingReviewSegment(
|
self.active_review_segments[camera] = PendingReviewSegment(
|
||||||
camera,
|
camera,
|
||||||
@ -398,7 +445,6 @@ class ReviewSegmentMaintainer(threading.Thread):
|
|||||||
{manual_info["event_id"]: manual_info["label"]},
|
{manual_info["event_id"]: manual_info["label"]},
|
||||||
set(),
|
set(),
|
||||||
set(),
|
set(),
|
||||||
[],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if manual_info["state"] == ManualEventState.start:
|
if manual_info["state"] == ManualEventState.start:
|
||||||
@ -425,8 +471,16 @@ def get_active_objects(
|
|||||||
return [
|
return [
|
||||||
o
|
o
|
||||||
for o in all_objects
|
for o in all_objects
|
||||||
if o["motionless_count"] < camera_config.detect.stationary.threshold
|
if o["motionless_count"]
|
||||||
and o["position_changes"] > 0
|
< camera_config.detect.stationary.threshold # no stationary objects
|
||||||
and o["frame_time"] == frame_time
|
and o["position_changes"] > 0 # object must have moved at least once
|
||||||
and not o["false_positive"]
|
and o["frame_time"] == frame_time # object must be detected in this frame
|
||||||
|
and not o["false_positive"] # object must not be a false positive
|
||||||
|
and (
|
||||||
|
o["label"] in camera_config.review.alerts.labels
|
||||||
|
or (
|
||||||
|
not camera_config.review.detections.labels
|
||||||
|
or o["label"] in camera_config.review.detections.labels
|
||||||
|
)
|
||||||
|
) # object must be in the alerts or detections label list
|
||||||
]
|
]
|
||||||
|
127
frigate/util/config.py
Normal file
127
frigate/util/config.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
"""configuration utils."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from ruamel.yaml import YAML
|
||||||
|
|
||||||
|
from frigate.const import CONFIG_DIR
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CURRENT_CONFIG_VERSION = 0.14
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_frigate_config(config_file: str):
|
||||||
|
"""handle migrating the frigate config."""
|
||||||
|
logger.info("Checking if frigate config needs migration...")
|
||||||
|
version_file = os.path.join(CONFIG_DIR, ".version")
|
||||||
|
|
||||||
|
if not os.path.isfile(version_file):
|
||||||
|
previous_version = 0.13
|
||||||
|
else:
|
||||||
|
with open(version_file) as f:
|
||||||
|
try:
|
||||||
|
previous_version = float(f.readline())
|
||||||
|
except Exception:
|
||||||
|
previous_version = 0.13
|
||||||
|
|
||||||
|
if previous_version == CURRENT_CONFIG_VERSION:
|
||||||
|
logger.info("frigate config does not need migration...")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("copying config as backup...")
|
||||||
|
shutil.copy(config_file, os.path.join(CONFIG_DIR, "backup_config.yaml"))
|
||||||
|
|
||||||
|
yaml = YAML()
|
||||||
|
yaml.indent(mapping=2, sequence=4, offset=2)
|
||||||
|
with open(config_file, "r") as f:
|
||||||
|
config: dict[str, dict[str, any]] = yaml.load(f)
|
||||||
|
|
||||||
|
if previous_version < 0.14:
|
||||||
|
logger.info(f"Migrating frigate config from {previous_version} to 0.14...")
|
||||||
|
new_config = migrate_014(config)
|
||||||
|
with open(config_file, "w") as f:
|
||||||
|
yaml.dump(new_config, f)
|
||||||
|
previous_version = 0.14
|
||||||
|
|
||||||
|
with open(version_file, "w") as f:
|
||||||
|
f.write(str(CURRENT_CONFIG_VERSION))
|
||||||
|
|
||||||
|
logger.info("Finished frigate config migration...")
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_014(config: dict[str, dict[str, any]]) -> dict[str, dict[str, any]]:
|
||||||
|
"""Handle migrating frigate config to 0.14"""
|
||||||
|
# migrate record.events.required_zones to review.alerts.required_zones
|
||||||
|
new_config = config.copy()
|
||||||
|
global_required_zones = (
|
||||||
|
config.get("record", {}).get("events", {}).get("required_zones", [])
|
||||||
|
)
|
||||||
|
|
||||||
|
if global_required_zones:
|
||||||
|
# migrate to new review config
|
||||||
|
if not new_config.get("review"):
|
||||||
|
new_config["review"] = {}
|
||||||
|
|
||||||
|
if not new_config["review"].get("alerts"):
|
||||||
|
new_config["review"]["alerts"] = {}
|
||||||
|
|
||||||
|
if not new_config["review"]["alerts"].get("required_zones"):
|
||||||
|
new_config["review"]["alerts"]["required_zones"] = global_required_zones
|
||||||
|
|
||||||
|
# remove record required zones config
|
||||||
|
del new_config["record"]["events"]["required_zones"]
|
||||||
|
|
||||||
|
# remove record altogether if there is not other config
|
||||||
|
if not new_config["record"]["events"]:
|
||||||
|
del new_config["record"]["events"]
|
||||||
|
|
||||||
|
if not new_config["record"]:
|
||||||
|
del new_config["record"]
|
||||||
|
|
||||||
|
# remove rtmp
|
||||||
|
if new_config.get("ffmpeg", {}).get("output_args", {}).get("rtmp"):
|
||||||
|
del new_config["ffmpeg"]["output_args"]["rtmp"]
|
||||||
|
|
||||||
|
if new_config.get("rtmp"):
|
||||||
|
del new_config["rtmp"]
|
||||||
|
|
||||||
|
for name, camera in config.get("cameras", {}).items():
|
||||||
|
camera_config: dict[str, dict[str, any]] = camera.copy()
|
||||||
|
required_zones = (
|
||||||
|
camera_config.get("record", {}).get("events", {}).get("required_zones", [])
|
||||||
|
)
|
||||||
|
|
||||||
|
if required_zones:
|
||||||
|
# migrate to new review config
|
||||||
|
if not camera_config.get("review"):
|
||||||
|
camera_config["review"] = {}
|
||||||
|
|
||||||
|
if not camera_config["review"].get("alerts"):
|
||||||
|
camera_config["review"]["alerts"] = {}
|
||||||
|
|
||||||
|
if not camera_config["review"]["alerts"].get("required_zones"):
|
||||||
|
camera_config["review"]["alerts"]["required_zones"] = required_zones
|
||||||
|
|
||||||
|
# remove record required zones config
|
||||||
|
del camera_config["record"]["events"]["required_zones"]
|
||||||
|
|
||||||
|
# remove record altogether if there is not other config
|
||||||
|
if not camera_config["record"]["events"]:
|
||||||
|
del camera_config["record"]["events"]
|
||||||
|
|
||||||
|
if not camera_config["record"]:
|
||||||
|
del camera_config["record"]
|
||||||
|
|
||||||
|
# remove rtmp
|
||||||
|
if camera_config.get("ffmpeg", {}).get("output_args", {}).get("rtmp"):
|
||||||
|
del camera_config["ffmpeg"]["output_args"]["rtmp"]
|
||||||
|
|
||||||
|
if camera_config.get("rtmp"):
|
||||||
|
del camera_config["rtmp"]
|
||||||
|
|
||||||
|
new_config["cameras"][name] = camera_config
|
||||||
|
|
||||||
|
return new_config
|
Loading…
Reference in New Issue
Block a user