Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/extensions/rcs_zed.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ pip install -ve . --no-build-isolation
pip install -ve extensions/rcs_zed
```

## Calibration

The `default_zed(...)` helper uses the identity
`DummyCalibrationStrategy` unless a `calibration_strategy` mapping is supplied.
Mapping keys must match the logical camera names, and values must implement
`rcs.camera.hw.CalibrationStrategy`. Calibration is injected explicitly so the
ZED extension remains independent of other hardware-camera extensions.

## CLI

```shell
Expand Down
2 changes: 1 addition & 1 deletion extensions/rcs_zed/Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PYSRC = src/rcs_zed
TESTSRC = tests
TESTFILE = tests/test_zed_extension.py
PYTHONPATH_LOCAL = ../../python:src:../rcs_realsense/src
PYTHONPATH_LOCAL = ../../python:src

pycheckformat:
isort --check-only ${PYSRC} ${TESTSRC}
Expand Down
23 changes: 23 additions & 0 deletions extensions/rcs_zed/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,29 @@ pip install -ve . --no-build-isolation
pip install -ve extensions/rcs_zed
```

## Calibration

`default_zed(...)` is standalone and uses RCS's identity
`DummyCalibrationStrategy` by default. To use measured extrinsics, pass one
calibration strategy per logical camera:

```python
from rcs_zed.utils import default_zed

calibration = {
"wrist": my_wrist_calibration,
"scene": my_scene_calibration,
}
cameras = default_zed(
{"wrist": "12345678", "scene": "87654321"},
calibration_strategy=calibration,
)
```

Each value must implement the `rcs.camera.hw.CalibrationStrategy` protocol.
This keeps ZED installation independent of other camera extensions and lets
applications choose the calibration method that matches their robot setup.

## CLI

```shell
Expand Down
2 changes: 0 additions & 2 deletions extensions/rcs_zed/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ license = "AGPL-3.0-or-later"
dependencies = [
"rcs-core>=0.7.2",
"opencv-python~=4.10.0",
"pupil_apriltags",
"diskcache",
"typer~=0.9",
]
maintainers = [{ name = "Tobias Juelg", email = "tobias.juelg@utn.de" }]
Expand Down
28 changes: 15 additions & 13 deletions extensions/rcs_zed/src/rcs_zed/utils.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,30 @@
import typing

from rcs import common
from rcs.camera.hw import CalibrationStrategy
from rcs_realsense.calibration import FR3BaseArucoCalibration

from rcs_zed.camera import ZEDCameraSet

from rcs import common

def default_zed(
name2id: dict[str, str] | None,
calibration_strategy: dict[str, CalibrationStrategy] | None = None,
) -> ZEDCameraSet | None:
"""Create the default ZED camera set.

def default_zed(name2id: dict[str, str] | None) -> ZEDCameraSet | None:
Args:
name2id: Mapping from logical camera names to ZED serial numbers.
calibration_strategy: Optional calibration strategy for each logical
camera. When omitted, ``ZEDCameraSet`` uses
``DummyCalibrationStrategy``.
"""
if name2id is None:
return None
cameras = {
name: common.BaseCameraConfig(identifier=id, resolution_width=1280, resolution_height=720, frame_rate=30)
for name, id in name2id.items()
}
calibration_strategy = {name: typing.cast(CalibrationStrategy, FR3BaseArucoCalibration(name)) for name in name2id}
return ZEDCameraSet(cameras=cameras, calibration_strategy=calibration_strategy)


def default_zed_dummy_calibration(name2id: dict[str, str] | None) -> ZEDCameraSet | None:
if name2id is None:
return None
cameras = {
name: common.BaseCameraConfig(identifier=id, resolution_width=1280, resolution_height=720, frame_rate=30)
for name, id in name2id.items()
}
return ZEDCameraSet(cameras=cameras)
"""Create the default ZED camera set with dummy calibration."""
return default_zed(name2id)
39 changes: 37 additions & 2 deletions extensions/rcs_zed/tests/test_zed_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
sys.path.insert(0, str(REPO_ROOT / "python"))
sys.path.insert(0, str(REPO_ROOT / "extensions/rcs_zed/src"))

from rcs_zed.camera import ZEDCameraSet, ZEDDeviceInfo, ZEDFrameBundle # noqa: E402

from rcs import common # noqa: E402
from rcs.camera.hw import DummyCalibrationStrategy # noqa: E402

from rcs_zed.camera import ZEDCameraSet, ZEDDeviceInfo, ZEDFrameBundle # noqa: E402
from rcs_zed.utils import default_zed, default_zed_dummy_calibration # noqa: E402


class FakeOpenedZEDCamera:
Expand All @@ -36,6 +38,14 @@ class PatchZedState(TypedDict):
open_calls: list[tuple[str, bool, bool, bool]]


class FakeCalibrationStrategy:
def calibrate(self, samples, intrinsics, lock):
return True

def get_extrinsics(self):
return np.eye(4)


@pytest.fixture()
def patch_zed(monkeypatch) -> PatchZedState:
state: PatchZedState = {"devices": {}, "opened": {}, "open_calls": []}
Expand Down Expand Up @@ -163,3 +173,28 @@ def test_zed_include_right_adds_logical_right_camera_without_double_grab(patch_z
assert left_frame.avg_timestamp == right_frame.avg_timestamp == 12.5
assert left_frame.camera.depth is None
assert right_frame.camera.depth is None


def test_default_zed_uses_builtin_dummy_calibration():
camera_set = default_zed({"wrist": "123"})

assert camera_set is not None
assert isinstance(camera_set.calibration_strategy["wrist"], DummyCalibrationStrategy)


def test_default_zed_accepts_explicit_calibration_strategy():
calibration = FakeCalibrationStrategy()
camera_set = default_zed(
{"wrist": "123"},
calibration_strategy={"wrist": calibration},
)

assert camera_set is not None
assert camera_set.calibration_strategy == {"wrist": calibration}


def test_default_zed_dummy_calibration_remains_compatible():
camera_set = default_zed_dummy_calibration({"wrist": "123"})

assert camera_set is not None
assert isinstance(camera_set.calibration_strategy["wrist"], DummyCalibrationStrategy)