diff --git a/frigate/test/test_motion_detector.py b/frigate/test/test_motion_detector.py new file mode 100644 index 000000000..8dd2cdfaa --- /dev/null +++ b/frigate/test/test_motion_detector.py @@ -0,0 +1,91 @@ +import unittest + +import numpy as np + +from frigate.config.camera.motion import MotionConfig +from frigate.motion.improved_motion import ImprovedMotionDetector + + +class TestImprovedMotionDetector(unittest.TestCase): + def setUp(self): + # small frame for testing; actual frames are grayscale + self.frame_shape = (100, 100) # height, width + self.config = MotionConfig() + # motion detector assumes a rasterized_mask attribute exists on config + # when update_mask() is called; add one manually by bypassing pydantic. + object.__setattr__( + self.config, + "rasterized_mask", + np.ones((self.frame_shape[0], self.frame_shape[1]), dtype=np.uint8), + ) + + # create minimal PTZ metrics stub to satisfy detector checks + class _Stub: + def __init__(self, value=False): + self.value = value + + def is_set(self): + return bool(self.value) + + class DummyPTZ: + def __init__(self): + self.autotracker_enabled = _Stub(False) + self.motor_stopped = _Stub(False) + self.stop_time = _Stub(0) + + self.detector = ImprovedMotionDetector( + self.frame_shape, self.config, fps=30, ptz_metrics=DummyPTZ() + ) + + # establish a baseline frame (all zeros) + base_frame = np.zeros( + (self.frame_shape[0], self.frame_shape[1]), dtype=np.uint8 + ) + self.detector.detect(base_frame) + + def _half_change_frame(self) -> np.ndarray: + """Produce a frame where roughly half of the pixels are different.""" + frame = np.zeros((self.frame_shape[0], self.frame_shape[1]), dtype=np.uint8) + # flip the top half to white + frame[: self.frame_shape[0] // 2, :] = 255 + return frame + + def test_skip_motion_threshold_default(self): + """With the default (1.0) setting, motion should still be reported.""" + frame = self._half_change_frame() + boxes = self.detector.detect(frame) + self.assertTrue( + boxes, "Expected motion boxes when skip threshold is at default" + ) + + def test_skip_motion_threshold_applied(self): + """Setting a low skip threshold should prevent any boxes from being returned.""" + # change the config and update the detector reference + self.config.skip_motion_threshold = 0.4 + self.detector.config = self.config + self.detector.update_mask() + + frame = self._half_change_frame() + boxes = self.detector.detect(frame) + self.assertEqual( + boxes, + [], + "Motion boxes should be empty when scene change exceeds skip threshold", + ) + + def test_skip_motion_threshold_does_not_affect_calibration(self): + """Even when skipping, the detector should go into calibrating state.""" + self.config.skip_motion_threshold = 0.4 + self.detector.config = self.config + self.detector.update_mask() + + frame = self._half_change_frame() + _ = self.detector.detect(frame) + self.assertTrue( + self.detector.calibrating, + "Detector should be in calibrating state after skip event", + ) + + +if __name__ == "__main__": + unittest.main()