diff --git a/frigate/test/test_obects.py b/frigate/test/test_obects.py index 8fe831980e..acafec81a7 100644 --- a/frigate/test/test_obects.py +++ b/frigate/test/test_obects.py @@ -1,6 +1,35 @@ +import random import unittest +import numpy as np + from frigate.track.tracked_object import TrackedObjectAttribute +from frigate.util.object import average_boxes, interpolated_percentile + + +class TestBoxStatistics(unittest.TestCase): + def test_average_boxes_matches_numpy(self) -> None: + rng = random.Random(0) + for _ in range(5000): + boxes = [ + [rng.randint(0, 4000) for _ in range(4)] + for _ in range(rng.randint(1, 10)) + ] + expected = [float(np.mean([b[i] for b in boxes])) for i in range(4)] + self.assertEqual(average_boxes(boxes), expected) + + def test_interpolated_percentile_matches_numpy(self) -> None: + rng = random.Random(1) + for _ in range(5000): + values = [rng.randint(0, 5000) for _ in range(rng.randint(1, 10))] + for q in (15, 85, 50): + self.assertEqual( + interpolated_percentile(values, q), + float(np.percentile(values, q)), + ) + + def test_interpolated_percentile_single_value(self) -> None: + self.assertEqual(interpolated_percentile([42], 15), 42.0) class TestAttribute(unittest.TestCase): diff --git a/frigate/track/norfair_tracker.py b/frigate/track/norfair_tracker.py index 3f11823572..ef419e81a2 100644 --- a/frigate/track/norfair_tracker.py +++ b/frigate/track/norfair_tracker.py @@ -27,7 +27,11 @@ from frigate.util.image import ( get_histogram, intersection_over_union, ) -from frigate.util.object import average_boxes, median_of_boxes +from frigate.util.object import ( + average_boxes, + interpolated_percentile, + median_of_boxes, +) logger = logging.getLogger(__name__) @@ -428,10 +432,10 @@ class NorfairTracker(ObjectTracker): position["xmaxs"].append(xmax) position["ymaxs"].append(ymax) # by using percentiles here, we hopefully remove outliers - position["xmin"] = np.percentile(position["xmins"], 15) - position["ymin"] = np.percentile(position["ymins"], 15) - position["xmax"] = np.percentile(position["xmaxs"], 85) - position["ymax"] = np.percentile(position["ymaxs"], 85) + position["xmin"] = interpolated_percentile(position["xmins"], 15) + position["ymin"] = interpolated_percentile(position["ymins"], 15) + position["xmax"] = interpolated_percentile(position["xmaxs"], 85) + position["ymax"] = interpolated_percentile(position["ymaxs"], 85) return True diff --git a/frigate/util/object.py b/frigate/util/object.py index 7c7edc10c5..ec280a7aa6 100644 --- a/frigate/util/object.py +++ b/frigate/util/object.py @@ -339,18 +339,13 @@ def reduce_boxes(boxes, iou_threshold=0.0): def average_boxes(boxes: list[list[int, int, int, int]]) -> list[int, int, int, int]: """Return a box that is the average of a list of boxes.""" - x_mins = [] - y_mins = [] - x_max = [] - y_max = [] - - for box in boxes: - x_mins.append(box[0]) - y_mins.append(box[1]) - x_max.append(box[2]) - y_max.append(box[3]) - - return [np.mean(x_mins), np.mean(y_mins), np.mean(x_max), np.mean(y_max)] + n = len(boxes) + return [ + sum(box[0] for box in boxes) / n, + sum(box[1] for box in boxes) / n, + sum(box[2] for box in boxes) / n, + sum(box[3] for box in boxes) / n, + ] def median_of_boxes(boxes: list[list[int, int, int, int]]) -> list[int, int, int, int]: @@ -359,6 +354,25 @@ def median_of_boxes(boxes: list[list[int, int, int, int]]) -> list[int, int, int return sorted_boxes[int(len(sorted_boxes) / 2.0)] +def interpolated_percentile(values: list[int], q: float) -> float: + """Linear-interpolated percentile, matching numpy.percentile for the small + lists used in position smoothing without the per-call numpy overhead.""" + ordered = sorted(values) + n = len(ordered) + if n == 1: + return float(ordered[0]) + rank = (q / 100.0) * (n - 1) + lo = int(rank) + frac = rank - lo + if frac == 0.0: + return float(ordered[lo]) + diff = ordered[lo + 1] - ordered[lo] + # numpy's lerp switches sides at frac >= 0.5 to limit rounding error + if frac < 0.5: + return ordered[lo] + diff * frac + return ordered[lo + 1] - diff * (1.0 - frac) + + def intersects_any(box_a, boxes): for box in boxes: if box_overlaps(box_a, box):