mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-11-13 00:53:02 +03:00
keep predicted target box positive and add regression tests
A shrinking object's predicted size could reach zero, so the relative zoom ratio divided by zero and sent a NaN zoom to the camera. The predicted size is now floored at 1 px. Tests also cover the movement time intercept and the pan/tilt timestamps that absolute zoom overwrites.
This commit is contained in:
@@ -609,7 +609,7 @@ class PtzAutoTracker(threading.Thread):
|
|||||||
predicted_size = max(
|
predicted_size = max(
|
||||||
right - left + frames * (velocity[2] - velocity[0]),
|
right - left + frames * (velocity[2] - velocity[0]),
|
||||||
bottom - top + frames * (velocity[3] - velocity[1]),
|
bottom - top + frames * (velocity[3] - velocity[1]),
|
||||||
0,
|
1,
|
||||||
)
|
)
|
||||||
|
|
||||||
# target_box is the squared size raised to the zoom factor
|
# target_box is the squared size raised to the zoom factor
|
||||||
|
|||||||
@@ -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.
|
running its motion estimator for a camera that could not autotrack.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from frigate.camera import PTZMetrics
|
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.config.camera.updater import CameraConfigUpdateEnum
|
||||||
from frigate.ptz.autotrack import PtzAutoTracker, calculate_max_target_box
|
from frigate.ptz.autotrack import PtzAutoTracker, calculate_max_target_box
|
||||||
|
|
||||||
@@ -126,6 +127,20 @@ class TestAutotrackerDisable(unittest.TestCase):
|
|||||||
|
|
||||||
|
|
||||||
class TestAutotrackerPrediction(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:
|
def test_approaching_object_keeps_its_velocity(self) -> None:
|
||||||
# the box grows, so the corners move in opposite directions
|
# the box grows, so the corners move in opposite directions
|
||||||
tracker = _make_tracker()
|
tracker = _make_tracker()
|
||||||
@@ -148,8 +163,16 @@ class TestAutotrackerPrediction(unittest.TestCase):
|
|||||||
self.assertFalse(np.any(velocity))
|
self.assertFalse(np.any(velocity))
|
||||||
|
|
||||||
def _target_box(self, velocity: list[float], predicted_time: float) -> float:
|
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 = _make_tracker()
|
||||||
tracker.tracked_object_history = {CAMERA: [{"box": (900, 400, 1000, 600)}]}
|
tracker.tracked_object_history = {CAMERA: [{"box": box}]}
|
||||||
tracker.tracked_object_metrics = {
|
tracker.tracked_object_metrics = {
|
||||||
CAMERA: {"target_box": 0.2, "velocity": np.array(velocity)}
|
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)
|
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):
|
class TestMaxTargetBox(unittest.TestCase):
|
||||||
def test_follows_zoom_factor(self) -> None:
|
def test_follows_zoom_factor(self) -> None:
|
||||||
self.assertAlmostEqual(calculate_max_target_box(0.5), 0.6**2)
|
self.assertAlmostEqual(calculate_max_target_box(0.5), 0.6**2)
|
||||||
|
|||||||
Reference in New Issue
Block a user