From 56f048794b6b8baf66e1baa290d36afc8298d629 Mon Sep 17 00:00:00 2001 From: Daniel-dev22 <47092714+Daniel-dev22@users.noreply.github.com> Date: Sat, 20 Jun 2026 17:19:28 -0400 Subject: [PATCH] perf(motion): compute contrast percentiles from a histogram, not np.percentile ImprovedMotionDetector.detect() runs on every frame of every camera. With improve_contrast enabled (the default), it called np.percentile twice per frame to find the 4th/96th percentile of the resized motion frame, each call partitioning the whole array. Compute both percentiles from a single 256-bin uint8 histogram instead. The result is bit-identical to int(np.percentile(...)) (the values are used as uint8 contrast bounds); only the array is now scanned once rather than partitioned twice. Measured in the release image on a height-100 motion frame (the default): np.percentile x2 ~225 us -> histogram ~11 us per frame (~20x). This runs per frame per camera, so it scales with camera count. Adds a test asserting the histogram percentiles equal numpy's across uniform, low-contrast, single-color and bimodal frames. Co-Authored-By: Claude Opus 4.8 (1M context) --- frigate/motion/improved_motion.py | 22 ++++++++++++++++++++-- frigate/test/test_motion_detector.py | 24 +++++++++++++++++++++++- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/frigate/motion/improved_motion.py b/frigate/motion/improved_motion.py index 6694dafff5..ed807e3a3e 100644 --- a/frigate/motion/improved_motion.py +++ b/frigate/motion/improved_motion.py @@ -13,6 +13,25 @@ from frigate.util.image import grab_cv2_contours logger = logging.getLogger(__name__) +def percentiles_from_uint8( + frame: np.ndarray, quantiles: tuple[float, ...] +) -> tuple[np.uint8, ...]: + """Linear-interpolated percentiles of a uint8 image, matching + numpy.percentile, but computed from a 256-bin histogram so the frame is + scanned once instead of being partitioned once per quantile.""" + csum = np.cumsum(cv2.calcHist([frame], [0], None, [256], [0, 256]).ravel()) + n = csum[-1] + results = [] + for q in quantiles: + rank = q / 100.0 * (n - 1) + lo = int(np.floor(rank)) + hi = int(np.ceil(rank)) + v_lo = float(np.searchsorted(csum, lo, side="right")) + v_hi = float(np.searchsorted(csum, hi, side="right")) + results.append(np.uint8(v_lo + (rank - lo) * (v_hi - v_lo))) + return tuple(results) + + class ImprovedMotionDetector(MotionDetector): def __init__( self, @@ -87,8 +106,7 @@ class ImprovedMotionDetector(MotionDetector): # Improve contrast if self.config.improve_contrast: # TODO tracking moving average of min/max to avoid sudden contrast changes - min_value = np.percentile(resized_frame, 4).astype(np.uint8) - max_value = np.percentile(resized_frame, 96).astype(np.uint8) + min_value, max_value = percentiles_from_uint8(resized_frame, (4, 96)) # skip contrast calcs if the image is a single color if min_value < max_value: # keep track of the last 50 contrast values diff --git a/frigate/test/test_motion_detector.py b/frigate/test/test_motion_detector.py index cdf4210a51..7b52a091d9 100644 --- a/frigate/test/test_motion_detector.py +++ b/frigate/test/test_motion_detector.py @@ -3,7 +3,29 @@ import unittest import numpy as np from frigate.config.camera.motion import MotionConfig -from frigate.motion.improved_motion import ImprovedMotionDetector +from frigate.motion.improved_motion import ( + ImprovedMotionDetector, + percentiles_from_uint8, +) + + +class TestPercentilesFromUint8(unittest.TestCase): + def test_matches_numpy_percentile(self) -> None: + rng = np.random.default_rng(0) + for trial in range(2000): + shape = (int(rng.integers(60, 200)), int(rng.integers(60, 260))) + kind = trial % 4 + if kind == 0: + frame = rng.integers(0, 256, size=shape, dtype=np.uint8) + elif kind == 1: # low contrast + frame = rng.integers(100, 140, size=shape, dtype=np.uint8) + elif kind == 2: # single color + frame = np.full(shape, 7, dtype=np.uint8) + else: # bimodal + frame = (rng.random(shape) * 40 + rng.choice([0, 200])).astype(np.uint8) + lo, hi = percentiles_from_uint8(frame, (4, 96)) + self.assertEqual(int(lo), int(np.percentile(frame, 4))) + self.assertEqual(int(hi), int(np.percentile(frame, 96))) class TestImprovedMotionDetector(unittest.TestCase):