> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.stereolabs.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.stereolabs.com/_mcp/server.

# Tutorial - AprilTag Detection with ZED and Isaac™ ROS

This tutorial demonstrates how to create an Isaac™ ROS application using AprilTag detection as example. It assumes you have already set up the ZED camera and are publishing its data using the `zed-ros2-wrapper` packages as described in the [Setting up Isaac™ ROS to work with ZED Cameras in ROS 2](/docs/integrations/isaac-ros/setup-and-installation/) section.

The application will subscribe to the ZED camera's image stream, convert the ZED images to a supported format, and finally process the images to detect AprilTags, and publish the detected tags' information.

![image](/_fern-img/4fa74ef2071cdfe1792ee3353812067fb27127496be85a3822084b5d09df68de.webp)

The example leverages the [NITROS communication framework](/docs/integrations/isaac-ros/#nitros-support) for efficient data exchange between the ZED camera and the AprilTag detection module through the GPU memory without unnecessary data copies on CPU.

> **Note**
>
> It is not required to rectify the images for AprilTag detection, because the ZED Wrapper component handles any necessary image preprocessing in the GPU memory through the ZED SDK.

Please refer to the official NVIDIA® documentation for more details on the [Image Format Converter](https://nvidia-isaac-ros.github.io/v/release-3.2/repositories_and_packages/isaac_ros_image_pipeline/isaac_ros_image_proc/index.html#imageformatconverternode) and [AprilTag](https://nvidia-isaac-ros.github.io/v/release-3.2/repositories_and_packages/isaac_ros_apriltag/index.html) Isaac™ ROS nodes.

![image](/_fern-img/3a89c8e580fd59eeec2c9674d2a45b98eba7baca29d3a8737d9a046f37d55e79.webp)

## Setup the example

If you haven't done so already, set up your development environment by following our [NVIDIA® Isaac™ ROS installation guide](/docs/integrations/isaac-ros/setup-and-installation/).

Install the `ZED ROS2 Examples` packages in the workspace:

```bash
cd ${ISAAC_ROS_WS}/src && \
git clone https://github.com/stereolabs/zed-ros2-examples.git
```

If you are using Docker, start the Isaac™ ROS environment with ZED support:

```bash
cd ${ISAAC_ROS_WS}/src/isaac_ros_common && \
./scripts/run_dev.sh -i ros2_humble.zed \
-a "-v /usr/local/zed/settings:/usr/local/zed/settings \
    -v /usr/local/zed/resources:/usr/local/zed/resources"
```

Install the Isaac™ ROS AprilTag and Image Process packages in the workspace:

```bash
sudo apt-get update && \
sudo apt-get install -y ros-humble-isaac-ros-apriltag ros-humble-isaac-ros-image-proc
```

Build the AprilTag detection application:

```bash
cd ${ISAAC_ROS_WS} && \
colcon build --symlink-install --packages-above zed_isaac_ros_april_tag
```

## Run the AprilTag detection application

Before running the AprilTag detection application you must configure the example.

```bash
cd ${ISAAC_ROS_WS}/src/zed-ros2-examples/isaac_ros/zed_isaac_ros_april_tag/config
```

Modify the `zed_isaac_ros_april_tag.yaml` file to configure the AprilTag detection parameters:

```yaml
/**:
    ros__parameters:
        size: 0.155
        max_tags: 64
        tile_size: 4
        tag_family: 'tag36h11' # Tag family to detect. CUDA backend only supports tag36h11. CPU and PVA backends support tag36h11, tag16h5, tag25h9, tag36h10, tag36h11, circle21h7, circle49h12, custom48h12, standard41h12, standard52h13
        backends: 'CUDA' # Backend to perform detection with. Options include CPU, CUDA, PVA
```

* `size`: The size of the AprilTag in meters (e.g., 0.155).
* `max_tags`: The maximum number of tags to detect (e.g., 64).
* `tile_size`: The tile/window size for adaptive thresholding in pixels (e.g., 4).
* `tag_family`: The family of AprilTags to detect. CUDA backend only supports `tag36h11`. CPU and PVA backends support `tag36h11`, `tag16h5`, `tag25h9`, `tag36h10`, `circle21h7`, `circle49h12`, `custom48h12`, `standard41h12`, and `standard52h13`.
* `backends`: The backend to use for detection. Options include `CPU`, `CUDA`, `PVA`.

You can generate the AprilTags using the tool at [this page](https://chaitanyantr.github.io/apriltag.html).

Modify the `zed_params.yaml` file to configure the ZED camera parameters:

```yaml
/**:
    ros__parameters:
        general:
          grab_resolution: 'HD1080'
          grab_frame_rate: 30
          pub_resolution: 'CUSTOM'
          pub_downscale_factor: 2.0
          pub_frame_rate: 30.0
```

* `grab_resolution`: The native camera grab resolution. Options include `HD2K`, `HD1200`, `HD1080`, `HD720`, `SVGA`, `AUTO`.
* `grab_frame_rate`: The ZED SDK internal grabbing rate. Options include `60`, `30`, `15` for `HD1200`/`HD1080` and `120`, `60`, `30`, `15` for `SVGA`.
* `pub_resolution`: The resolution used for image and depth map publishing. Options include `NATIVE` and `CUSTOM`. Set `NATIVE` to use the same `general.grab_resolution`; set `CUSTOM` to apply the `general.pub_downscale_factor` downscale factor to reduce bandwidth in data transmission.
* `pub_downscale_factor`: The rescale factor used to rescale the image before publishing when `pub_resolution` is `CUSTOM`.
* `pub_frame_rate`: The data publishing frame rate.

### Visualize Results

First, make sure you compiled and built the example as described in the [Setup](#setup-the-example) section.

> **Note**
>
> If you are using Docker, make sure to start the Isaac™ ROS environment with ZED support:
>
> ```bash
> cd ${ISAAC_ROS_WS}/src/isaac_ros_common && \
> ./scripts/run_dev.sh -i ros2_humble.zed \
> -a "-v /usr/local/zed/settings:/usr/local/zed/settings \
>     -v /usr/local/zed/resources:/usr/local/zed/resources"
> ```

Start the processing by using the command:

```bash
ros2 launch zed_isaac_ros_april_tag zed_isaac_ros_april_tag.launch.py camera_model:=<camera_model>
```

where `<camera_model>` is the model of the ZED camera you are using (e.g., `zedx`, `zed2i`, etc.).

Point the camera to an AprilTag target and observe the AprilTag detection output `/tag_detections` on a separate terminal with the command:

```bash
ros2 topic echo /tag_detections
```

> **Note**
>
> The AprilTag detection does not use the ZED depth information, so you can use this tutorial with either a ZED stereo or a ZED monocular camera.

You can visualize the full ROS 2 topic graph with:

```bash
ros2 run rqt_graph rqt_graph
```

![image](/_fern-img/19a9cadff1c51d00b2397442a5b90b96fc2a822c2480f40588750cea9bd2df53.webp)

## The launch file explained

The launch file `launch/zed_isaac_ros_april_tag.launch.py` is responsible for starting all the necessary nodes and configurations for the AprilTag detection application. It sets up the parameters, including the ZED camera settings and the AprilTag detection parameters, and launches the required nodes in a ROS 2 component container.

Retrieve the configuration parameters from the relative path `config/zed_isaac_ros_april_tag.yaml` and `config/zed_params.yaml`:

**`Python`**

```python Python
    # Get the path to the camera configuration file
    camera_config_override_path = os.path.join(
        get_package_share_directory('zed_isaac_ros_april_tag'),
        'config',
        'zed_params.yaml'
    )

    # Get the path to the AprilTag configuration file
    apriltag_config_path = os.path.join(
        get_package_share_directory('zed_isaac_ros_april_tag'),
        'config',
        'zed_isaac_ros_april_tag.yaml'
    )
```

Create a ROS 2 component container:

**`Python`**

```python Python
    # ROS 2 Component Container
    container_name = 'zed_container'
    info = '* Starting Composable node container: ' + namespace_val + '/' + container_name
    actions.append(LogInfo(msg=TextSubstitution(text=info)))

    # Note: It is crucial that the 'executable' field is set to be 'component_container_mt'
    #  so that the created nodes can be started and communicated correctly within the same process.

    zed_container = ComposableNodeContainer(
        name=container_name,
        namespace=namespace_val,
        package='rclcpp_components',
        executable='component_container_mt',
        arguments=['--ros-args', '--log-level', 'info'],
        output='screen',
    )
    actions.append(zed_container)
```

Create a ZED Component and automatically load it into the `container_name` container previously created:

**`Python`**

```python Python
    # ZED Wrapper launch file
    zed_wrapper_launch = IncludeLaunchDescription(
        launch_description_source=PythonLaunchDescriptionSource([
            get_package_share_directory('zed_wrapper'),
            '/launch/zed_camera.launch.py'
        ]),
        launch_arguments={
            'camera_model': camera_model,
            'container_name': container_name,
            'namespace': namespace_val,
            'enable_ipc': 'false',
            'ros_params_override_path': camera_config_override_path
        }.items()
    )
    actions.append(zed_wrapper_launch)
```

Setup the Image Format converter resolution by retrieving the information from the camera configuration:

**`Python`**

```python Python
    # Read the resolution from the ZED parameters file
    with open(camera_config_override_path, 'r') as f:
        configuration = yaml.safe_load(f)
        print(f'Loaded configuration: {configuration}')
        resolution = configuration["/**"]["ros__parameters"]["general"]["grab_resolution"]
        pub_resolution = configuration["/**"]["ros__parameters"]["general"]["pub_resolution"]
        pub_downscale_factor = configuration["/**"]["ros__parameters"]["general"]["pub_downscale_factor"]
        if pub_resolution == 'CUSTOM':
            rescale = pub_downscale_factor
        else:
            rescale = 1.0

    if resolution == 'HD2K':
        image_width = 2208
        image_height = 1242
    elif resolution == 'HD1200':
        image_width = 1920
        image_height = 1200
    elif resolution == 'HD1080':
        image_width = 1920
        image_height = 1080
    elif resolution == 'HD720':
        image_width = 1280
        image_height = 720
    elif resolution == 'SVGA':
        image_width = 960
        image_height = 600
    elif resolution == 'VGA':
        image_width = 672
        image_height = 376
```

Create the Image Format Converter component and set the resolution parameter and the required topic name remappings:

**`Python`**

```python Python
    # Isaac ROS Node to convert from ZED BGRA8 image to BGR8 required by AprilTag
    isaac_converter_node = ComposableNode(
        package='isaac_ros_image_proc',
        plugin='nvidia::isaac_ros::image_proc::ImageFormatConverterNode',
        name='zed_image_converter',
        namespace=namespace_val,
        parameters=[
            {
                'image_width': int(image_width / rescale),
                'image_height': int(image_height / rescale),
                'encoding_desired': 'bgr8',
                'num_blocks': 40
            }
        ],
        remappings=[
            ('image_raw', 'zed/rgb/color/rect/image'),
            ('image', 'zed/rgb/color/rect/image_bgr8')
        ]
    )
```

Create the AprilTag component with the required topic name remappings:

**`Python`**

```python Python
    # AprilTag detection node
    isac_apriltag_node = ComposableNode(
        package='isaac_ros_apriltag',
        plugin='nvidia::isaac_ros::apriltag::AprilTagNode',
        name='apriltag',
        namespace=namespace_val,
        remappings=[
                ('image', 'zed/rgb/color/rect/image_bgr8'),
                ('camera_info', 'zed/rgb/color/rect/camera_info')
        ],
        parameters=[apriltag_config_path]
    )
```

Load the Image Converter and AprilTag components into the container:

**`Python`**

```python Python
    container_full_name = namespace_val + '/' + container_name
    # Load the Converter node into the container
    load_converter_node = LoadComposableNodes(
        composable_node_descriptions=[isaac_converter_node],
        target_container=container_full_name
    )
    actions.append(load_converter_node)

    # Load the AprilTag node into the container
    load_april_tag_node = LoadComposableNodes(
        composable_node_descriptions=[isac_apriltag_node],
        target_container=container_full_name
    )
    actions.append(load_april_tag_node)
```

The launch file accepts two arguments:

1. `camera_model`: The model of the camera being used (e.g., `zed2i`, `zedx`, etc.).
2. `disable_tf`: If `True` disable TF broadcasting for all the cameras in order to fuse visual odometry information externally.

These arguments can be passed when launching the file to customize the behavior of the nodes.

## Fix the TF issue (optional)

If you notice that the TF of the detected AprilTags is not correctly propagated, you must download the patched version of the `isaac_ros_apriltag` package and build it from the source.

Clone the repository into your workspace:

```bash
cd ${ISAAC_ROS_WS}/src
git clone https://github.com/NVIDIA-ISAAC-ROS/isaac_ros_apriltag.git
```

Fetch the specific [Pull Request](https://github.com/NVIDIA-ISAAC-ROS/isaac_ros_apriltag/pull/64) that contains the fix:

```bash
cd isaac_ros_apriltag
git fetch origin pull/64/head:fix_tf
git checkout fix_tf
```

Build the package:

```bash
cd ${ISAAC_ROS_WS}
colcon build --symlink-install --packages-up-to isaac_ros_apriltag
```

Now you can run the launch file again, and the TF of the detected AprilTags should be correctly propagated.