AirStack module (macvo, type ros_package, target robot) —
MAC-VO learned stereo visual odometry
(ICRA 2025 Best Conference Paper / Best Paper on Robot Perception) wrapped for
AirStack: the macvo_ros2 node consumes rectified stereo and publishes
odometry, a covariance-aware sparse point cloud, and a learned disparity
image that the local planner can use for obstacle avoidance in place of
classical stereo.
MAC-VO's heavy dependencies — TensorRT, torch, and ~250 MB of model
weights — live in this repo's Dockerfile.module (Docker dependency tier 2),
keeping AirStack's base robot image slim for everyone who doesn't fly MAC-VO.
/{robot}/sensors/front_stereo/{left,right}/image_rect + right/camera_info
→ macvo_node (MAC-VO network, TensorRT/torch)
→ /{robot}/perception/macvo/{odometry, point_cloud, disparity}
└─ disparity → local planner (disparity_expansion → DROAN), wired by
the consuming stack (trunk stacks/full_macvo)
| Component | Path | Role |
|---|---|---|
| ROS 2 wrapper | macvo_ros2/ (ament_python colcon package) |
macvo_node: stereo-pair sync, MAC-VO inference, odometry/point-cloud/disparity publishers |
| MAC-VO upstream | macvo_ros2/macvo_ros2/macvo/ — git submodule → MAC-VO/MAC-VO @ 8683b532 |
The actual network + pipeline code |
| Module launch file | macvo_ros2/launch/macvo.launch.xml |
The module's interface: every topic endpoint a launch arg with canonical defaults, zero remaps |
| Dep layer (tier 2) | Dockerfile.module |
TensorRT apt block, torch/torchvision/onnx/tensorrt pips, the 14 MACVO pips, /model_weights |
| Test stack | test_stack/ |
CI target + living install documentation — trunk's full_macvo wiring with the module's launch include |
Submodule note: clone with --recurse-submodules (or run
git submodule update --init after). airstack module sync imports with
vcs import --recursive, which handles it automatically. The submodule is
~116 MB.
cd ~/AirStack
airstack module add git@github.com:castacks/asm_macvo.git --version <tag-or-sha>
airstack module sync # overlay: symlinks macvo_ros2 into robot/ros_ws/src/modules/
airstack module lock --build # composes the robot image + MAC-VO dep layer (tier 2)Then bring up the test stack — see test_stack/README.md.
module.yaml keeps tier 1 empty on purpose (deps: {apt: [], pip: []})
so the layer story is unambiguous: everything image-level lives in
Dockerfile.module (tier 2), one logical RUN per concern, built by
airstack module lock --build with BASE_IMAGE = the previous link in the
per-host layer chain. Tier 1 lists could not express what MAC-VO needs anyway:
the NVIDIA cuda-keyring setup before the TensorRT apt install, the
weights bake, and the huggingface_hub/matplotlib-uninstall fix are all
imperative steps.
Tier 3 (planned): MAC-VO's multi-GB CUDA/torch layer is exactly the case
prebuilt overlays exist for, so the intended end state is a prebuilt overlay —
ghcr.io/castacks/asm_macvo-overlay:<airstack_version> built from this same
Dockerfile.module (fragment = source of truth, overlay = cache) — that
developers pull instead of build. overlay_image stays
null until the org settles the registry questions (ghcr.io vs the internal
airlab-docker.andrew.cmu.edu registry, which CI job pushes/cosigns overlay
images, and how module CI gets push credentials). Tracked in module.yaml.
What trunk provides: the base robot image pins numpy~=1.26.4 in
Dockerfile.robot's unconditional pip list. numpy is shared surface for many
trunk packages, so the pin is trunk's to own; this module requires numpy<2
and assumes the base image provides it.
Two files from the MAC-VO release page, pinned by sha256 in module.yaml
assets: (hashes computed from fresh downloads of the exact trunk URLs):
| File | Size | Purpose |
|---|---|---|
MACVO_FrontendCov.pth |
74 MB | Frontend (matching + covariance) weights |
MACVO_posenet.pkl |
181 MB | Pose network |
Dockerfile.module bakes them at /model_weights/ — the path the
macvo_ros2 configs expect — so a composed image is
self-contained. The assets: list additionally lets airstack sync cache
them host-side. No Git LFS anywhere.
The module's interface is macvo_ros2/launch/macvo.launch.xml
(ros2 launch macvo_ros2 macvo.launch.xml --show-args): stereo/camera-info
inputs default to the canonical /$ROBOT_NAME/sensors/front_stereo/...
topics, outputs default to relative names that land on
/$ROBOT_NAME/perception/macvo/{odometry,point_cloud,disparity} when the
stack includes the file under its perception namespace. There are no
remaps in the module (single-locus rule): macvo_node takes every topic
name as a parameter, including the camera_info subscription.
The canonical consumer is trunk's stacks/full_macvo reference stack,
which includes this module's launch file under its perception namespace and
feeds /$ROBOT_NAME/perception/macvo/disparity into the local layer
(local_disparity_in_topic); this repo's test_stack/ uses the same include.
AirStack's base robot image carries none of MAC-VO's payload — the torch +
CUDA wheel stack (~6–8 GB installed), TensorRT apt + python bindings
(~1–2 GB), and model weights (250+ MB) enter an image only when a user
composes them in via airstack module lock --build (or, later, the tier-3
overlay pull). Everyone else gets the slim base image.
.github/workflows/ci.yml calls trunk's reusable
module-system-tests.yml with
marks: "build_docker or build_packages or liveliness" on Isaac Sim.
The reusable workflow builds the module's Docker layer chain
(airstack module lock --build) before running tests, so
Dockerfile.module is exercised, not just schema-validated.
The module wrapper (this repo: macvo_ros2, Dockerfile.module,
test_stack/) is licensed under the BSD 3-Clause Clear License
(SPDX: BSD-3-Clause-Clear) — see LICENSE. The MAC-VO git submodule keeps
its own upstream license: MIT, Copyright (c) 2025 Carnegie Mellon University
(verified at submodule pin 8683b532).
Maintainer: Andrew Jong — ajong@andrew.cmu.edu.