Calibrate a static camera relative to a known root frame using ArUco markers with known poses.
Outputs a camera_extrinsics.xacro file containing a single fixed joint — ready to include in a robot description.
- Docker (host needs no ROS or calibration dependencies)
- A host ROS 2 graph with a camera publishing
sensor_msgs/Imageandsensor_msgs/CameraInfo - ArUco markers physically fixed in the root frame at known poses
docker build -t workcell_camera_calibrator .Copy src/workcell_camera_calibrator/config/calibration.yaml and edit it:
camera:
image_topic: /camera/image_raw
camera_info_topic: /camera/camera_info
camera_frame: camera_optical_frame
calibration:
root_frame: base_link
num_samples: 30
output_path: /output/camera_extrinsics.xacro
aruco_dictionary: DICT_4X4_50
markers:
- id: 0
size: 0.10 # marker side length in metres
pose: # known pose in root_frame; RPY in radians
x: 0.50
y: 0.00
z: 0.00
roll: 0.00
pitch: 0.00
yaw: 0.00aruco_dictionary: anycv2.aruco.DICT_*constant name (e.g.DICT_4X4_50,DICT_6X6_250)num_samples: number of detection samples to average before writing the resultoutput_path: path inside the container; mount a host directory here
docker run --rm \
--network host \
-v $(pwd)/src/workcell_camera_calibrator/config/calibration.yaml:/config/calibration.yaml:ro \
-v $(pwd)/output:/output \
workcell_camera_calibrator \
ros2 launch workcell_camera_calibrator calibrate.launch.py \
config:=/config/calibration.yamlThe node prints progress every 5 samples and exits automatically after writing the xacro file.
Use this directly after calibration to verify the result using the xacro file written by the calibration node.
docker run --rm \
--network host \
-v $(pwd)/src/workcell_camera_calibrator/config/calibration.yaml:/config/calibration.yaml:ro \
-v $(pwd)/output:/output:ro \
workcell_camera_calibrator \
ros2 launch workcell_camera_calibrator validate.launch.py \
config:=/config/calibration.yamlUse this after the calibration data has been integrated into your robot description and is being broadcast on the TF tree. No output directory mount is needed — the camera pose is read directly from TF.
docker run --rm \
--network host \
-v $(pwd)/src/workcell_camera_calibrator/config/calibration.yaml:/config/calibration.yaml:ro \
workcell_camera_calibrator \
ros2 launch workcell_camera_calibrator validate_tf.launch.py \
config:=/config/calibration.yamlThe node waits until root_frame → camera_frame (and the optical frame) are available on TF before processing frames.
Open RViz on the host with Fixed Frame set to the root_frame value from your config.
Add a MarkerArray display subscribed to /calibration/markers.
| Colour | Meaning |
|---|---|
| Green cube | Known marker pose (from config) |
| Red cube | Observed marker pose (through camera as seen via TF) |
| Yellow line | Residual between known and observed |
| Red / Green / Blue arrows | Camera X / Y / Z axes from TF |
| Host path | Container path | Mode |
|---|---|---|
src/workcell_camera_calibrator/config/calibration.yaml |
/config/calibration.yaml |
read-only |
output/ |
/output |
read-write (calibrate) / read-only (file-based validate) |
camera_extrinsics.xacro is a file you can <xacro:include> in your robot description:
<xacro:include filename="$(find your_robot_description)/camera_extrinsics.xacro"/>docker run --rm workcell_camera_calibrator \
bash -c "cd /ws && colcon test --packages-select workcell_camera_calibrator \
&& colcon test-result --verbose"