mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-06-21 20:01:54 +03:00
* refactor motion search * cleanup dead code and tests * tweaks * fix multi-day seeking * start playback a few seconds before the change so the motion is in view
88 lines
3.6 KiB
Python
88 lines
3.6 KiB
Python
"""Tests for motion search spatial (crop/scale/mask) helpers."""
|
|
|
|
import unittest
|
|
|
|
import numpy as np
|
|
|
|
from frigate.jobs.motion_search import (
|
|
build_scaled_roi_mask,
|
|
compute_roi_crop_and_scale,
|
|
detect_motion_scaled,
|
|
)
|
|
|
|
|
|
class TestComputeRoiCropAndScale(unittest.TestCase):
|
|
def test_crop_box_in_record_pixels(self):
|
|
# ROI covering x [0.25, 0.75], y [0.5, 1.0] of a 1000x600 frame.
|
|
polygon = [[0.25, 0.5], [0.75, 0.5], [0.75, 1.0], [0.25, 1.0]]
|
|
crop, scaled = compute_roi_crop_and_scale(polygon, 1000, 600, scale_target=125)
|
|
cw, ch, cx, cy = crop
|
|
self.assertEqual((cx, cy), (250, 300))
|
|
self.assertEqual((cw, ch), (500, 300))
|
|
# longest side 500 -> factor 0.25 -> (125, 75), rounded down to even.
|
|
self.assertEqual(scaled, (124, 74))
|
|
|
|
def test_never_upscales(self):
|
|
polygon = [[0.0, 0.0], [0.1, 0.0], [0.1, 0.1], [0.0, 0.1]]
|
|
crop, scaled = compute_roi_crop_and_scale(polygon, 200, 200, scale_target=400)
|
|
cw, ch, _, _ = crop
|
|
# crop is 20x20; target 400 would upscale, so scaled == crop size.
|
|
self.assertEqual(scaled, (cw, ch))
|
|
|
|
def test_scaled_dims_are_at_least_one(self):
|
|
polygon = [[0.0, 0.0], [0.02, 0.0], [0.02, 0.02], [0.0, 0.02]]
|
|
crop, scaled = compute_roi_crop_and_scale(polygon, 50, 50, scale_target=1)
|
|
self.assertGreaterEqual(scaled[0], 1)
|
|
self.assertGreaterEqual(scaled[1], 1)
|
|
|
|
def test_all_dims_are_even_for_nv12(self):
|
|
# Odd-aligned ROI on an odd-ish frame must still yield even crop/scale so
|
|
# the nv12 hwdownload byte stream matches the expected frame size.
|
|
polygon = [[0.123, 0.321], [0.777, 0.321], [0.777, 0.901], [0.123, 0.901]]
|
|
crop, scaled = compute_roi_crop_and_scale(polygon, 1377, 911, scale_target=257)
|
|
for value in (*crop, *scaled):
|
|
self.assertEqual(value % 2, 0, f"{value} is not even")
|
|
|
|
|
|
class TestBuildScaledRoiMask(unittest.TestCase):
|
|
def test_mask_matches_scaled_dims_and_has_coverage(self):
|
|
polygon = [[0.25, 0.5], [0.75, 0.5], [0.75, 1.0], [0.25, 1.0]]
|
|
crop, scaled = compute_roi_crop_and_scale(polygon, 1000, 600, scale_target=125)
|
|
mask = build_scaled_roi_mask(polygon, 1000, 600, crop, scaled)
|
|
self.assertEqual(mask.shape, (scaled[1], scaled[0]))
|
|
self.assertEqual(mask.dtype, np.uint8)
|
|
# A full rectangle ROI fills its whole crop -> mask is all 255.
|
|
self.assertGreater(np.count_nonzero(mask), 0)
|
|
self.assertEqual(np.count_nonzero(mask), mask.size)
|
|
|
|
|
|
class TestDetectMotionScaled(unittest.TestCase):
|
|
def _ts(self, idx):
|
|
return float(idx)
|
|
|
|
def test_finds_change_between_frames(self):
|
|
mask = np.full((60, 80), 255, dtype=np.uint8)
|
|
f0 = np.zeros((60, 80), dtype=np.uint8)
|
|
f1 = np.zeros((60, 80), dtype=np.uint8)
|
|
f1[10:50, 20:60] = 255 # big bright block appears
|
|
frames = [(0, f0), (30, f1)]
|
|
results = detect_motion_scaled(
|
|
frames, mask, threshold=30, min_area=1.0, timestamp_fn=self._ts
|
|
)
|
|
self.assertEqual(len(results), 1)
|
|
self.assertEqual(results[0].timestamp, 30.0)
|
|
self.assertGreater(results[0].change_percentage, 0.0)
|
|
|
|
def test_no_change_yields_nothing(self):
|
|
mask = np.full((60, 80), 255, dtype=np.uint8)
|
|
f0 = np.zeros((60, 80), dtype=np.uint8)
|
|
f1 = np.zeros((60, 80), dtype=np.uint8)
|
|
results = detect_motion_scaled(
|
|
[(0, f0), (30, f1)], mask, threshold=30, min_area=1.0, timestamp_fn=self._ts
|
|
)
|
|
self.assertEqual(results, [])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|