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 <noreply@anthropic.com>
This commit is contained in:
OliviaCandClaude Opus 5.5 committed 2026-09-29 13:12:47 +02:00
1 parent 2e80447264
commit 55e0a991b4
1 file changed
+14 -4
+14 -4
View File
@@ -47,6 +47,11 @@ class CameraFrame:
timestamp: float 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 @dataclass
class CameraIntrinsics: class CameraIntrinsics:
"""Per-sensor intrinsics. `*_ir` = depth, `*_rgb` = color.""" """Per-sensor intrinsics. `*_ir` = depth, `*_rgb` = color."""
@@ -389,10 +394,11 @@ class RealSenseD435:
# Frame capture # Frame capture
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def capture(self, timeout_ms: int = 5000) -> Optional[CameraFrame]: 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. A grab already in progress when the call arrives may have started
Requests waiting at the same moment get the same frame. 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 acquisition = self._acquisition
if not self._open or acquisition is None: if not self._open or acquisition is None:
@@ -423,7 +429,11 @@ class RealSenseD435:
return None return None
with timer.phase("convert"): 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: 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).""" """numpy BGR + depth in mm from the SDK frames (YUYV decode, USB 2 resize, depth scale)."""