From ed08d4152bba56144764dbd1bec3f4408c76fd27 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Fri, 22 May 2026 08:48:19 -0500 Subject: [PATCH] discard tracked-object state when detect resolution changes mid-session When detect resolution changes mid-session every tracked object we hold was localized against the old pixel grid. Their boxes no longer correspond to anything in the new frame, and the `end` callback that fires when their IDs disappear from the new detect process's detections publishes those stale boxes to consumers (LPR, snapshot crop) that slice the new frame and crash on empty arrays. Drop the tracked-object state on a shape change so no stale boxes ever cross the CameraState boundary. Belt-and-suspenders: also drop any incoming batch whose boxes exceed the current detect resolution. These are in-flight queue entries from the pre-recycle detect process that beat the new detect process to the queue; processing them would re-introduce stale-resolution tracked objects we just dropped above. The per-camera detect process clamps legitimate boxes to detect.width-1 / detect.height-1, so any coord beyond that is unambiguously stale. --- frigate/camera/state.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/frigate/camera/state.py b/frigate/camera/state.py index dc44e7a88e..f0cf553e52 100644 --- a/frigate/camera/state.py +++ b/frigate/camera/state.py @@ -45,6 +45,7 @@ class CameraState: self.frame_cache: dict[float, dict[str, Any]] = {} self.zone_objects: defaultdict[str, list[Any]] = defaultdict(list) self._current_frame = np.zeros(self.camera_config.frame_shape_yuv, np.uint8) + self._last_frame_shape: tuple[int, int] = self.camera_config.frame_shape_yuv self.current_frame_lock = threading.Lock() self.current_frame_time = 0.0 self.motion_boxes: list[tuple[int, int, int, int]] = [] @@ -311,6 +312,31 @@ class CameraState: motion_boxes: list[tuple[int, int, int, int]], regions: list[tuple[int, int, int, int]], ) -> None: + # detect resolution changed — drop tracked state so old-grid + # boxes don't leak through end-callbacks + current_shape = self.camera_config.frame_shape_yuv + if current_shape != self._last_frame_shape: + logger.debug( + f"{self.name}: detect resolution changed {self._last_frame_shape} -> {current_shape}, dropping tracked state" + ) + with self.current_frame_lock: + self.tracked_objects.clear() + self.motion_boxes = [] + self.regions = [] + self._last_frame_shape = current_shape + + # drop in-flight batches from the pre-recycle detect process + # whose boxes exceed the current detect resolution + detect = self.camera_config.detect + if detect.width is not None and detect.height is not None: + for obj in current_detections.values(): + box = obj.get("box") + if box and (box[2] > detect.width or box[3] > detect.height): + logger.debug( + f"{self.name}: dropping stale-resolution detection batch (box {box} exceeds {detect.width}x{detect.height})" + ) + return + current_frame = self.frame_manager.get( frame_name, self.camera_config.frame_shape_yuv )