From 55e0a991b46f9df95c25c70c294466d418850df0 Mon Sep 17 00:00:00 2001 From: Olivia Casadei Date: Tue, 29 Sep 2026 13:12:47 +0200 Subject: [PATCH] fix: acquired frames own their arrays, not views on SDK buffers Since the acquisition thread, frames outlive the grab (the latest frame, the 100 ms cache, the 4 shared encodings). rgb/depth were views on the SDK frame memory (np.asanyarray of get_data), so each kept an SDK or rs.align pool frame busy; on the Jetson an exhausted pool shows as wait_for_frames timeouts. _grab_once now copies them (_owned: only arrays that are views; the YUYV decode, USB 2 resize and scaled depth already own theirs). New phase "copy" in the timings, to see its cost on PC2. capture() docstring: the frame is the next one COMPLETED after the call; a grab already in progress may have started before it. Co-Authored-By: Claude Opus 5.5 --- src/backend/services/camera.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/backend/services/camera.py b/src/backend/services/camera.py index e853be3..54a7fa3 100644 --- a/src/backend/services/camera.py +++ b/src/backend/services/camera.py @@ -47,6 +47,11 @@ class CameraFrame: timestamp: float +def _owned(array: np.ndarray) -> np.ndarray: + """`array` if it owns its memory, else a copy (a view on an SDK buffer has a `base`).""" + return array if array.base is None else array.copy() + + @dataclass class CameraIntrinsics: """Per-sensor intrinsics. `*_ir` = depth, `*_rgb` = color.""" @@ -389,10 +394,11 @@ class RealSenseD435: # Frame capture # ------------------------------------------------------------------ def capture(self, timeout_ms: int = 5000) -> Optional[CameraFrame]: - """The next frame acquired after this call (None on timeout or when closed). + """The next frame COMPLETED after this call (None on timeout or when closed). - Does not touch the pipeline: it waits for the acquisition thread. - Requests waiting at the same moment get the same frame. + A grab already in progress when the call arrives may have started + before it. Does not touch the pipeline: it waits for the acquisition + thread. Requests waiting at the same moment get the same frame. """ acquisition = self._acquisition if not self._open or acquisition is None: @@ -423,7 +429,11 @@ class RealSenseD435: return None with timer.phase("convert"): - return self._frame_from(color_frame, depth_frame) + frame = self._frame_from(color_frame, depth_frame) + # Own copies: frames now live on after the grab (latest frame, 100 ms cache, + # shared encodings), and views would keep the SDK's frame pool busy. + with timer.phase("copy"): + return CameraFrame(rgb=_owned(frame.rgb), depth=_owned(frame.depth), timestamp=frame.timestamp) def _frame_from(self, color_frame, depth_frame) -> CameraFrame: """numpy BGR + depth in mm from the SDK frames (YUYV decode, USB 2 resize, depth scale)."""