diff --git a/frigate/ptz/autotrack.py b/frigate/ptz/autotrack.py index 96bf238365..cd0edec684 100644 --- a/frigate/ptz/autotrack.py +++ b/frigate/ptz/autotrack.py @@ -609,7 +609,7 @@ class PtzAutoTracker(threading.Thread): predicted_size = max( right - left + frames * (velocity[2] - velocity[0]), bottom - top + frames * (velocity[3] - velocity[1]), - 0, + 1, ) # target_box is the squared size raised to the zoom factor diff --git a/frigate/test/test_ptz_autotrack.py b/frigate/test/test_ptz_autotrack.py index 7353d5e7c7..fa84f0a672 100644 --- a/frigate/test/test_ptz_autotrack.py +++ b/frigate/test/test_ptz_autotrack.py @@ -11,13 +11,14 @@ KeyError on the autotracker thread or silently keep the wrong state: running its motion estimator for a camera that could not autotrack. """ +import asyncio import unittest -from unittest.mock import MagicMock +from unittest.mock import AsyncMock, MagicMock import numpy as np from frigate.camera import PTZMetrics -from frigate.config import FrigateConfig +from frigate.config import FrigateConfig, ZoomingModeEnum from frigate.config.camera.updater import CameraConfigUpdateEnum from frigate.ptz.autotrack import PtzAutoTracker, calculate_max_target_box @@ -126,6 +127,20 @@ class TestAutotrackerDisable(unittest.TestCase): class TestAutotrackerPrediction(unittest.TestCase): + def test_movement_time_uses_the_fitted_intercept_unchanged(self) -> None: + tracker = _make_tracker() + tracker.intercept = {CAMERA: 0.5} + tracker.move_coefficients = {CAMERA: [0.3, 0.8]} + + self.assertAlmostEqual(tracker._predict_movement_time(CAMERA, 0.25, 0.25), 0.7) + + def test_shrinking_box_keeps_a_positive_target_box(self) -> None: + # 60 px shrinking 6 px/frame reaches zero after 2 s at 5 fps + target_box = self._target_box_for((0, 0, 60, 60), [3, 3, -3, -3], 2.0) + + self.assertGreater(target_box, 0) + self.assertTrue(np.isfinite(target_box)) + def test_approaching_object_keeps_its_velocity(self) -> None: # the box grows, so the corners move in opposite directions tracker = _make_tracker() @@ -148,8 +163,16 @@ class TestAutotrackerPrediction(unittest.TestCase): self.assertFalse(np.any(velocity)) def _target_box(self, velocity: list[float], predicted_time: float) -> float: + return self._target_box_for((900, 400, 1000, 600), velocity, predicted_time) + + def _target_box_for( + self, + box: tuple[int, int, int, int], + velocity: list[float], + predicted_time: float, + ) -> float: tracker = _make_tracker() - tracker.tracked_object_history = {CAMERA: [{"box": (900, 400, 1000, 600)}]} + tracker.tracked_object_history = {CAMERA: [{"box": box}]} tracker.tracked_object_metrics = { CAMERA: {"target_box": 0.2, "velocity": np.array(velocity)} } @@ -170,6 +193,51 @@ class TestAutotrackerPrediction(unittest.TestCase): self.assertEqual(self._target_box([-1, -2, 1, 2], 0), 0.2) +class TestAutotrackerMoveQueue(unittest.IsolatedAsyncioTestCase): + async def test_absolute_zoom_does_not_overwrite_the_pan_tilt_sample(self) -> None: + tracker = _make_tracker() + autotracking = tracker.config.cameras[CAMERA].onvif.autotracking + autotracking.zooming = ZoomingModeEnum.absolute + autotracking.calibrate_on_startup = True + metrics = tracker.ptz_metrics[CAMERA] + + tracker.stop_event = MagicMock() + tracker.stop_event.is_set.side_effect = [False, True] + tracker.move_queues = {CAMERA: asyncio.Queue()} + tracker.move_queue_locks = {CAMERA: asyncio.Lock()} + tracker.intercept = {CAMERA: 0.5} + tracker.move_metrics = {CAMERA: []} + tracker._wait_until_stopped = AsyncMock() + tracker._calculate_move_coefficients = MagicMock() + + async def move_relative(*_args) -> None: + metrics.start_time.value = 100.0 + metrics.stop_time.value = 102.0 + + async def zoom_absolute(*_args) -> None: + metrics.start_time.value = 103.0 + metrics.stop_time.value = 0 + + tracker.onvif._move_relative = AsyncMock(side_effect=move_relative) + tracker.onvif._zoom_absolute = AsyncMock(side_effect=zoom_absolute) + + tracker.move_queues[CAMERA].put_nowait((50.0, 0.5, 0.5, 0.5)) + await tracker._process_move_queue(CAMERA) + + tracker.onvif._zoom_absolute.assert_awaited_once() + self.assertEqual( + tracker.move_metrics[CAMERA], + [ + { + "pan": 0.5, + "tilt": 0.5, + "start_timestamp": 100.0, + "end_timestamp": 102.0, + } + ], + ) + + class TestMaxTargetBox(unittest.TestCase): def test_follows_zoom_factor(self) -> None: self.assertAlmostEqual(calculate_max_target_box(0.5), 0.6**2)