Skip to content

ArUco Detector Node (aruco_detector)

Overview

aruco_detector detects OpenCV ArUco markers in a camera stream and estimates each marker’s 6-DoF pose using known camera intrinsics + marker size. It can:

  • Subscribe to raw or compressed images
  • Optionally subscribe to CameraInfo for live calibration
  • Filter weak detections using a minimum bounding-box area threshold
  • Publish annotated debug images
  • Publish detections on a topic (ObjectDetections) (optional)
  • Broadcast TF frames for each detected marker (aruco_marker_<id>) (optional)
  • Provide detections via a service, with an option to save an annotated image to disk

Node Name

aruco_detector

Subscribed Topics

Image input (raw)

Used when compressed_io = false:

<input_img>   (sensor_msgs/Image)

Default

/camera/camera/color/image_raw

Image input (compressed)

Used when compressed_io = true:

<input_img>/compressed   (sensor_msgs/CompressedImage)

Camera info (optional)

Used only when use_camera_info = true and compressed_io = false (as implemented):

<camera_info_topic>   (sensor_msgs/CameraInfo)

Default

/camera/camera/color/camera_info

Note: In the current code, camera_info subscription is created only in the raw-image branch (not in the compressed branch).


Published Topics

Annotated output image (raw)

Published when publish_img = true and compressed_io = false:

<output_img>   (sensor_msgs/Image)

Default

/detection/aruco/image

Annotated output image (compressed)

Published when publish_img = true and compressed_io = true:

<output_img>/compressed   (sensor_msgs/CompressedImage)

Detection output topic (optional)

Published when publish_output = true:

<output_topic>   (perseus_interfaces/msg/ObjectDetections)

Default

/detection/aruco/detections

TF Frames

If publish_tf = true, for every detected marker ID N, the node publishes:

aruco_marker_<N>

Parent frame:

tf_output_frame

Default parent frame

odom

Services

Detect Objects

/detect_objects   (perseus_interfaces/srv/DetectObjects)

Returns the latest cached detections, including:

  • ids[]: marker IDs
  • poses[]: marker poses in tf_output_frame
  • stamp: timestamp of the processed frame
  • frame_id: set to tf_output_frame

Image capture feature (new)

The request supports an image capture mode:

  • request->capture_image (bool)
  • request->img_save_path (string path)

If capture_image = true, the node will:

  1. Create the directory img_save_path (via mkdir -p)
  2. Save an annotated PNG image:
  3. Filename includes detected marker IDs: aruco_<id1>_<id2>...png
  4. If no markers: aruco_no_markers.png

  5. Overlay text on the image:

  6. Human-readable timestamp (system clock)
  7. Marker coordinate summary (X, Y, Z derived from tvec conversion)

The saved image uses the node’s cached latest_frame_ which includes drawn markers/axes.


Parameters and Defaults

All parameters are under:

aruco_detector:
  ros__parameters: ...

Marker detection / pose estimation

Parameter Type Default Description
marker_length double 0.35 Physical marker size (meters). Used for pose estimation scale.
axis_length double 0.03 Length of drawn axes in the debug image (meters).
dictionary_id int 1 OpenCV predefined dictionary enum value (must match the printed markers).
min_bounding_box_area double 100.0 Filters detections: marker’s 2D bounding box area in pixels must be ≥ this threshold to be accepted.

Bounding box filtering details

  • For each detected marker’s 4 corner points, the node computes:
  • min_x, max_x, min_y, max_y
  • area = (max_x - min_x) * (max_y - min_y)

  • If area < min_bounding_box_area, the marker is ignored (no TF, no output pose).

This helps reject:

  • tiny far-away false positives
  • noisy corner detections
  • partially detected markers

Frames / transforms

Parameter Type Default Description
camera_frame string camera_link_optical Frame ID assigned to marker poses before TF transform. Should be the camera optical frame.
tf_output_frame string odom Target frame to transform marker poses into. Also used as frame_id in outputs.

Image I/O

Parameter Type Default Description
input_img string /camera/camera/color/image_raw Raw image topic (base).
output_img string /detection/aruco/image Output annotated image topic (base).
compressed_io bool false If true: subscribe/publish to <topic>/compressed as CompressedImage.
publish_img bool true Publish annotated debug images.

Outputs

Parameter Type Default Description
publish_tf bool true Broadcast TF transforms aruco_marker_<id> in tf_output_frame.
publish_output bool false Publish ObjectDetections messages to output_topic.
output_topic string /detection/aruco/detections Detection output topic name.

Camera calibration

You support two modes:

1) Parameter-based calibration (always initialized)

These parameters are always declared and used as initial calibration.

Parameter Type Default Description
camera_matrix double[9] [530.4, 0, 320, 0, 530.4, 240, 0, 0, 1] Row-major intrinsic matrix K.
distortion_coefficients double[] [0,0,0,0,0] Distortion coeffs, typically [k1,k2,p1,p2,k3].

2) CameraInfo override (optional)

If enabled, incoming CameraInfo replaces intrinsics.

Parameter Type Default Description
use_camera_info bool false If true, subscribe to camera_info_topic and overwrite camera intrinsics dynamically.
camera_info_topic string /camera/camera/color/camera_info Topic for sensor_msgs/CameraInfo.

CameraInfo behavior

  • camera_matrix_ is built from msg->k[0..8]
  • dist_coeffs_ is built from msg->d[] (any length supported)

In processImage, if camera_matrix_ is empty, pose estimation is skipped (warn once). In the current code, camera_matrix_ is initialized from params, so it will not be empty unless changed elsewhere.


Detection Pipeline (detailed)

  1. Receive image (raw or compressed)
  2. Detect markers with:
detector_.detectMarkers(frame, corners, ids);
  1. Clone frame for annotation:
annotated_frame = frame.clone();
  1. Clear cached detections and update timestamp:
  2. latest_ids_, latest_poses_
  3. latest_timestamp_ = header.stamp

  4. If markers exist:

  5. draw marker borders
  6. for each marker:

    • estimate pose via cv::solvePnP using 3D marker corner points and detected 2D image points
    • compute bbox area in pixels
    • apply min_bounding_box_area filter
    • draw axes
    • convert pose + transform to output frame
    • cache pose + id
    • optionally publish TF transform
  7. Cache annotated frame:

  8. latest_frame_ = annotated_frame.clone()
  9. latest_marker_coords_ stores marker positions (in the camera-converted XYZ convention used for display)

  10. Optionally publish ObjectDetections message if enabled.


Example YAML Configuration

aruco_detector:
  ros__parameters:
    marker_length: 0.35
    axis_length: 0.03
    # 4x4: 50=0, 100=1, 250=2, 1000=3 | 5x5: 50=4, 100=5, 250=6, 1000=7 | 6x6: 50=8, 100=9, 250=10, 1000=11
    dictionary_id: 1
    min_bounding_box_area: 150.0

    camera_frame: camera_link_optical
    tf_output_frame: odom

    input_img: /camera/camera/color/image_raw
    output_img: /detection/aruco/image

    compressed_io: false
    publish_img: true

    publish_tf: true

    publish_output: true
    output_topic: /detection/aruco/detections

    use_camera_info: true
    camera_info_topic: /camera/camera/color/camera_info

Usage

Run with params file

ros2 run perseus_vision aruco_detector_node --ros-args \
  --params-file <path_to_yaml>

Service call (detections only)

ros2 service call /detect_objects perseus_interfaces/srv/DetectObjects "{}"

Service call (capture image)

ros2 service call /detect_objects perseus_interfaces/srv/DetectObjects \
"{capture_image: true, img_save_path: '/tmp/aruco_captures'}"

Notes

  • dictionary_id must match the marker dictionary used to generate/print the tags.
  • marker_length must match the real marker size in meters.
  • Filtering by min_bounding_box_area is in pixels², so thresholds depend on:
  • camera resolution
  • distance to marker
  • FOV and lens

  • In current modifications, camera_info subscription happens only in the raw-image path.