-
Notifications
You must be signed in to change notification settings - Fork 614
AprilTag marker 3D detector #2107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
51 commits
Select commit
Hold shift + click to select a range
c7b15b5
first pass on apriltag generator
leshy 9c92334
good spacing
leshy 7b3f657
Merge branch 'main' into apriltag-generator
leshy 43047b9
fixes, tests
leshy a684f01
Merge branch 'apriltag-generator' of github.com:dimensionalOS/dimos i…
leshy 40087f5
real reportlab type stubs
leshy a1edd38
Merge branch 'main' into apriltag-generator
leshy 795883f
impl_init: AprilTag marker 3D detector #2036
bogwi 6ccca17
add aruco families and letter page size to apriltag generator
leshy 671d475
Merge remote-tracking branch 'origin/main' into apriltag-generator
leshy 8deedbe
add: new integ test; small improv
bogwi dc1ed54
Merge feat/2036-apriltag-marker into feat/2036-with-2037 (#2037 integ…
bogwi 0f0f99e
Pin family and size to `DICT_APRILTAG_36h11` in `MarkerTfModuleConfig`
bogwi 9d38d4f
Fail loud at import time when contrib is missing
bogwi 8fd2138
CI: minimal aruco smoke test
bogwi e313790
Create the package skeleton to ship `dimos cameracalibrate` cli
bogwi fd83e02
Pure function: serialize calibration to ROS YAML
bogwi c93aa07
Test: YAML round-trip
bogwi c796f38
Pure function: corner detection on a single frame
bogwi 0d7f2fc
Pure function: calibrate from a list of frames
bogwi 3779de1
Test: synthetic calibration
bogwi 3b52b6d
Frame source: image folder
bogwi 4216540
Frame source: live webcam capture (interactive)
bogwi 3921024
Typer command + `dimos cameracalibrate` registration
bogwi e27e032
Save preview overlay PNG next to YAML
bogwi 10df129
Lint and CI guard
bogwi a3c6c72
Add Camera calibration runbook
bogwi a158641
Add test: calib YAML works DimosCameraInfo.from_yaml(str(out))
bogwi 51e4560
Write "Capture practice" section
bogwi e4b27b3
Write "Run `dimos cameracalibrate`" section
bogwi b8d81ac
Update cameracalibrate on e2e test; add debug logger
bogwi b96788d
Write "Verify the YAML" section && Cross-link runbook from code
bogwi a58be3a
Static TF publisher helper
bogwi 4ba5f11
Wire `Webcam` -> `CameraModule`; fixed bug in topic.py::def on_msg(ch…
bogwi a656949
Wire `MarkerTfModule` into desk blueprint
bogwi 8d46b52
rename assets to fixtures
bogwi e3e71a4
add fixtures for apriltag detection verification
bogwi d09783e
add new testing
bogwi 3a4edcd
cleanup
bogwi 86ae3a4
Merge upstream/main into feat/2036-with-2037
bogwi 1f5efa7
Move manual frame camera stub under fiducial/testing for worker pickling
bogwi f48184f
fix: ci failures
bogwi 62c737e
[autofix.ci] apply automated fixes
autofix-ci[bot] 5633f70
fix: test_marker_tf_deploy_lcm_tf_integration
bogwi 0bb720c
fix: paul's cmt
bogwi 3efdd41
add limit in camera read loop
bogwi 9ef0649
Ignore layout.tags for false positive; ruff
bogwi e299d1c
add `except (ValueError, RuntimeError)` to collect `RuntimeError` re…
bogwi 4570f82
optimize: boost cameracalibrate.py performance; various improvs
bogwi 168f99d
Merge upstream/main into feat/2036-with-2037
bogwi 26d7fc3
Merge branch 'main' into feat/2036-with-2037
leshy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| # Copyright 2026 Dimensional Inc. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from pathlib import Path | ||
| import threading | ||
| import time | ||
|
|
||
| from dimos.core.coordination.blueprints import autoconnect | ||
| from dimos.core.core import rpc | ||
| from dimos.core.module import Module, ModuleConfig | ||
| from dimos.hardware.sensors.camera.module import CameraModule | ||
| from dimos.hardware.sensors.camera.webcam import Webcam | ||
| from dimos.msgs.geometry_msgs.Quaternion import Quaternion | ||
| from dimos.msgs.geometry_msgs.Transform import Transform | ||
| from dimos.msgs.geometry_msgs.Vector3 import Vector3 | ||
| from dimos.msgs.sensor_msgs.CameraInfo import CameraInfo | ||
| from dimos.perception.fiducial.marker_tf_module import MarkerTfModule | ||
|
|
||
| DESK_CAMERA_FRAME_ID = "camera_optical" | ||
| DESK_MARKER_NAMESPACE_PREFIX = "marker_tf" | ||
| DESK_MARKER_ARUCO_DICTIONARY = "DICT_APRILTAG_36h11" | ||
| DESK_MARKER_LENGTH_M = 0.05 | ||
| DEFAULT_DESK_CAMERA_INFO_YAML = Path(__file__).resolve().parent / "fixtures" / "camera_info.yaml" | ||
|
|
||
|
|
||
| def create_desk_webcam( | ||
| camera_info_yaml: str | Path = DEFAULT_DESK_CAMERA_INFO_YAML, | ||
| camera_index: int = 0, | ||
| fps: float = 15.0, | ||
| ) -> Webcam: | ||
| camera_info = CameraInfo.from_yaml(str(camera_info_yaml)) | ||
| camera_info.frame_id = DESK_CAMERA_FRAME_ID | ||
| return Webcam( | ||
| camera_index=camera_index, | ||
| width=camera_info.width, | ||
| height=camera_info.height, | ||
| fps=fps, | ||
| camera_info=camera_info, | ||
| ) | ||
|
|
||
|
|
||
| class DeskStaticTfModuleConfig(ModuleConfig): | ||
| world_frame: str = "world" | ||
| base_frame: str = "base_link" | ||
|
leshy marked this conversation as resolved.
|
||
| camera_optical_frame: str = "camera_optical" | ||
| camera_translation_m: tuple[float, float, float] = ( | ||
| 0.25, | ||
| 0.0, | ||
| 0.15, | ||
| ) | ||
| camera_rotation_rpy_rad: tuple[float, float, float] = (0.0, 0.0, 0.0) | ||
| #: Republish fixed transforms at this rate so TF lookups at image timestamps stay | ||
| #: within MarkerTfModule tf_lookup_tolerance (single-shot stamps fall out of tolerance). | ||
| static_tf_republish_hz: float = 10.0 | ||
|
|
||
|
|
||
| class DeskStaticTfModule(Module): | ||
| """Publish the fixed desk TF chain needed by marker pose estimation.""" | ||
|
|
||
| config: DeskStaticTfModuleConfig | ||
| _last_publish_ts: float | None = None | ||
| _republish_stop: threading.Event | None = None | ||
| _republish_thread: threading.Thread | None = None | ||
|
|
||
| @rpc | ||
| def start(self) -> None: | ||
| super().start() | ||
| self.publish_static_chain() | ||
| hz = self.config.static_tf_republish_hz | ||
| if hz > 0.0: | ||
| self._republish_stop = threading.Event() | ||
| period = 1.0 / hz | ||
|
|
||
| def _republish_loop() -> None: | ||
| assert self._republish_stop is not None | ||
| while not self._republish_stop.wait(period): | ||
| self.publish_static_chain() | ||
|
|
||
| self._republish_thread = threading.Thread( | ||
| target=_republish_loop, | ||
| name="desk_static_tf_republish", | ||
| daemon=True, | ||
| ) | ||
| self._republish_thread.start() | ||
|
|
||
| @rpc | ||
| def stop(self) -> None: | ||
| if self._republish_stop is not None: | ||
| self._republish_stop.set() | ||
| if self._republish_thread is not None: | ||
| self._republish_thread.join(timeout=2.0) | ||
| self._republish_thread = None | ||
| self._republish_stop = None | ||
| super().stop() | ||
|
|
||
| def publish_static_chain(self) -> None: | ||
| ts = time.time() | ||
| self._last_publish_ts = ts | ||
| roll, pitch, yaw = self.config.camera_rotation_rpy_rad | ||
| x, y, z = self.config.camera_translation_m | ||
|
|
||
| self.tf.publish( | ||
| Transform( | ||
| translation=Vector3(0.0, 0.0, 0.0), | ||
| rotation=Quaternion(0.0, 0.0, 0.0, 1.0), | ||
| frame_id=self.config.world_frame, | ||
| child_frame_id=self.config.base_frame, | ||
| ts=ts, | ||
| ), | ||
| Transform( | ||
| # Default desk camera pose: about 25 cm forward and 15 cm above base_link. | ||
| translation=Vector3(x, y, z), | ||
| rotation=Quaternion.from_euler(Vector3(roll, pitch, yaw)), | ||
| frame_id=self.config.base_frame, | ||
| child_frame_id=self.config.camera_optical_frame, | ||
| ts=ts, | ||
| ), | ||
| ) | ||
|
|
||
|
|
||
| desk_marker_tf = autoconnect( | ||
| DeskStaticTfModule.blueprint(), | ||
| CameraModule.blueprint( | ||
| hardware=create_desk_webcam, | ||
| transform=None, | ||
| ), | ||
| MarkerTfModule.blueprint( | ||
| marker_length_m=DESK_MARKER_LENGTH_M, | ||
| aruco_dictionary=DESK_MARKER_ARUCO_DICTIONARY, | ||
| marker_namespace_prefix=DESK_MARKER_NAMESPACE_PREFIX, | ||
| ), | ||
| ) | ||
55 changes: 55 additions & 0 deletions
55
dimos/perception/fiducial/blueprints/fixtures/camera_info.yaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| image_width: 1920 | ||
| image_height: 1080 | ||
| camera_name: macbook_pro_14_2025_center_stage | ||
| distortion_model: plumb_bob | ||
| camera_matrix: | ||
| rows: 3 | ||
| cols: 3 | ||
| data: | ||
| - 2236.6990940785167 | ||
| - 0.0 | ||
| - 989.9782243556235 | ||
| - 0.0 | ||
| - 2378.17472170537 | ||
| - 568.3408769482405 | ||
| - 0.0 | ||
| - 0.0 | ||
| - 1.0 | ||
| distortion_coefficients: | ||
| rows: 1 | ||
| cols: 5 | ||
| data: | ||
| - 1.749205286914066 | ||
| - -24.50117288997724 | ||
| - -0.03351506321703973 | ||
| - -0.10786678290448087 | ||
| - 212.1609935197354 | ||
| rectification_matrix: | ||
| rows: 3 | ||
| cols: 3 | ||
| data: | ||
| - 1.0 | ||
| - 0.0 | ||
| - 0.0 | ||
| - 0.0 | ||
| - 1.0 | ||
| - 0.0 | ||
| - 0.0 | ||
| - 0.0 | ||
| - 1.0 | ||
| projection_matrix: | ||
| rows: 3 | ||
| cols: 4 | ||
| data: | ||
| - 2236.6990940785167 | ||
| - 0.0 | ||
| - 989.9782243556235 | ||
| - 0.0 | ||
| - 0.0 | ||
| - 2378.17472170537 | ||
| - 568.3408769482405 | ||
| - 0.0 | ||
| - 0.0 | ||
| - 0.0 | ||
| - 1.0 | ||
| - 0.0 |
101 changes: 101 additions & 0 deletions
101
dimos/perception/fiducial/blueprints/test_desk_marker_tf.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| # Copyright 2026 Dimensional Inc. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| from pathlib import Path | ||
|
|
||
| from dimos.core.coordination.blueprints import Blueprint | ||
| from dimos.hardware.sensors.camera.module import CameraModule | ||
| from dimos.hardware.sensors.camera.webcam import Webcam | ||
| from dimos.perception.fiducial.blueprints.desk_marker_tf import ( | ||
| DESK_CAMERA_FRAME_ID, | ||
| DESK_MARKER_ARUCO_DICTIONARY, | ||
| DESK_MARKER_LENGTH_M, | ||
| DESK_MARKER_NAMESPACE_PREFIX, | ||
| DeskStaticTfModule, | ||
| create_desk_webcam, | ||
| desk_marker_tf, | ||
| ) | ||
| from dimos.perception.fiducial.marker_tf_module import MarkerTfModule | ||
|
|
||
|
|
||
| def test_desk_marker_tf_blueprint_declares_static_tf_module() -> None: | ||
| assert isinstance(desk_marker_tf, Blueprint) | ||
| assert desk_marker_tf.blueprints[0].module is DeskStaticTfModule | ||
| assert desk_marker_tf.blueprints[1].module is CameraModule | ||
| assert desk_marker_tf.blueprints[1].kwargs["hardware"] is create_desk_webcam | ||
| assert desk_marker_tf.blueprints[1].kwargs["transform"] is None | ||
| assert desk_marker_tf.blueprints[2].module is MarkerTfModule | ||
| assert desk_marker_tf.blueprints[2].kwargs["marker_length_m"] == DESK_MARKER_LENGTH_M | ||
| assert desk_marker_tf.blueprints[2].kwargs["aruco_dictionary"] == DESK_MARKER_ARUCO_DICTIONARY | ||
| assert ( | ||
| desk_marker_tf.blueprints[2].kwargs["marker_namespace_prefix"] | ||
| == DESK_MARKER_NAMESPACE_PREFIX | ||
| ) | ||
|
|
||
|
|
||
| def test_create_desk_webcam_loads_camera_info_yaml(tmp_path: Path) -> None: | ||
| camera_info_yaml = tmp_path / "camera_info.yaml" | ||
| camera_info_yaml.write_text( | ||
| """ | ||
| image_width: 1920 | ||
| image_height: 1080 | ||
| camera_name: macbook_pro_14_2025_center_stage | ||
| distortion_model: plumb_bob | ||
| camera_matrix: | ||
| rows: 3 | ||
| cols: 3 | ||
| data: [2236.0, 0.0, 990.0, 0.0, 2378.0, 568.0, 0.0, 0.0, 1.0] | ||
| distortion_coefficients: | ||
| rows: 1 | ||
| cols: 5 | ||
| data: [1.7, -24.5, -0.03, -0.1, 212.1] | ||
| rectification_matrix: | ||
| rows: 3 | ||
| cols: 3 | ||
| data: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0] | ||
| projection_matrix: | ||
| rows: 3 | ||
| cols: 4 | ||
| data: [2236.0, 0.0, 990.0, 0.0, 0.0, 2378.0, 568.0, 0.0, 0.0, 0.0, 1.0, 0.0] | ||
| """.lstrip() | ||
| ) | ||
|
|
||
| camera = create_desk_webcam(camera_info_yaml, camera_index=1, fps=7.5) | ||
|
|
||
| assert isinstance(camera, Webcam) | ||
| assert camera.config.camera_index == 1 | ||
| assert camera.config.width == 1920 | ||
| assert camera.config.height == 1080 | ||
| assert camera.config.fps == 7.5 | ||
| assert camera.config.camera_info.frame_id == DESK_CAMERA_FRAME_ID | ||
|
|
||
|
|
||
| def test_desk_static_tf_module_publishes_world_to_camera_optical_chain() -> None: | ||
| mod = DeskStaticTfModule( | ||
| camera_translation_m=(0.3, 0.0, 0.2), | ||
| camera_rotation_rpy_rad=(0.0, 0.0, 0.0), | ||
| ) | ||
| try: | ||
| mod.start() | ||
| assert mod._last_publish_ts is not None | ||
|
|
||
| world_camera = mod.tf.get("world", "camera_optical", mod._last_publish_ts, 1.0) | ||
| assert world_camera is not None | ||
| assert world_camera.frame_id == "world" | ||
| assert world_camera.child_frame_id == "camera_optical" | ||
| assert world_camera.translation.x == 0.3 | ||
| assert world_camera.translation.y == 0.0 | ||
| assert world_camera.translation.z == 0.2 | ||
| finally: | ||
| mod.stop() |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.