From 93260f6cfdcdaa2ea239d78c24cd3d0d7cd1edd9 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Fri, 15 Mar 2024 09:29:22 -0600 Subject: [PATCH] Add region count to database and use for motion activity (#10480) * Add region count to database and use for motion activity * Fix test --- frigate/api/review.py | 11 ++++-- frigate/models.py | 1 + frigate/record/maintainer.py | 15 ++++++-- frigate/test/test_record_retention.py | 16 ++++++--- migrations/006_add_motion_active_objects.py | 3 ++ migrations/023_add_regions.py | 39 +++++++++++++++++++++ 6 files changed, 75 insertions(+), 10 deletions(-) create mode 100644 migrations/023_add_regions.py diff --git a/frigate/api/review.py b/frigate/api/review.py index 7cc3d2695..6d3ba1c3f 100644 --- a/frigate/api/review.py +++ b/frigate/api/review.py @@ -358,7 +358,6 @@ def motion_activity(): ) clauses = [(Recordings.start_time > after) & (Recordings.end_time < before)] - clauses.append((Recordings.motion <= 100)) if cameras != "all": camera_list = cameras.split(",") @@ -367,7 +366,7 @@ def motion_activity(): data: list[Recordings] = ( Recordings.select( Recordings.start_time, - Recordings.motion, + Recordings.regions, ) .where(reduce(operator.and_, clauses)) .order_by(Recordings.start_time.asc()) @@ -379,7 +378,8 @@ def motion_activity(): scale = request.args.get("scale", type=int, default=30) # resample data using pandas to get activity on scaled basis - df = pd.DataFrame(data, columns=["start_time", "motion"]) + df = pd.DataFrame(data, columns=["start_time", "regions"]) + df = df.rename(columns={"regions": "motion"}) # set date as datetime index df["start_time"] = pd.to_datetime(df["start_time"], unit="s") @@ -391,6 +391,11 @@ def motion_activity(): .apply(lambda x: max(x, key=abs, default=0.0)) .fillna(0.0) ) + df["motion"] = ( + (df["motion"] - df["motion"].min()) + / (df["motion"].max() - df["motion"].min()) + * 100 + ) # change types for output df.index = df.index.astype(int) // (10**9) diff --git a/frigate/models.py b/frigate/models.py index 87424e3a8..a9b0f16ca 100644 --- a/frigate/models.py +++ b/frigate/models.py @@ -74,6 +74,7 @@ class Recordings(Model): # type: ignore[misc] objects = IntegerField(null=True) dBFS = IntegerField(null=True) segment_size = FloatField(default=0) # this should be stored as MB + regions = IntegerField(null=True) class ReviewSegment(Model): # type: ignore[misc] diff --git a/frigate/record/maintainer.py b/frigate/record/maintainer.py index 71c7a6a3d..89395db12 100644 --- a/frigate/record/maintainer.py +++ b/frigate/record/maintainer.py @@ -38,10 +38,15 @@ QUEUE_READ_TIMEOUT = 0.00001 # seconds class SegmentInfo: def __init__( - self, motion_area: int, active_object_count: int, average_dBFS: int + self, + motion_area: int, + active_object_count: int, + region_count: int, + average_dBFS: int, ) -> None: self.motion_area = motion_area self.active_object_count = active_object_count + self.region_count = region_count self.average_dBFS = average_dBFS def should_discard_segment(self, retain_mode: RetainModeEnum) -> bool: @@ -298,6 +303,7 @@ class RecordingMaintainer(threading.Thread): ) -> SegmentInfo: video_frame_count = 0 active_count = 0 + region_count = 0 total_motion_area = 0 for frame in self.object_recordings_info[camera]: # frame is after end time of segment @@ -315,8 +321,8 @@ class RecordingMaintainer(threading.Thread): if not o["false_positive"] and o["motionless_count"] == 0 ] ) - total_motion_area += sum([area(box) for box in frame[2]]) + region_count += len(frame[3]) if video_frame_count > 0: normalized_motion_area = min( @@ -350,7 +356,9 @@ class RecordingMaintainer(threading.Thread): average_dBFS = 0 if not audio_values else np.average(audio_values) - return SegmentInfo(normalized_motion_area, active_count, round(average_dBFS)) + return SegmentInfo( + normalized_motion_area, active_count, region_count, round(average_dBFS) + ) async def move_segment( self, @@ -438,6 +446,7 @@ class RecordingMaintainer(threading.Thread): Recordings.motion: segment_info.motion_area, # TODO: update this to store list of active objects at some point Recordings.objects: segment_info.active_object_count, + Recordings.regions: segment_info.region_count, Recordings.dBFS: segment_info.average_dBFS, Recordings.segment_size: segment_size, } diff --git a/frigate/test/test_record_retention.py b/frigate/test/test_record_retention.py index 81230449d..4620c9a3e 100644 --- a/frigate/test/test_record_retention.py +++ b/frigate/test/test_record_retention.py @@ -6,20 +6,28 @@ from frigate.record.maintainer import SegmentInfo class TestRecordRetention(unittest.TestCase): def test_motion_should_keep_motion_not_object(self): - segment_info = SegmentInfo(motion_area=1, active_object_count=0, average_dBFS=0) + segment_info = SegmentInfo( + motion_area=1, active_object_count=0, region_count=0, average_dBFS=0 + ) assert not segment_info.should_discard_segment(RetainModeEnum.motion) assert segment_info.should_discard_segment(RetainModeEnum.active_objects) def test_object_should_keep_object_not_motion(self): - segment_info = SegmentInfo(motion_area=0, active_object_count=1, average_dBFS=0) + segment_info = SegmentInfo( + motion_area=0, active_object_count=1, region_count=0, average_dBFS=0 + ) assert segment_info.should_discard_segment(RetainModeEnum.motion) assert not segment_info.should_discard_segment(RetainModeEnum.active_objects) def test_all_should_keep_all(self): - segment_info = SegmentInfo(motion_area=0, active_object_count=0, average_dBFS=0) + segment_info = SegmentInfo( + motion_area=0, active_object_count=0, region_count=0, average_dBFS=0 + ) assert not segment_info.should_discard_segment(RetainModeEnum.all) def test_should_keep_audio_in_motion_mode(self): - segment_info = SegmentInfo(motion_area=0, active_object_count=0, average_dBFS=1) + segment_info = SegmentInfo( + motion_area=0, active_object_count=0, region_count=0, average_dBFS=1 + ) assert not segment_info.should_discard_segment(RetainModeEnum.motion) assert segment_info.should_discard_segment(RetainModeEnum.active_objects) diff --git a/migrations/006_add_motion_active_objects.py b/migrations/006_add_motion_active_objects.py index 2980b441d..6ab67ee3a 100644 --- a/migrations/006_add_motion_active_objects.py +++ b/migrations/006_add_motion_active_objects.py @@ -34,6 +34,9 @@ def migrate(migrator, database, fake=False, **kwargs): objects=pw.IntegerField(null=True), motion=pw.IntegerField(null=True), ) + migrator.sql( + 'CREATE INDEX "recordings_activity" ON "recordings" ("camera", "start_time" DESC, "regions")' + ) def rollback(migrator, database, fake=False, **kwargs): diff --git a/migrations/023_add_regions.py b/migrations/023_add_regions.py new file mode 100644 index 000000000..17d93962a --- /dev/null +++ b/migrations/023_add_regions.py @@ -0,0 +1,39 @@ +"""Peewee migrations -- 023_add_regions.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.python(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +from frigate.models import Recordings + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.add_fields( + Recordings, + regions=pw.IntegerField(null=True), + ) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.remove_fields(Recordings, ["regions"])