mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-10 00:32:48 +03:00
Fix detection not correctly being started after an alert ends (#24534)
* Fix detection not correctly being started after an alert ends * Improve coverage * Fix incorrect behavior * Handle unsent thumbs * Add more tests
This commit is contained in:
+135
-55
@@ -66,6 +66,9 @@ class PendingReviewSegment:
|
||||
self.zones = zones
|
||||
self.audio = audio
|
||||
self.classification_state_changes: list[dict[str, Any]] = []
|
||||
# detection-level activity after the last alert activity, split by the
|
||||
# detection cutoff, these are published when the alert is cut off
|
||||
self.pending_detections: list[PendingReviewSegment] = []
|
||||
self.thumb_time: float | None = None
|
||||
self.last_alert_time: float | None = None
|
||||
self.last_detection_time: float = frame_time
|
||||
@@ -83,6 +86,20 @@ class PendingReviewSegment:
|
||||
CLIPS_DIR, f"review/thumb-{self.camera}-{self.id}.webp"
|
||||
)
|
||||
|
||||
def add_object(self, obj: dict[str, Any], attributes: list[str]) -> None:
|
||||
"""Add a tracked object's label, sub label, and zones to the segment."""
|
||||
if not obj["sub_label"]:
|
||||
self.detections[obj["id"]] = obj["label"]
|
||||
elif obj["sub_label"][0] in attributes:
|
||||
self.detections[obj["id"]] = obj["sub_label"][0]
|
||||
else:
|
||||
self.detections[obj["id"]] = f"{obj['label']}-verified"
|
||||
self.sub_labels[obj["id"]] = obj["sub_label"][0]
|
||||
|
||||
for zone in obj["current_zones"]:
|
||||
if zone not in self.zones:
|
||||
self.zones.append(zone)
|
||||
|
||||
def update_frame(
|
||||
self,
|
||||
camera_config: CameraConfig,
|
||||
@@ -435,9 +452,29 @@ class ReviewSegmentMaintainer(threading.Thread):
|
||||
segment.last_detection_time = now
|
||||
|
||||
prev_data = segment.get_data(False)
|
||||
return self._publish_segment_end(segment, prev_data)
|
||||
end_time = self._publish_segment_end(segment, prev_data)
|
||||
self._publish_pending_detections(segment, None)
|
||||
return end_time
|
||||
return None
|
||||
|
||||
def _publish_pending_detections(
|
||||
self, segment: PendingReviewSegment, ongoing_since: float | None
|
||||
) -> None:
|
||||
"""Publish the detections held while an ended alert was active.
|
||||
|
||||
A detection with activity after ongoing_since stays open, only the
|
||||
latest can. With None every detection is ended, this does not read the
|
||||
camera config since a removed camera is no longer in it.
|
||||
"""
|
||||
for pending in segment.pending_detections:
|
||||
self._activate_segment(pending)
|
||||
self._publish_segment_start(pending)
|
||||
|
||||
if ongoing_since is None or pending.last_detection_time < ongoing_since:
|
||||
self._publish_segment_end(pending, pending.get_data(False))
|
||||
|
||||
segment.pending_detections = []
|
||||
|
||||
def get_manual_event_severity(self, camera: str, label: str) -> SeverityEnum | None:
|
||||
"""Determine the review severity for a manual event label.
|
||||
|
||||
@@ -470,6 +507,55 @@ class ReviewSegmentMaintainer(threading.Thread):
|
||||
self.indefinite_events.pop(camera, None)
|
||||
self.recent_classification_state_changes.pop(camera, None)
|
||||
|
||||
def _track_pending_detection(
|
||||
self,
|
||||
segment: PendingReviewSegment,
|
||||
camera_config: CameraConfig,
|
||||
frame_name: str,
|
||||
frame_time: float,
|
||||
objects: list[dict[str, Any]],
|
||||
) -> None:
|
||||
"""Hold detection-level activity seen after an alert's last alert
|
||||
activity, starting a separate detection when the gap since the previous
|
||||
activity exceeds the detection cutoff."""
|
||||
pending = segment.pending_detections[-1] if segment.pending_detections else None
|
||||
|
||||
if pending is None or frame_time > (
|
||||
pending.last_detection_time + camera_config.review.detections.cutoff_time
|
||||
):
|
||||
pending = PendingReviewSegment(
|
||||
segment.camera,
|
||||
frame_time,
|
||||
SeverityEnum.detection,
|
||||
{},
|
||||
sub_labels={},
|
||||
audio=set(),
|
||||
zones=[],
|
||||
)
|
||||
segment.pending_detections.append(pending)
|
||||
|
||||
pending.last_detection_time = frame_time
|
||||
|
||||
for obj in objects:
|
||||
pending.add_object(obj, self.config.all_attributes)
|
||||
|
||||
if len(objects) <= pending.frame_active_count:
|
||||
return
|
||||
|
||||
try:
|
||||
yuv_frame = self.frame_manager.get(
|
||||
frame_name, camera_config.frame_shape_yuv
|
||||
)
|
||||
except FileNotFoundError:
|
||||
return
|
||||
|
||||
if yuv_frame is None:
|
||||
logger.debug(f"Failed to get frame {frame_name} from SHM")
|
||||
return
|
||||
|
||||
pending.update_frame(camera_config, yuv_frame, objects)
|
||||
self.frame_manager.close(frame_name)
|
||||
|
||||
def update_existing_segment(
|
||||
self,
|
||||
segment: PendingReviewSegment,
|
||||
@@ -505,6 +591,21 @@ class ReviewSegmentMaintainer(threading.Thread):
|
||||
should_update_state = True
|
||||
should_update_image = True
|
||||
|
||||
# alert activity resumed, so the pending detection activity
|
||||
# falls within this alert
|
||||
for pending in segment.pending_detections:
|
||||
segment.detections.update(pending.detections)
|
||||
segment.sub_labels.update(pending.sub_labels)
|
||||
|
||||
for zone in pending.zones:
|
||||
if zone not in segment.zones:
|
||||
segment.zones.append(zone)
|
||||
|
||||
Path(pending.frame_path).unlink(missing_ok=True)
|
||||
should_update_state = True
|
||||
|
||||
segment.pending_detections = []
|
||||
|
||||
if activity.has_activity_category(SeverityEnum.detection):
|
||||
if (
|
||||
segment.last_detection_time is None
|
||||
@@ -512,6 +613,8 @@ class ReviewSegmentMaintainer(threading.Thread):
|
||||
):
|
||||
segment.last_detection_time = frame_time
|
||||
|
||||
pending_objects: list[dict[str, Any]] = []
|
||||
|
||||
for object in activity.get_all_objects():
|
||||
# Alert-level objects should always be added (they extend/upgrade the segment)
|
||||
# Detection-level objects should only be added if:
|
||||
@@ -522,23 +625,22 @@ class ReviewSegmentMaintainer(threading.Thread):
|
||||
|
||||
if not is_alert_object and segment.severity == SeverityEnum.alert:
|
||||
# This is a detection-level object
|
||||
if (
|
||||
segment.last_alert_time is not None
|
||||
and frame_time > segment.last_alert_time
|
||||
):
|
||||
pending_objects.append(object)
|
||||
|
||||
# Only add if it started during the alert's active period
|
||||
if object["start_time"] > segment.last_alert_time:
|
||||
continue
|
||||
|
||||
if not object["sub_label"]:
|
||||
segment.detections[object["id"]] = object["label"]
|
||||
elif object["sub_label"][0] in self.config.all_attributes:
|
||||
segment.detections[object["id"]] = object["sub_label"][0]
|
||||
else:
|
||||
segment.detections[object["id"]] = f"{object['label']}-verified"
|
||||
segment.sub_labels[object["id"]] = object["sub_label"][0]
|
||||
segment.add_object(object, self.config.all_attributes)
|
||||
|
||||
# keep zones up to date
|
||||
if len(object["current_zones"]) > 0:
|
||||
for zone in object["current_zones"]:
|
||||
if zone not in segment.zones:
|
||||
segment.zones.append(zone)
|
||||
if pending_objects:
|
||||
self._track_pending_detection(
|
||||
segment, camera_config, frame_name, frame_time, pending_objects
|
||||
)
|
||||
|
||||
if len(activity.get_all_objects()) > segment.frame_active_count:
|
||||
should_update_state = True
|
||||
@@ -590,50 +692,28 @@ class ReviewSegmentMaintainer(threading.Thread):
|
||||
except FileNotFoundError:
|
||||
return
|
||||
|
||||
if (
|
||||
segment.severity == SeverityEnum.alert
|
||||
and segment.last_alert_time is not None
|
||||
and frame_time
|
||||
> (segment.last_alert_time + camera_config.review.alerts.cutoff_time)
|
||||
):
|
||||
needs_new_detection = (
|
||||
segment.last_detection_time > segment.last_alert_time
|
||||
and (
|
||||
segment.last_detection_time
|
||||
+ camera_config.review.detections.cutoff_time
|
||||
)
|
||||
> frame_time
|
||||
)
|
||||
last_detection_time = segment.last_detection_time
|
||||
|
||||
end_time = self._publish_segment_end(segment, prev_data)
|
||||
|
||||
if needs_new_detection:
|
||||
new_detections: dict[str, str] = {}
|
||||
new_zones = set()
|
||||
|
||||
for o in activity.categorized_objects["detections"]:
|
||||
new_detections[o["id"]] = o["label"]
|
||||
new_zones.update(o["current_zones"])
|
||||
|
||||
if new_detections:
|
||||
new_segment = PendingReviewSegment(
|
||||
segment.camera,
|
||||
end_time,
|
||||
SeverityEnum.detection,
|
||||
new_detections,
|
||||
sub_labels={},
|
||||
audio=set(),
|
||||
zones=list(new_zones),
|
||||
)
|
||||
self._activate_segment(new_segment)
|
||||
self._publish_segment_start(new_segment)
|
||||
new_segment.last_detection_time = last_detection_time
|
||||
elif segment.severity == SeverityEnum.detection and frame_time > (
|
||||
# detection-level activity must not keep an alert open, it continues
|
||||
# in a new detection segment once the alert is cut off
|
||||
if (
|
||||
segment.severity == SeverityEnum.alert
|
||||
and segment.last_alert_time is not None
|
||||
and frame_time
|
||||
> (segment.last_alert_time + camera_config.review.alerts.cutoff_time)
|
||||
):
|
||||
self._publish_segment_end(segment, prev_data)
|
||||
self._publish_pending_detections(
|
||||
segment, frame_time - camera_config.review.detections.cutoff_time
|
||||
)
|
||||
elif (
|
||||
not has_activity
|
||||
and segment.severity == SeverityEnum.detection
|
||||
and frame_time
|
||||
> (
|
||||
segment.last_detection_time
|
||||
+ camera_config.review.detections.cutoff_time
|
||||
):
|
||||
self._publish_segment_end(segment, prev_data)
|
||||
)
|
||||
):
|
||||
self._publish_segment_end(segment, prev_data)
|
||||
|
||||
def check_if_new_segment(
|
||||
self,
|
||||
|
||||
Reference in New Issue
Block a user