Add camera quality control and debug dense stereo

This commit is contained in:
Meta Vens 2026-05-18 20:12:00 +01:00
commit bd28491b42
8 changed files with 523 additions and 37 deletions

View file

@ -12,15 +12,19 @@ if str(ROOT) not in sys.path:
from localcast.sensor_fusion import (
CameraModel,
DenseStereoConfig,
FusionConfig,
Observation2D,
RenderBridgeConfig,
RenderFramePacket,
RenderPointPacket,
SensorRig,
dense_stereo_points,
measure_frame_quality,
put_live_render_frame,
lower_points_to_render_frame,
)
from localcast.sensor_fusion.camera_control import AdaptiveCameraController, OpenCvCameraSettingPort
def acquire_runtime_lock(path: Path) -> BinaryIO | None:
@ -214,6 +218,9 @@ class RgbSplatSampler:
height: int,
sample_step: int,
room_step: int,
dense_step: int,
enable_cpu_dense_stereo: bool,
adaptive_controls: bool,
fallback_dir: Path,
) -> None:
self.api = api
@ -223,9 +230,14 @@ class RgbSplatSampler:
self.height = height
self.sample_step = max(4, int(sample_step))
self.room_step = max(self.sample_step, int(room_step))
self.dense_step = max(1, int(dense_step))
self.enable_cpu_dense_stereo = enable_cpu_dense_stereo
self.adaptive_controls = adaptive_controls
self.fallback_dir = fallback_dir
self._captures: dict[int, object] = {}
self._frames: dict[int, np.ndarray] = {}
self._controllers: dict[int, AdaptiveCameraController] = {}
self._setting_ports: dict[int, OpenCvCameraSettingPort] = {}
def close(self) -> None:
for capture in self._captures.values():
@ -239,12 +251,16 @@ class RgbSplatSampler:
primary = self._read_frame(self.primary_index)
secondary = self._read_frame(self.secondary_index)
points: list[RenderPointPacket] = []
if self.enable_cpu_dense_stereo and primary is not None and secondary is not None:
points.extend(rgb_dense_stereo_splats(primary, secondary, timestamp_ns, step=self.dense_step))
if primary is not None:
points.extend(rgb_room_splats("room-rgb:primary", primary, np.array([-0.42, 0.05, 0.0]), timestamp_ns, step=self.room_step))
points.extend(rgb_body_splats("host-rgb", primary, np.array([-0.42, 0.05, 0.0]), now_s, timestamp_ns, side=-1.0, step=self.sample_step))
if not points:
points.extend(rgb_body_splats("host-rgb", primary, np.array([-0.42, 0.05, 0.0]), now_s, timestamp_ns, side=-1.0, step=self.sample_step))
if secondary is not None:
points.extend(rgb_room_splats("room-rgb:secondary", secondary, np.array([0.48, 0.28, 0.0]), timestamp_ns, step=self.room_step))
points.extend(rgb_body_splats("deru-rgb", secondary, np.array([0.48, 0.28, 0.0]), now_s + 0.6, timestamp_ns, side=1.0, step=self.sample_step))
if not points:
points.extend(rgb_body_splats("deru-rgb", secondary, np.array([0.48, 0.28, 0.0]), now_s + 0.6, timestamp_ns, side=1.0, step=self.sample_step))
return tuple(points)
def _read_frame(self, index: int) -> np.ndarray | None:
@ -258,6 +274,9 @@ class RgbSplatSampler:
capture.set(cv2.CAP_PROP_FRAME_HEIGHT, self.height)
capture.set(cv2.CAP_PROP_BUFFERSIZE, 1)
self._captures[index] = capture
if self.adaptive_controls:
self._controllers[index] = AdaptiveCameraController()
self._setting_ports[index] = OpenCvCameraSettingPort(capture)
else:
try:
capture.release()
@ -269,6 +288,7 @@ class RgbSplatSampler:
for _ in range(2):
ok, frame = capture.read()
if ok and frame is not None:
self._tune_capture(index, frame)
self._frames[index] = frame
return frame
cached = self._frames.get(index)
@ -276,6 +296,14 @@ class RgbSplatSampler:
return cached
return self._fallback_frame(index)
def _tune_capture(self, index: int, frame: np.ndarray) -> None:
controller = self._controllers.get(index)
port = self._setting_ports.get(index)
if controller is None or port is None:
return
command = controller.update(measure_frame_quality(frame))
port.apply(command)
def _fallback_frame(self, index: int) -> np.ndarray | None:
import cv2
@ -303,6 +331,59 @@ def cv2_api(name: str) -> int:
}[name]
def rgb_dense_stereo_splats(
primary_bgr: np.ndarray,
secondary_bgr: np.ndarray,
timestamp_ns: int,
*,
step: int,
) -> tuple[RenderPointPacket, ...]:
height, width = primary_bgr.shape[:2]
secondary_height, secondary_width = secondary_bgr.shape[:2]
width = min(width, secondary_width)
height = min(height, secondary_height)
left = rgb_dense_camera("kiyo-primary", -0.18, width, height)
right = rgb_dense_camera("kiyo-secondary", 0.18, width, height)
return dense_stereo_points(
prefix="dense-rgb",
left_frame_bgr=primary_bgr[:height, :width],
right_frame_bgr=secondary_bgr[:height, :width],
left_camera=left,
right_camera=right,
timestamp_ns=timestamp_ns,
config=DenseStereoConfig(sample_step=step, block_radius=3, max_disparity_px=max(24, width // 3), radius_m=0.0045),
)
def rgb_dense_camera(sensor_id: str, x: float, width: int, height: int) -> CameraModel:
focal = 0.82 * float(width)
return CameraModel(
sensor_id=sensor_id,
camera_matrix=np.array(
[
[focal, 0.0, width * 0.5],
[0.0, focal, height * 0.5],
[0.0, 0.0, 1.0],
],
dtype=np.float64,
),
dist_coeffs=np.zeros(5),
world_from_sensor=np.array(
[
[1.0, 0.0, 0.0, x],
[0.0, 1.0, 0.0, -0.72],
[0.0, 0.0, 1.0, 1.28],
[0.0, 0.0, 0.0, 1.0],
],
dtype=np.float64,
),
width=width,
height=height,
role="rgb_dense_stereo",
latency_ms=45.0,
)
def rgb_body_splats(
prefix: str,
frame_bgr: np.ndarray,
@ -619,6 +700,9 @@ def main() -> None:
parser.add_argument("--rgb-height", type=int, default=480)
parser.add_argument("--rgb-sample-step", type=int, default=10)
parser.add_argument("--rgb-room-step", type=int, default=28)
parser.add_argument("--cpu-dense-stereo", action="store_true", help="Debug-only CPU stereo matcher. Production dense fusion belongs on the GPU.")
parser.add_argument("--rgb-dense-step", type=int, default=16)
parser.add_argument("--no-adaptive-camera-controls", action="store_true")
parser.add_argument("--no-leap", action="store_true")
parser.add_argument("--leap-api", choices=["any", "dshow", "msmf"], default="msmf")
parser.add_argument("--leap-index", type=int, default=0)
@ -650,6 +734,9 @@ def main() -> None:
height=args.rgb_height,
sample_step=args.rgb_sample_step,
room_step=args.rgb_room_step,
dense_step=args.rgb_dense_step,
enable_cpu_dense_stereo=args.cpu_dense_stereo,
adaptive_controls=not args.no_adaptive_camera_controls,
fallback_dir=ROOT / "calibration" / "runs",
)
leap_sampler = None if args.no_leap else LeapPackedMotionSampler(