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
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)."""