test: two channels on a fake 30 fps camera with a 100 ms align

fake_realsense: clocked mode (a frameset every 1/fps s, like the device),
FakeAlign, camera timestamps, and the frame number written in the pixels
(color block, depth[0, 0]) so a test knows which frame an image came from.
test_two_channels:
- /api/rgb, /api/capture, /api/depth: rgb and depth from one frame (criterion
  B of optimize-streams R03; failed before the previous commit: 1 vs 2);
- /api/color/latest serves strictly newer frames, the image is the frame its
  headers name (camera clock), 504 on timeout, each frame encoded once;
- color >= 25 new frames/s while /api/capture loops (criteria A1/A2, 3 s).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
OliviaCandClaude Opus 5.5 committed 2026-09-30 16:55:19 +02:00
1 parent 5c97d166be
commit 13826adc97
2 files changed
+203 -18

No files matched your search

+69 -18
View File
@@ -1,7 +1,12 @@
"""A fake pyrealsense2 pipeline for tests: renders one 640x480 frameset per call after a fixed latency.
"""A fake pyrealsense2 pipeline for tests (there is no RealSense on the development PC).
There is no RealSense on the development PC. The driver only uses a few
methods of the SDK objects, and these fakes provide exactly those.
Two ways to deliver frames:
- per call (default): wait_for_frames blocks `latency_s` and returns a new frameset;
- clocked (`fps=...`): the camera produces a frameset every 1/fps s whether anyone
reads it or not, and wait_for_frames returns the next one, like the real device.
Frame N carries its number in the pixels (a 16x16 color block = N % 256, depth[0, 0] = N)
so a test can tell which frame an image came from. FakeAlign stands in for rs.align.
The driver only uses a few methods of the SDK objects, and these fakes provide exactly those.
"""
import threading
@@ -9,13 +14,16 @@ import time
import numpy as np
FAKE_LATENCY_S = 0.1 # one frame every 100 ms, as the request's test asks
FAKE_LATENCY_S = 0.1 # one frame every 100 ms, as the R01 test asks
WIDTH, HEIGHT = 640, 480
MARK = 16 # side of the color block that carries the frame number
class FakeVideoFrame:
def __init__(self, data: np.ndarray):
def __init__(self, data: np.ndarray, index: int, fps: float):
self._data = data
self._index = index
self._fps = fps
def get_data(self):
# A buffer over the frame's memory, as pyrealsense2's BufData: np.asanyarray() of it is a view.
@@ -31,16 +39,28 @@ class FakeVideoFrame:
def get_width(self) -> int:
return self._data.shape[1]
def get_timestamp(self) -> float:
"""Camera time of the frame, milliseconds."""
return self._index * 1000.0 / self._fps
def get_frame_timestamp_domain(self) -> str:
return "timestamp_domain.hardware_clock"
def __bool__(self) -> bool:
return True
class FakeFrameset:
def __init__(self, index: int):
def __init__(self, index: int, fps: float = 1.0 / FAKE_LATENCY_S):
self.index = index
rng = np.random.default_rng(index)
# Textured images, so the JPEG/PNG encoders do realistic work.
self._color = FakeVideoFrame(rng.integers(0, 255, (HEIGHT, WIDTH, 3), dtype=np.uint8))
self._depth = FakeVideoFrame(rng.integers(300, 3000, (HEIGHT, WIDTH), dtype=np.uint16))
color = rng.integers(0, 255, (HEIGHT, WIDTH, 3), dtype=np.uint8)
color[:MARK, :MARK] = index % 256
depth = rng.integers(300, 3000, (HEIGHT, WIDTH), dtype=np.uint16)
depth[0, 0] = index
self._color = FakeVideoFrame(color, index, fps)
self._depth = FakeVideoFrame(depth, index, fps)
def get_color_frame(self):
return self._color
@@ -49,27 +69,45 @@ class FakeFrameset:
return self._depth
class FakePipeline:
"""wait_for_frames blocks FAKE_LATENCY_S and returns a new frameset; counts the calls."""
def frame_number_of_color(bgr: np.ndarray) -> int:
"""The frame number a fake color image carries (JPEG-tolerant: median of the block)."""
return int(np.median(bgr[2:MARK - 2, 2:MARK - 2]))
def __init__(self, latency_s: float = FAKE_LATENCY_S):
class FakePipeline:
"""wait_for_frames returns framesets numbered 1, 2, 3, ...; counts the calls."""
def __init__(self, latency_s: float = FAKE_LATENCY_S, fps: float | None = None):
self.latency_s = latency_s
self.fps = fps
self.calls = 0
self.max_concurrent = 0
self._active = 0
self._lock = threading.Lock()
self._start = time.monotonic()
self.stopped = False
self.last_frameset = None
def _next_clocked_index(self) -> int:
"""Sleep until the camera's next frame and return its number."""
period = 1.0 / self.fps
elapsed = time.monotonic() - self._start
index = int(elapsed / period) + 1
time.sleep(max(0.0, self._start + index * period - time.monotonic()))
return index
def wait_for_frames(self, timeout_ms: int = 5000):
with self._lock:
self._active += 1
self.max_concurrent = max(self.max_concurrent, self._active)
self.calls += 1
index = self.calls
call = self.calls
try:
time.sleep(self.latency_s)
self.last_frameset = FakeFrameset(index)
if self.fps:
self.last_frameset = FakeFrameset(self._next_clocked_index(), self.fps)
else:
time.sleep(self.latency_s)
self.last_frameset = FakeFrameset(call)
return self.last_frameset
finally:
with self._lock:
@@ -79,15 +117,28 @@ class FakePipeline:
self.stopped = True
def open_fake_camera(camera, pipeline: FakePipeline) -> None:
"""Put a RealSenseD435 in the 'open' state on a fake pipeline (USB 3 path, no align).
class FakeAlign:
"""rs.align stand-in: takes `delay_s` and returns the frameset (the fake depth is already on the color grid)."""
def __init__(self, delay_s: float):
self.delay_s = delay_s
self.calls = 0
def process(self, frameset):
self.calls += 1
time.sleep(self.delay_s)
return frameset
def open_fake_camera(camera, pipeline: FakePipeline, align: FakeAlign | None = None) -> None:
"""Put a RealSenseD435 in the 'open' state on a fake pipeline (USB 3 path; align only if given).
Uses the driver's own start hook when it has one, so the same helper serves
the driver before and after the acquisition thread was introduced.
every version of the driver.
"""
import pyrealsense2 as rs
camera._pipeline = pipeline
camera._align = None
camera._align = align
camera._color_format = rs.format.bgr8
camera._depth_scale = 0.001
camera._initialized = True