From ea47a1808248d6b55364451b833293677a24d570 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Mon, 21 Sep 2026 08:59:05 -0700 Subject: [PATCH 01/10] CI: use ubuntu arm runners instead of QEMU --- .github/workflows/android.yml | 78 ++++++++++ .github/workflows/docker-ros.yml | 227 +++++++++++++++++++++++++++++ .github/workflows/docker.yml | 243 ------------------------------- README.md | 3 +- docker/README.md | 33 +++++ 5 files changed, 340 insertions(+), 244 deletions(-) create mode 100644 .github/workflows/android.yml create mode 100644 .github/workflows/docker-ros.yml delete mode 100644 .github/workflows/docker.yml diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 00000000..e79a19ef --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,78 @@ +name: android + +# Android build environments (docker/noble/android/rtabmap_apiXX). +# amd64 only, so a single native runner and no manifest juggling. +# Note: these build FROM introlab3it/rtabmap:android-noble-deps, which is not +# produced by any workflow; it is still built and pushed by hand. + +on: + push: + branches: + - 'master' + pull_request: + branches: + - '**' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + docker: + # A manual dispatch is honored only on master, the only ref we push from. + if: ${{ github.event_name != 'workflow_dispatch' || github.ref == 'refs/heads/master' }} + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + docker_tag: [android23, android24, android26, android30] + include: + - docker_tag: android23 + docker_tags: | + introlab3it/rtabmap:android23 + introlab3it/rtabmap:tango + api_version: 23 + - docker_tag: android24 + docker_tags: | + introlab3it/rtabmap:android24 + api_version: 24 + - docker_tag: android26 + docker_tags: | + introlab3it/rtabmap:android26 + api_version: 26 + - docker_tag: android30 + docker_tags: | + introlab3it/rtabmap:android30 + api_version: 30 + + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - + name: Login to DockerHub + # Only needed when pushing; skipped on pull requests (secrets are + # unavailable for fork PRs and we don't push there anyway). + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + platforms: linux/amd64 + file: ./docker/noble/android/rtabmap_apiXX/Dockerfile + build-args: | + API_VERSION=${{ matrix.api_version }} + tags: ${{ matrix.docker_tags }} + cache-from: type=registry,ref=introlab3it/rtabmap:${{ matrix.docker_tag }} + cache-to: type=inline diff --git a/.github/workflows/docker-ros.yml b/.github/workflows/docker-ros.yml new file mode 100644 index 00000000..5c46a61f --- /dev/null +++ b/.github/workflows/docker-ros.yml @@ -0,0 +1,227 @@ +name: docker-ros + +# ROS images: focal/noetic (ROS1) and jammy/humble, noble/jazzy, noble-kilted, +# resolute (ROS2). Android images live in android.yml. +# +# Every arch is built natively: amd64 on an x86 runner, arm64 on a GitHub +# arm64 runner, so no QEMU emulation is involved. Because a single Docker Hub +# tag cannot hold two independently pushed architectures, each build pushes an +# arch-suffixed tag (e.g. :resolute-amd64 / :resolute-arm64) and a final job +# joins them into the real multi-arch tag (:resolute) with `imagetools create`. +# +# The runner image only hosts the build; it does not have to match the Ubuntu +# release inside the image, so ubuntu-26.04{,-arm} is used for all of them +# (ubuntu-22.04{,-arm} and ubuntu-24.04{,-arm} also exist, if ever needed). +# That is also why focal can sit in this matrix: a 20.04 userland runs fine on +# a newer host kernel, and ros:noetic-perception publishes a native arm64 image. + +on: + push: + branches: + - 'master' + pull_request: + branches: + - '**' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + docker_deps: + + # The ###-deps images used to be too flaky to build here at all (seg faults, + # arm64 build timeouts under QEMU -- see + # https://github.com/introlab/rtabmap/issues/1454) and had to be built by + # hand from a 20.04 box with an upgraded qemu-user-static. Building each + # arch natively removes that cause. + # Normally skipped on pull requests; built and pushed only from master + # (push or manual dispatch), since it pushes the :*-deps tags to Docker Hub. + # + # TEMPORARY (revert before merge): also run on pull requests, without + # pushing, purely to measure how long a cold native deps build takes on CI. + # The resulting images are discarded -- the docker job below still pulls the + # :*-deps manifests already on Docker Hub. + if: ${{ github.event_name == 'pull_request' || github.ref == 'refs/heads/master' }} + + runs-on: ${{ matrix.runner }} + + strategy: + fail-fast: false + matrix: + docker_dir: [focal, jammy, noble, noble-kilted, resolute] + arch: [amd64, arm64] + include: + - arch: amd64 + runner: ubuntu-26.04 + docker_platform: linux/amd64 + - arch: arm64 + runner: ubuntu-26.04-arm + docker_platform: linux/arm64 + + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - + name: Login to DockerHub + # TEMPORARY (revert before merge): condition added with the PR-timing run. + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + # TEMPORARY (revert before merge): was `push: true`. + push: ${{ github.event_name != 'pull_request' }} + platforms: ${{ matrix.docker_platform }} + file: ./docker/${{ matrix.docker_dir }}/deps/Dockerfile + tags: introlab3it/rtabmap:${{ matrix.docker_dir }}-deps-${{ matrix.arch }} + cache-from: type=registry,ref=introlab3it/rtabmap:${{ matrix.docker_dir }}-deps-${{ matrix.arch }} + cache-to: type=inline + + docker_deps_manifest: + needs: docker_deps + # Same gate as docker_deps, so both are skipped together on pull requests + # (github.ref is refs/pull//merge there): the per-arch -deps tags this + # joins are only ever pushed from master. + if: ${{ !cancelled() && !failure() && github.ref == 'refs/heads/master' }} + runs-on: ubuntu-26.04 + + strategy: + fail-fast: false + matrix: + docker_dir: [focal, jammy, noble, noble-kilted, resolute] + + steps: + - + name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Create multi-arch manifest + run: | + docker buildx imagetools create \ + -t introlab3it/rtabmap:${{ matrix.docker_dir }}-deps \ + introlab3it/rtabmap:${{ matrix.docker_dir }}-deps-amd64 \ + introlab3it/rtabmap:${{ matrix.docker_dir }}-deps-arm64 + + docker: + needs: docker_deps_manifest + # Run even when the deps jobs are skipped (they are, on pull requests): + # the runtime Dockerfiles then pull the :*-deps manifest already on Docker Hub. + # A manual dispatch is honored only on master, the only ref we push from. + # + # TEMPORARY (revert before merge): the `github.event_name == 'pull_request'` + # escape keeps these builds running even if the PR-only timing build of + # docker_deps fails, since its images are discarded anyway. On master a deps + # failure still blocks this job. + if: ${{ !cancelled() && (github.event_name == 'pull_request' || !failure()) && (github.event_name != 'workflow_dispatch' || github.ref == 'refs/heads/master') }} + runs-on: ${{ matrix.runner }} + + strategy: + fail-fast: false + matrix: + docker_dir: [focal, jammy, noble, noble-kilted, resolute] + arch: [amd64, arm64] + include: + - arch: amd64 + runner: ubuntu-26.04 + docker_platform: linux/amd64 + - arch: arm64 + runner: ubuntu-26.04-arm + docker_platform: linux/arm64 + + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - + name: Login to DockerHub + # Only needed when pushing; skipped on pull requests (secrets are + # unavailable for fork PRs and we don't push there anyway). + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + platforms: ${{ matrix.docker_platform }} + file: ./docker/${{ matrix.docker_dir }}/Dockerfile + tags: introlab3it/rtabmap:${{ matrix.docker_dir }}-${{ matrix.arch }} + cache-from: type=registry,ref=introlab3it/rtabmap:${{ matrix.docker_dir }}-${{ matrix.arch }} + cache-to: type=inline + + docker_manifest: + needs: docker + # Nothing to join on pull requests, where the per-arch tags are never pushed. + if: ${{ !cancelled() && !failure() && github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || github.ref == 'refs/heads/master') }} + runs-on: ubuntu-26.04 + + strategy: + fail-fast: false + matrix: + docker_dir: [focal, jammy, noble, noble-kilted, resolute] + include: + - docker_dir: focal + docker_tags: | + introlab3it/rtabmap:focal + introlab3it/rtabmap:20.04 + - docker_dir: jammy + docker_tags: | + introlab3it/rtabmap:jammy + introlab3it/rtabmap:22.04 + - docker_dir: noble + docker_tags: | + introlab3it/rtabmap:noble + introlab3it/rtabmap:24.04 + - docker_dir: noble-kilted + docker_tags: | + introlab3it/rtabmap:noble-kilted + # :latest tracks the newest ROS2 image (currently resolute, ROS2 lyrical + # on ubuntu 26.04) -- move it along with the next distro bump. + - docker_dir: resolute + docker_tags: | + introlab3it/rtabmap:resolute + introlab3it/rtabmap:26.04 + introlab3it/rtabmap:latest + + steps: + - + name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Create multi-arch manifest + env: + DOCKER_TAGS: ${{ matrix.docker_tags }} + run: | + tag_args=() + while read -r tag; do + if [ -n "$tag" ]; then + tag_args+=(-t "$tag") + fi + done <<< "$DOCKER_TAGS" + docker buildx imagetools create "${tag_args[@]}" \ + introlab3it/rtabmap:${{ matrix.docker_dir }}-amd64 \ + introlab3it/rtabmap:${{ matrix.docker_dir }}-arm64 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml deleted file mode 100644 index a37d1940..00000000 --- a/.github/workflows/docker.yml +++ /dev/null @@ -1,243 +0,0 @@ -name: docker - -on: - push: - branches: - - 'master' - pull_request: - branches: - - '**' - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} - -jobs: - docker_deps: - - # Disabling ###-deps step from CI because it is too flaky (seg faults, arm64 build timeout...) - # Only way I was able to build all images is to do it from a ubuntu 20.04 computer with: - # $ sudo add-apt-repository ppa:canonical-server/server-backports - # $ sudo apt-get update - # $ sudo apt-get upgrade qemu-user-static - # $ docker run --rm --privileged multiarch/qemu-user-static --reset -p yes -c yes - # More info: https://github.com/introlab/rtabmap/issues/1454 - # Skipped on pull requests; built and pushed only from master (push or - # manual dispatch), since it pushes the :*-deps tags to Docker Hub. - if: github.ref == 'refs/heads/master' - - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - docker_tag: [focal-deps, jammy-deps, noble-deps, noble-kilted-deps, resolute-deps] - include: - - docker_tag: focal-deps - docker_tags: | - introlab3it/rtabmap:focal-deps - docker_platforms: | - linux/amd64 - linux/arm64 - docker_path: 'focal/deps' - - docker_tag: jammy-deps - docker_tags: | - introlab3it/rtabmap:jammy-deps - docker_platforms: | - linux/amd64 - linux/arm64 - docker_path: 'jammy/deps' - - docker_tag: noble-deps - docker_tags: | - introlab3it/rtabmap:noble-deps - docker_platforms: | - linux/amd64 - linux/arm64 - docker_path: 'noble/deps' - - docker_tag: noble-kilted-deps - docker_tags: | - introlab3it/rtabmap:noble-kilted-deps - docker_platforms: | - linux/amd64 - linux/arm64 - docker_path: 'noble-kilted/deps' - - docker_tag: resolute-deps - docker_tags: | - introlab3it/rtabmap:resolute-deps - docker_platforms: | - linux/amd64 - linux/arm64 - docker_path: 'resolute/deps' - - steps: - - - name: Checkout - uses: actions/checkout@v2 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - with: - platforms: all - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to DockerHub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and push - uses: docker/build-push-action@v6 - with: - context: . - push: true - platforms: ${{ matrix.docker_platforms }} - file: ./docker/${{ matrix.docker_path }}/Dockerfile - tags: ${{ matrix.docker_tags }} - cache-from: type=registry,ref=introlab3it/rtabmap:${{ matrix.docker_tag }} - cache-to: type=inline - - docker: - needs: docker_deps - # Run even when docker_deps is skipped (it is, on pull requests). - # A manual dispatch is honored only on master, the only ref we push from. - if: ${{ !cancelled() && !failure() && (github.event_name != 'workflow_dispatch' || github.ref == 'refs/heads/master') }} - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - docker_tag: [bionic, focal, jammy, noble, noble-kilted, resolute, android23, android24, android26, android30] - include: - - docker_tag: bionic - docker_tags: | - introlab3it/rtabmap:bionic - introlab3it/rtabmap:18.04 - docker_args: | - NOT_USED=0 - docker_platforms: | - linux/amd64 - linux/arm64 - docker_path: 'bionic' - - docker_tag: focal - docker_tags: | - introlab3it/rtabmap:focal - introlab3it/rtabmap:20.04 - introlab3it/rtabmap:latest - docker_args: | - NOT_USED=0 - docker_platforms: | - linux/amd64 - linux/arm64 - docker_path: 'focal' - - docker_tag: jammy - docker_tags: | - introlab3it/rtabmap:jammy - introlab3it/rtabmap:22.04 - docker_args: | - NOT_USED=0 - docker_platforms: | - linux/amd64 - linux/arm64 - docker_path: 'jammy' - - docker_tag: noble - docker_tags: | - introlab3it/rtabmap:noble - introlab3it/rtabmap:24.04 - docker_args: | - NOT_USED=0 - docker_platforms: | - linux/amd64 - linux/arm64 - docker_path: 'noble' - - docker_tag: noble-kilted - docker_tags: | - introlab3it/rtabmap:noble-kilted - docker_args: | - NOT_USED=0 - docker_platforms: | - linux/amd64 - linux/arm64 - docker_path: 'noble-kilted' - - docker_tag: resolute - docker_tags: | - introlab3it/rtabmap:resolute - introlab3it/rtabmap:26.04 - docker_args: | - NOT_USED=0 - docker_platforms: | - linux/amd64 - linux/arm64 - docker_path: 'resolute' - - docker_tag: android23 - docker_tags: | - introlab3it/rtabmap:android23 - introlab3it/rtabmap:tango - docker_args: | - API_VERSION=23 - docker_platforms: | - linux/amd64 - docker_path: 'noble/android/rtabmap_apiXX' - - docker_tag: android24 - docker_tags: | - introlab3it/rtabmap:android24 - docker_args: | - API_VERSION=24 - docker_platforms: | - linux/amd64 - docker_path: 'noble/android/rtabmap_apiXX' - - docker_tag: android26 - docker_tags: | - introlab3it/rtabmap:android26 - docker_args: | - API_VERSION=26 - docker_platforms: | - linux/amd64 - docker_path: 'noble/android/rtabmap_apiXX' - - docker_tag: android30 - docker_tags: | - introlab3it/rtabmap:android30 - docker_args: | - API_VERSION=30 - docker_platforms: | - linux/amd64 - docker_path: 'noble/android/rtabmap_apiXX' - - steps: - - - name: Checkout - uses: actions/checkout@v2 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - with: - platforms: all - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to DockerHub - # Only needed when pushing; skipped on pull requests (secrets are - # unavailable for fork PRs and we don't push there anyway). - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and push - uses: docker/build-push-action@v6 - with: - context: . - push: ${{ github.event_name != 'pull_request' }} - platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || matrix.docker_platforms }} - file: ./docker/${{ matrix.docker_path }}/Dockerfile - build-args: | - ${{ matrix.docker_args }} - tags: ${{ matrix.docker_tags }} - cache-from: type=registry,ref=introlab3it/rtabmap:${{ matrix.docker_tag }} - cache-to: type=inline - diff --git a/README.md b/README.md index 75297533..a47597b8 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,8 @@ This project is supported by [IntRoLab - Intelligent / Interactive / Integrated CMake Windows Build Status
CMake MaCOS Build Status
CMake ROS Build Status
-
Docker Build Status + Docker ROS Build Status
+
Android Build Status diff --git a/docker/README.md b/docker/README.md index ccec6c94..50397525 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,3 +1,36 @@ ### Docker * Go to the [wiki](https://github.com/introlab/rtabmap/wiki/Installation#docker) for usage examples and how to build locally the images. + +#### Tags + +All images are published to [introlab3it/rtabmap](https://hub.docker.com/r/introlab3it/rtabmap) as +multi-arch manifests (`linux/amd64` and `linux/arm64`). + +| Tag | Alias | Base | ROS | +| --- | --- | --- | --- | +| `resolute` | `26.04`, `latest` | Ubuntu 26.04 | ROS2 Lyrical | +| `noble-kilted` | | Ubuntu 24.04 | ROS2 Kilted | +| `noble` | `24.04` | Ubuntu 24.04 | ROS2 Jazzy | +| `jammy` | `22.04` | Ubuntu 22.04 | ROS2 Humble | +| `focal` | `20.04` | Ubuntu 20.04 | ROS1 Noetic | + +Each image is built on top of a matching `-deps` image holding the third-party +dependencies, so that a source change only rebuilds the top layer. + +> [!IMPORTANT] +> **`latest` now points to the newest ROS2 image** (`resolute`), where it used to point +> to `focal` (ROS1 Noetic). Pulling `introlab3it/rtabmap` without a tag therefore gets you +> a different ROS version than before. Use `introlab3it/rtabmap:focal` to stay on ROS1, or +> pin an explicit tag in general rather than relying on `latest`. + +`bionic` / `18.04` (ROS1 Melodic) is no longer built; the last published image stays on +Docker Hub but will not be updated. Its Dockerfile is kept in [bionic/](bionic) for reference. + +The `-amd64` / `-arm64` tags are per-architecture build outputs that CI joins into +the manifests above (see [.github/workflows/docker-ros.yml](../.github/workflows/docker-ros.yml)); +use the plain tags instead. + +Android build environments (`android23`, `android24`, `android26`, `android30`, `tango`) are +`linux/amd64` only and are built by +[.github/workflows/android.yml](../.github/workflows/android.yml). From 8516479ca08161b437d25d0f6c0a8ebf4aa0e006 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Mon, 21 Sep 2026 19:10:47 -0700 Subject: [PATCH 02/10] removed focal deps docker image ci --- .github/workflows/docker-ros.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-ros.yml b/.github/workflows/docker-ros.yml index 5c46a61f..05b8307f 100644 --- a/.github/workflows/docker-ros.yml +++ b/.github/workflows/docker-ros.yml @@ -39,6 +39,10 @@ jobs: # Normally skipped on pull requests; built and pushed only from master # (push or manual dispatch), since it pushes the :*-deps tags to Docker Hub. # + # focal is deliberately absent: its ROS1/noetic deps are frozen, so + # :focal-deps is built by hand on the rare occasion it changes. The focal + # runtime image below still builds here, FROM the published :focal-deps. + # # TEMPORARY (revert before merge): also run on pull requests, without # pushing, purely to measure how long a cold native deps build takes on CI. # The resulting images are discarded -- the docker job below still pulls the @@ -50,7 +54,7 @@ jobs: strategy: fail-fast: false matrix: - docker_dir: [focal, jammy, noble, noble-kilted, resolute] + docker_dir: [jammy, noble, noble-kilted, resolute] arch: [amd64, arm64] include: - arch: amd64 @@ -99,7 +103,7 @@ jobs: strategy: fail-fast: false matrix: - docker_dir: [focal, jammy, noble, noble-kilted, resolute] + docker_dir: [jammy, noble, noble-kilted, resolute] steps: - From 045a1bad23aaafdbfcb64926fdbd81973c124b7a Mon Sep 17 00:00:00 2001 From: matlabbe Date: Mon, 21 Sep 2026 19:22:11 -0700 Subject: [PATCH 03/10] run tests in docker ci --- .dockerignore | 8 ++++++++ .github/workflows/docker-ros.yml | 2 ++ docker/focal/Dockerfile | 5 ++++- docker/jammy/Dockerfile | 5 ++++- docker/noble-kilted/Dockerfile | 5 ++++- docker/noble/Dockerfile | 5 ++++- docker/resolute/Dockerfile | 5 ++++- 7 files changed, 30 insertions(+), 5 deletions(-) diff --git a/.dockerignore b/.dockerignore index 1badba74..08902f6d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,10 @@ build/* build_* + +data/tests/*.db +data/tests/*.7z +data/tests/*.zip +data/tests/*.pt +data/tests/*.pth +data/tests/*.py +data/tests/__pycache__/ diff --git a/.github/workflows/docker-ros.yml b/.github/workflows/docker-ros.yml index 05b8307f..0afa2621 100644 --- a/.github/workflows/docker-ros.yml +++ b/.github/workflows/docker-ros.yml @@ -170,6 +170,8 @@ jobs: push: ${{ github.event_name != 'pull_request' }} platforms: ${{ matrix.docker_platform }} file: ./docker/${{ matrix.docker_dir }}/Dockerfile + build-args: | + RUN_TESTS=1 tags: introlab3it/rtabmap:${{ matrix.docker_dir }}-${{ matrix.arch }} cache-from: type=registry,ref=introlab3it/rtabmap:${{ matrix.docker_dir }}-${{ matrix.arch }} cache-to: type=inline diff --git a/docker/focal/Dockerfile b/docker/focal/Dockerfile index d6a3e866..06cc5490 100644 --- a/docker/focal/Dockerfile +++ b/docker/focal/Dockerfile @@ -8,11 +8,14 @@ RUN mkdir -p /root/Documents/RTAB-Map && chmod 777 /root/Documents/RTAB-Map # Copy current source code COPY . /root/rtabmap +ARG RUN_TESTS=0 + # Build RTAB-Map project RUN source /ros_entrypoint.sh && \ cd rtabmap/build && \ - cmake -DWITH_ALICE_VISION=ON -DWITH_OPENGV=ON .. && \ + cmake -DWITH_ALICE_VISION=ON -DWITH_OPENGV=ON -DBUILD_TESTING=$RUN_TESTS .. && \ make -j4 && \ + if [ "$RUN_TESTS" = "1" ]; then ctest --output-on-failure -LE "long|performance"; fi && \ make install && \ cd ../.. && \ rm -rf rtabmap && \ diff --git a/docker/jammy/Dockerfile b/docker/jammy/Dockerfile index c28543ec..731a88e1 100644 --- a/docker/jammy/Dockerfile +++ b/docker/jammy/Dockerfile @@ -8,11 +8,14 @@ RUN mkdir -p /root/Documents/RTAB-Map && chmod 777 /root/Documents/RTAB-Map # Copy current source code COPY . /root/rtabmap +ARG RUN_TESTS=0 + # Build RTAB-Map project RUN source /ros_entrypoint.sh && \ cd rtabmap/build && \ - cmake -DWITH_OPENGV=ON .. && \ + cmake -DWITH_OPENGV=ON -DBUILD_TESTING=$RUN_TESTS .. && \ make -j4 && \ + if [ "$RUN_TESTS" = "1" ]; then ctest --output-on-failure -LE "long|performance"; fi && \ make install && \ cd ../.. && \ rm -rf rtabmap && \ diff --git a/docker/noble-kilted/Dockerfile b/docker/noble-kilted/Dockerfile index 5c6a955a..2d3059d6 100644 --- a/docker/noble-kilted/Dockerfile +++ b/docker/noble-kilted/Dockerfile @@ -8,11 +8,14 @@ RUN mkdir -p /root/Documents/RTAB-Map && chmod 777 /root/Documents/RTAB-Map # Copy current source code COPY . /root/rtabmap +ARG RUN_TESTS=0 + # Build RTAB-Map project RUN source /ros_entrypoint.sh && \ cd rtabmap/build && \ - cmake -DWITH_OPENGV=ON .. && \ + cmake -DWITH_OPENGV=ON -DBUILD_TESTING=$RUN_TESTS .. && \ make -j4 && \ + if [ "$RUN_TESTS" = "1" ]; then ctest --output-on-failure -LE "long|performance"; fi && \ make install && \ cd ../.. && \ rm -rf rtabmap && \ diff --git a/docker/noble/Dockerfile b/docker/noble/Dockerfile index 88d9e042..3b119c53 100644 --- a/docker/noble/Dockerfile +++ b/docker/noble/Dockerfile @@ -8,11 +8,14 @@ RUN mkdir -p /root/Documents/RTAB-Map && chmod 777 /root/Documents/RTAB-Map # Copy current source code COPY . /root/rtabmap +ARG RUN_TESTS=0 + # Build RTAB-Map project RUN source /ros_entrypoint.sh && \ cd rtabmap/build && \ - cmake -DWITH_OPENGV=ON .. && \ + cmake -DWITH_OPENGV=ON -DBUILD_TESTING=$RUN_TESTS .. && \ make -j4 && \ + if [ "$RUN_TESTS" = "1" ]; then ctest --output-on-failure -LE "long|performance"; fi && \ make install && \ cd ../.. && \ rm -rf rtabmap && \ diff --git a/docker/resolute/Dockerfile b/docker/resolute/Dockerfile index 8e043f61..f5f9bda9 100644 --- a/docker/resolute/Dockerfile +++ b/docker/resolute/Dockerfile @@ -8,11 +8,14 @@ RUN mkdir -p /root/Documents/RTAB-Map && chmod 777 /root/Documents/RTAB-Map # Copy current source code COPY . /root/rtabmap +ARG RUN_TESTS=0 + # Build RTAB-Map project RUN source /ros_entrypoint.sh && \ cd rtabmap/build && \ - cmake -DWITH_OPENGV=ON .. && \ + cmake -DWITH_OPENGV=ON -DBUILD_TESTING=$RUN_TESTS .. && \ make -j4 && \ + if [ "$RUN_TESTS" = "1" ]; then ctest --output-on-failure -LE "long|performance"; fi && \ make install && \ cd ../.. && \ rm -rf rtabmap && \ From 5e5fa7e96198cf351be74f9f78f07db955fde962 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Mon, 21 Sep 2026 19:25:55 -0700 Subject: [PATCH 04/10] revert temporary test --- .github/workflows/docker-ros.yml | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/.github/workflows/docker-ros.yml b/.github/workflows/docker-ros.yml index 0afa2621..a31438c0 100644 --- a/.github/workflows/docker-ros.yml +++ b/.github/workflows/docker-ros.yml @@ -36,18 +36,13 @@ jobs: # https://github.com/introlab/rtabmap/issues/1454) and had to be built by # hand from a 20.04 box with an upgraded qemu-user-static. Building each # arch natively removes that cause. - # Normally skipped on pull requests; built and pushed only from master - # (push or manual dispatch), since it pushes the :*-deps tags to Docker Hub. + # Skipped on pull requests; built and pushed only from master (push or + # manual dispatch), since it pushes the :*-deps tags to Docker Hub. # # focal is deliberately absent: its ROS1/noetic deps are frozen, so # :focal-deps is built by hand on the rare occasion it changes. The focal # runtime image below still builds here, FROM the published :focal-deps. - # - # TEMPORARY (revert before merge): also run on pull requests, without - # pushing, purely to measure how long a cold native deps build takes on CI. - # The resulting images are discarded -- the docker job below still pulls the - # :*-deps manifests already on Docker Hub. - if: ${{ github.event_name == 'pull_request' || github.ref == 'refs/heads/master' }} + if: github.ref == 'refs/heads/master' runs-on: ${{ matrix.runner }} @@ -73,8 +68,6 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - # TEMPORARY (revert before merge): condition added with the PR-timing run. - if: github.event_name != 'pull_request' uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} @@ -84,8 +77,7 @@ jobs: uses: docker/build-push-action@v6 with: context: . - # TEMPORARY (revert before merge): was `push: true`. - push: ${{ github.event_name != 'pull_request' }} + push: true platforms: ${{ matrix.docker_platform }} file: ./docker/${{ matrix.docker_dir }}/deps/Dockerfile tags: introlab3it/rtabmap:${{ matrix.docker_dir }}-deps-${{ matrix.arch }} @@ -125,12 +117,7 @@ jobs: # Run even when the deps jobs are skipped (they are, on pull requests): # the runtime Dockerfiles then pull the :*-deps manifest already on Docker Hub. # A manual dispatch is honored only on master, the only ref we push from. - # - # TEMPORARY (revert before merge): the `github.event_name == 'pull_request'` - # escape keeps these builds running even if the PR-only timing build of - # docker_deps fails, since its images are discarded anyway. On master a deps - # failure still blocks this job. - if: ${{ !cancelled() && (github.event_name == 'pull_request' || !failure()) && (github.event_name != 'workflow_dispatch' || github.ref == 'refs/heads/master') }} + if: ${{ !cancelled() && !failure() && (github.event_name != 'workflow_dispatch' || github.ref == 'refs/heads/master') }} runs-on: ${{ matrix.runner }} strategy: From 9f538b432cc09dba80cf2d345884367887949af4 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Mon, 21 Sep 2026 20:00:13 -0700 Subject: [PATCH 05/10] trigger ci jobs with modified files --- .github/workflows/android.yml | 15 +++++++++++++++ .github/workflows/cmake-linux.yml | 7 +++++++ .github/workflows/cmake-macos.yml | 7 +++++++ .github/workflows/cmake-ros.yml | 7 +++++++ .github/workflows/cmake-windows.yml | 7 +++++++ .github/workflows/coverage.yml | 7 +++++++ .github/workflows/docker-ros.yml | 7 +++++++ 7 files changed, 57 insertions(+) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index e79a19ef..c0ae050f 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -9,9 +9,24 @@ on: push: branches: - 'master' + paths: &android_paths + # What an Android build actually compiles: CMakeLists.txt adds only + # utilite, corelib and app under IF(ANDROID), and rtabmap.bash builds with + # WITH_OPENGV=OFF, BUILD_EXAMPLES=OFF, BUILD_TOOLS=OFF. + - 'CMakeLists.txt' + - 'Version.h.in' + - 'RTABMapConfig.cmake.in' + - 'cmake_uninstall.cmake.in' + - 'cmake_modules/**' + - 'utilite/**' + - 'corelib/**' + - 'app/**' + - 'docker/noble/android/**' + - '.github/workflows/android.yml' pull_request: branches: - '**' + paths: *android_paths workflow_dispatch: concurrency: diff --git a/.github/workflows/cmake-linux.yml b/.github/workflows/cmake-linux.yml index 217e05e1..8e678dc7 100644 --- a/.github/workflows/cmake-linux.yml +++ b/.github/workflows/cmake-linux.yml @@ -4,9 +4,16 @@ on: push: branches: - master + paths-ignore: &platform_only + - '.github/workflows/android.yml' + - '.github/workflows/ios.yml' + - 'app/android/**' + - 'app/ios/**' + - 'docker/noble/android/**' pull_request: branches: - '**' + paths-ignore: *platform_only workflow_dispatch: env: diff --git a/.github/workflows/cmake-macos.yml b/.github/workflows/cmake-macos.yml index 1f8caac0..9bfa6050 100644 --- a/.github/workflows/cmake-macos.yml +++ b/.github/workflows/cmake-macos.yml @@ -4,9 +4,16 @@ on: push: branches: - master + paths-ignore: &platform_only + - '.github/workflows/android.yml' + - '.github/workflows/ios.yml' + - 'app/android/**' + - 'app/ios/**' + - 'docker/noble/android/**' pull_request: branches: - '**' + paths-ignore: *platform_only workflow_dispatch: env: diff --git a/.github/workflows/cmake-ros.yml b/.github/workflows/cmake-ros.yml index 6004bb61..ad0bde13 100644 --- a/.github/workflows/cmake-ros.yml +++ b/.github/workflows/cmake-ros.yml @@ -4,9 +4,16 @@ on: push: branches: - master + paths-ignore: &platform_only + - '.github/workflows/android.yml' + - '.github/workflows/ios.yml' + - 'app/android/**' + - 'app/ios/**' + - 'docker/noble/android/**' pull_request: branches: - '**' + paths-ignore: *platform_only workflow_dispatch: env: diff --git a/.github/workflows/cmake-windows.yml b/.github/workflows/cmake-windows.yml index 83cbdbb3..4c6825bd 100644 --- a/.github/workflows/cmake-windows.yml +++ b/.github/workflows/cmake-windows.yml @@ -4,9 +4,16 @@ on: push: branches: - master + paths-ignore: &platform_only + - '.github/workflows/android.yml' + - '.github/workflows/ios.yml' + - 'app/android/**' + - 'app/ios/**' + - 'docker/noble/android/**' pull_request: branches: - '**' + paths-ignore: *platform_only workflow_dispatch: env: diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 23eced80..fa8c30fc 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -4,9 +4,16 @@ on: push: branches: - master + paths-ignore: &platform_only + - '.github/workflows/android.yml' + - '.github/workflows/ios.yml' + - 'app/android/**' + - 'app/ios/**' + - 'docker/noble/android/**' pull_request: branches: - '**' + paths-ignore: *platform_only workflow_dispatch: concurrency: diff --git a/.github/workflows/docker-ros.yml b/.github/workflows/docker-ros.yml index a31438c0..84caae9f 100644 --- a/.github/workflows/docker-ros.yml +++ b/.github/workflows/docker-ros.yml @@ -19,9 +19,16 @@ on: push: branches: - 'master' + paths-ignore: &platform_only + - '.github/workflows/android.yml' + - '.github/workflows/ios.yml' + - 'app/android/**' + - 'app/ios/**' + - 'docker/noble/android/**' pull_request: branches: - '**' + paths-ignore: *platform_only workflow_dispatch: concurrency: From 96b5881e2f68fc14e556883e4b15983229a1341d Mon Sep 17 00:00:00 2001 From: matlabbe Date: Tue, 22 Sep 2026 08:24:53 -0700 Subject: [PATCH 06/10] ldconfig --- docker/focal/Dockerfile | 3 ++- docker/jammy/Dockerfile | 3 ++- docker/noble-kilted/Dockerfile | 3 ++- docker/noble/Dockerfile | 3 ++- docker/resolute/Dockerfile | 3 ++- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/docker/focal/Dockerfile b/docker/focal/Dockerfile index 06cc5490..fe18c8ad 100644 --- a/docker/focal/Dockerfile +++ b/docker/focal/Dockerfile @@ -15,7 +15,8 @@ RUN source /ros_entrypoint.sh && \ cd rtabmap/build && \ cmake -DWITH_ALICE_VISION=ON -DWITH_OPENGV=ON -DBUILD_TESTING=$RUN_TESTS .. && \ make -j4 && \ - if [ "$RUN_TESTS" = "1" ]; then ctest --output-on-failure -LE "long|performance"; fi && \ + # ldconfig first: source-installed deps (GTSAM) are not yet in the loader cache. + if [ "$RUN_TESTS" = "1" ]; then ldconfig && ctest --output-on-failure -LE "long|performance"; fi && \ make install && \ cd ../.. && \ rm -rf rtabmap && \ diff --git a/docker/jammy/Dockerfile b/docker/jammy/Dockerfile index 731a88e1..7548682a 100644 --- a/docker/jammy/Dockerfile +++ b/docker/jammy/Dockerfile @@ -15,7 +15,8 @@ RUN source /ros_entrypoint.sh && \ cd rtabmap/build && \ cmake -DWITH_OPENGV=ON -DBUILD_TESTING=$RUN_TESTS .. && \ make -j4 && \ - if [ "$RUN_TESTS" = "1" ]; then ctest --output-on-failure -LE "long|performance"; fi && \ + # ldconfig first: source-installed deps (GTSAM) are not yet in the loader cache. + if [ "$RUN_TESTS" = "1" ]; then ldconfig && ctest --output-on-failure -LE "long|performance"; fi && \ make install && \ cd ../.. && \ rm -rf rtabmap && \ diff --git a/docker/noble-kilted/Dockerfile b/docker/noble-kilted/Dockerfile index 2d3059d6..a33006da 100644 --- a/docker/noble-kilted/Dockerfile +++ b/docker/noble-kilted/Dockerfile @@ -15,7 +15,8 @@ RUN source /ros_entrypoint.sh && \ cd rtabmap/build && \ cmake -DWITH_OPENGV=ON -DBUILD_TESTING=$RUN_TESTS .. && \ make -j4 && \ - if [ "$RUN_TESTS" = "1" ]; then ctest --output-on-failure -LE "long|performance"; fi && \ + # ldconfig first: source-installed deps (GTSAM) are not yet in the loader cache. + if [ "$RUN_TESTS" = "1" ]; then ldconfig && ctest --output-on-failure -LE "long|performance"; fi && \ make install && \ cd ../.. && \ rm -rf rtabmap && \ diff --git a/docker/noble/Dockerfile b/docker/noble/Dockerfile index 3b119c53..511ecafa 100644 --- a/docker/noble/Dockerfile +++ b/docker/noble/Dockerfile @@ -15,7 +15,8 @@ RUN source /ros_entrypoint.sh && \ cd rtabmap/build && \ cmake -DWITH_OPENGV=ON -DBUILD_TESTING=$RUN_TESTS .. && \ make -j4 && \ - if [ "$RUN_TESTS" = "1" ]; then ctest --output-on-failure -LE "long|performance"; fi && \ + # ldconfig first: source-installed deps (GTSAM) are not yet in the loader cache. + if [ "$RUN_TESTS" = "1" ]; then ldconfig && ctest --output-on-failure -LE "long|performance"; fi && \ make install && \ cd ../.. && \ rm -rf rtabmap && \ diff --git a/docker/resolute/Dockerfile b/docker/resolute/Dockerfile index f5f9bda9..da0d3720 100644 --- a/docker/resolute/Dockerfile +++ b/docker/resolute/Dockerfile @@ -15,7 +15,8 @@ RUN source /ros_entrypoint.sh && \ cd rtabmap/build && \ cmake -DWITH_OPENGV=ON -DBUILD_TESTING=$RUN_TESTS .. && \ make -j4 && \ - if [ "$RUN_TESTS" = "1" ]; then ctest --output-on-failure -LE "long|performance"; fi && \ + # ldconfig first: source-installed deps (GTSAM) are not yet in the loader cache. + if [ "$RUN_TESTS" = "1" ]; then ldconfig && ctest --output-on-failure -LE "long|performance"; fi && \ make install && \ cd ../.. && \ rm -rf rtabmap && \ From aa9cc2f3d775a4937159694bff95ff46596712ab Mon Sep 17 00:00:00 2001 From: matlabbe Date: Tue, 22 Sep 2026 09:01:55 -0700 Subject: [PATCH 07/10] arm64 ldconfig order --- docker/focal-foxy/deps/Dockerfile | 4 ++++ docker/focal/deps/Dockerfile | 4 ++++ docker/jammy-iron/deps/Dockerfile | 4 ++++ docker/jammy/deps/Dockerfile | 4 ++++ docker/noble-kilted/deps/Dockerfile | 4 ++++ docker/noble/deps/Dockerfile | 4 ++++ docker/resolute/deps/Dockerfile | 4 ++++ 7 files changed, 28 insertions(+) diff --git a/docker/focal-foxy/deps/Dockerfile b/docker/focal-foxy/deps/Dockerfile index 54317f05..bc5385e3 100644 --- a/docker/focal-foxy/deps/Dockerfile +++ b/docker/focal-foxy/deps/Dockerfile @@ -165,6 +165,10 @@ RUN git clone --branch 4.2.0 https://github.com/opencv/opencv.git && \ cd ../.. && \ rm -rf opencv opencv_contrib +# ld.so.conf.d is read in filename order: -linux-gnu.conf sorts before +# libc.conf (/usr/local/lib) on arm64, so source builds lose to the distro ones. +RUN echo /usr/local/lib > /etc/ld.so.conf.d/00-usr-local.conf && ldconfig + RUN rm /bin/sh && ln -s /bin/bash /bin/sh COPY ./docker/focal-foxy/deps/ros_entrypoint.sh /ros_entrypoint.sh diff --git a/docker/focal/deps/Dockerfile b/docker/focal/deps/Dockerfile index aa3ee674..35eb7c2c 100644 --- a/docker/focal/deps/Dockerfile +++ b/docker/focal/deps/Dockerfile @@ -190,6 +190,10 @@ RUN git clone https://github.com/laurentkneip/opengv.git && \ cd && \ rm -r opengv +# ld.so.conf.d is read in filename order: -linux-gnu.conf sorts before +# libc.conf (/usr/local/lib) on arm64, so source builds lose to the distro ones. +RUN echo /usr/local/lib > /etc/ld.so.conf.d/00-usr-local.conf && ldconfig + RUN rm /bin/sh && ln -s /bin/bash /bin/sh # for jetson (https://github.com/introlab/rtabmap/issues/776) diff --git a/docker/jammy-iron/deps/Dockerfile b/docker/jammy-iron/deps/Dockerfile index 4258f9f2..686c3f8f 100644 --- a/docker/jammy-iron/deps/Dockerfile +++ b/docker/jammy-iron/deps/Dockerfile @@ -74,6 +74,10 @@ RUN git clone --branch 4.5.4 https://github.com/opencv/opencv.git && \ cd ../.. && \ rm -rf opencv opencv_contrib +# ld.so.conf.d is read in filename order: -linux-gnu.conf sorts before +# libc.conf (/usr/local/lib) on arm64, so source builds lose to the distro ones. +RUN echo /usr/local/lib > /etc/ld.so.conf.d/00-usr-local.conf && ldconfig + RUN rm /bin/sh && ln -s /bin/bash /bin/sh COPY ./docker/jammy-iron/deps/ros_entrypoint.sh /ros_entrypoint.sh diff --git a/docker/jammy/deps/Dockerfile b/docker/jammy/deps/Dockerfile index 04c280db..07ee6093 100644 --- a/docker/jammy/deps/Dockerfile +++ b/docker/jammy/deps/Dockerfile @@ -98,6 +98,10 @@ RUN git clone https://github.com/laurentkneip/opengv.git && \ cd && \ rm -r opengv +# ld.so.conf.d is read in filename order: -linux-gnu.conf sorts before +# libc.conf (/usr/local/lib) on arm64, so source builds lose to the distro ones. +RUN echo /usr/local/lib > /etc/ld.so.conf.d/00-usr-local.conf && ldconfig + RUN rm /bin/sh && ln -s /bin/bash /bin/sh RUN echo -e '#!/bin/bash\nset -e\n\n# setup ros2 environment\nsource "/opt/ros/humble/setup.bash" --\nexec "$@"' > /ros_entrypoint.sh diff --git a/docker/noble-kilted/deps/Dockerfile b/docker/noble-kilted/deps/Dockerfile index 5c83a383..1edee22e 100644 --- a/docker/noble-kilted/deps/Dockerfile +++ b/docker/noble-kilted/deps/Dockerfile @@ -118,6 +118,10 @@ RUN git clone https://github.com/laurentkneip/opengv.git && \ cd && \ rm -r opengv +# ld.so.conf.d is read in filename order: -linux-gnu.conf sorts before +# libc.conf (/usr/local/lib) on arm64, so source builds lose to the distro ones. +RUN echo /usr/local/lib > /etc/ld.so.conf.d/00-usr-local.conf && ldconfig + RUN rm /bin/sh && ln -s /bin/bash /bin/sh RUN echo -e '#!/bin/bash\nset -e\n\n# setup ros2 environment\nsource "/opt/ros/kilted/setup.bash" --\nexec "$@"' > /ros_entrypoint.sh diff --git a/docker/noble/deps/Dockerfile b/docker/noble/deps/Dockerfile index 4129a7ef..d12b01fc 100644 --- a/docker/noble/deps/Dockerfile +++ b/docker/noble/deps/Dockerfile @@ -117,6 +117,10 @@ RUN git clone https://github.com/laurentkneip/opengv.git && \ cd && \ rm -r opengv +# ld.so.conf.d is read in filename order: -linux-gnu.conf sorts before +# libc.conf (/usr/local/lib) on arm64, so source builds lose to the distro ones. +RUN echo /usr/local/lib > /etc/ld.so.conf.d/00-usr-local.conf && ldconfig + RUN rm /bin/sh && ln -s /bin/bash /bin/sh RUN echo -e '#!/bin/bash\nset -e\n\n# setup ros2 environment\nsource "/opt/ros/jazzy/setup.bash" --\nexec "$@"' > /ros_entrypoint.sh diff --git a/docker/resolute/deps/Dockerfile b/docker/resolute/deps/Dockerfile index 3e2986c2..a023ed06 100644 --- a/docker/resolute/deps/Dockerfile +++ b/docker/resolute/deps/Dockerfile @@ -107,6 +107,10 @@ RUN git clone https://github.com/laurentkneip/opengv.git && \ cd && \ rm -r opengv +# ld.so.conf.d is read in filename order: -linux-gnu.conf sorts before +# libc.conf (/usr/local/lib) on arm64, so source builds lose to the distro ones. +RUN echo /usr/local/lib > /etc/ld.so.conf.d/00-usr-local.conf && ldconfig + RUN rm /bin/sh && ln -s /bin/bash /bin/sh RUN echo -e '#!/bin/bash\nset -e\n\n# setup ros2 environment\nsource "/opt/ros/lyrical/setup.bash" --\nexec "$@"' > /ros_entrypoint.sh From 31c59d09777c067b6e758c9f01b9c56ada9e6405 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Tue, 22 Sep 2026 09:48:45 -0700 Subject: [PATCH 08/10] No response filtering here: cv::goodFeaturesToTrack() already applies GFTT/QualityLevel, relative to the best corner's measure. Re-applying it as an absolute floor on KeyPoint::response double-filtered (~86% of keypoints ropped on OpenCV 4.5), and dropped *every* keypoint on OpenCV < 4.5, whose GFTTDetector leaves response at 0. --- corelib/src/Features2d.cpp | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/corelib/src/Features2d.cpp b/corelib/src/Features2d.cpp index ecb637ee..8de873d9 100644 --- a/corelib/src/Features2d.cpp +++ b/corelib/src/Features2d.cpp @@ -2282,20 +2282,6 @@ std::vector GFTT::generateKeypointsImpl(const cv::Mat & image, con _gftt->detect(imgRoi, keypoints, maskRoi); // Opencv keypoints } - if(!_useHarrisDetector && _qualityLevel>0.0) - { - std::vector bestKeypoints; - bestKeypoints.reserve(keypoints.size()); - for(size_t i=0; i _qualityLevel) - { - bestKeypoints.push_back(keypoints[i]); - } - } - - return bestKeypoints; - } return keypoints; } From 6a3171005b1483537624f65be771652043b9206a Mon Sep 17 00:00:00 2001 From: matlabbe Date: Tue, 22 Sep 2026 11:52:07 -0700 Subject: [PATCH 09/10] fixing ExtractXYZCorrespondencesRANSAC ci error --- .../include/rtabmap/core/StereoCameraModel.h | 45 ++++++++++ corelib/src/StereoCameraModel.cpp | 24 +++++ corelib/test/test_cameramodel.cpp | 39 ++++++++ corelib/test/test_stereocameramodel.cpp | 89 +++++++++++++++++++ corelib/test/test_util3d_correspondences.cpp | 80 +++++++++++++---- 5 files changed, 262 insertions(+), 15 deletions(-) diff --git a/corelib/include/rtabmap/core/StereoCameraModel.h b/corelib/include/rtabmap/core/StereoCameraModel.h index 4edca15c..d01a4f54 100644 --- a/corelib/include/rtabmap/core/StereoCameraModel.h +++ b/corelib/include/rtabmap/core/StereoCameraModel.h @@ -427,6 +427,51 @@ public: */ float computeDisparity(unsigned short depth) const; // mm + /** + * @brief Reprojects a 3D point of the left camera frame into both image planes (floating-point). + * + * The point is given in the rectified left camera frame (/camera_link), the same frame used + * by CameraModel::reproject() of left(). The baseline is taken from the Tx of the rectified + * projection matrices, so the horizontal shift between uLeft and uRight is the disparity of + * that point. On a rectified stereo pair the rows are aligned, thus vRight equals vLeft. + * + * @note Unlike this function, CameraModel::reproject() ignores Tx, because a Tx set on a + * single camera model is also used to tag a left camera having stereo observations + * (see the stereo edges built by the BA optimizers). + * + * @param x X coordinate in the left camera space. + * @param y Y coordinate in the left camera space. + * @param z Z coordinate in the left camera space (must be non-zero). + * @param[out] uLeft Output horizontal image coordinate in the left image (float). + * @param[out] vLeft Output vertical image coordinate in the left image (float). + * @param[out] uRight Output horizontal image coordinate in the right image (float). + * @param[out] vRight Output vertical image coordinate in the right image (float). + * + * @pre `z != 0` + * + * @see CameraModel::reproject(), reproject(int&, int&, int&, int&) + */ + void reproject(float x, float y, float z, float & uLeft, float & vLeft, float & uRight, float & vRight) const; + + /** + * @brief Reprojects a 3D point of the left camera frame into both image planes (rounded to int). + * + * This version of `reproject()` returns integer pixel indices, computed from the 3D position. + * + * @param x X coordinate in the left camera space. + * @param y Y coordinate in the left camera space. + * @param z Z coordinate in the left camera space (must be non-zero). + * @param[out] uLeft Output horizontal image coordinate in the left image (integer pixel). + * @param[out] vLeft Output vertical image coordinate in the left image (integer pixel). + * @param[out] uRight Output horizontal image coordinate in the right image (integer pixel). + * @param[out] vRight Output vertical image coordinate in the right image (integer pixel). + * + * @pre `z != 0` + * + * @see CameraModel::reproject(), reproject(float&, float&, float&, float&) + */ + void reproject(float x, float y, float z, int & uLeft, int & vLeft, int & uRight, int & vRight) const; + const cv::Mat & R() const {return R_;} ///< Stereo extrinsic rotation matrix. const cv::Mat & T() const {return T_;} ///< Stereo extrinsic translation vector. const cv::Mat & E() const {return E_;} ///< Essential matrix. diff --git a/corelib/src/StereoCameraModel.cpp b/corelib/src/StereoCameraModel.cpp index cae0437c..b2ca00b2 100644 --- a/corelib/src/StereoCameraModel.cpp +++ b/corelib/src/StereoCameraModel.cpp @@ -598,6 +598,30 @@ float StereoCameraModel::computeDisparity(unsigned short depth) const return baseline() * left().fx() / (float(depth)/1000.0f) - right().cx() + left().cx(); } +void StereoCameraModel::reproject(float x, float y, float z, float & uLeft, float & vLeft, float & uRight, float & vRight) const +{ + UASSERT(z!=0.0f); + float invZ = 1.0f/z; + // CameraModel::reproject() doesn't apply Tx, as a camera model with a Tx set is + // also used to tag a left camera having stereo observations (see the stereo edges + // of the BA optimizers). Here Tx is the baseline of the rectified projection + // matrices (0 for the left camera, -fx*baseline for the right one), so that + // (uLeft-uRight) is the disparity of the point. + uLeft = (left_.fx()*x + left_.Tx())*invZ + left_.cx(); + vLeft = (left_.fy()*y)*invZ + left_.cy(); + uRight = (right_.fx()*x + right_.Tx())*invZ + right_.cx(); + vRight = (right_.fy()*y)*invZ + right_.cy(); +} +void StereoCameraModel::reproject(float x, float y, float z, int & uLeft, int & vLeft, int & uRight, int & vRight) const +{ + float uLeftF, vLeftF, uRightF, vRightF; + this->reproject(x, y, z, uLeftF, vLeftF, uRightF, vRightF); + uLeft = uLeftF; + vLeft = vLeftF; + uRight = uRightF; + vRight = vRightF; +} + Transform StereoCameraModel::stereoTransform() const { if(!R_.empty() && !T_.empty()) diff --git a/corelib/test/test_cameramodel.cpp b/corelib/test/test_cameramodel.cpp index b7542eb0..300cdaf0 100644 --- a/corelib/test/test_cameramodel.cpp +++ b/corelib/test/test_cameramodel.cpp @@ -298,6 +298,45 @@ TEST_F(CameraModelTest, ReprojectInt) EXPECT_NEAR(v, static_cast(cy_), 1); } +TEST_F(CameraModelTest, ReprojectIgnoresTx) +{ + // A Tx set on a single camera model tags a left camera having stereo + // observations (the BA optimizers read the baseline from it to build their + // stereo edges), so reprojection stays that of the camera itself. Use + // StereoCameraModel::reproject() to get both images of a stereo pair. + double baseline = 0.12; + CameraModel withTx(fx_, fy_, cx_, cy_, CameraModel::opticalRotation(), -baseline*fx_, imageSize_); + CameraModel withoutTx(fx_, fy_, cx_, cy_, CameraModel::opticalRotation(), 0.0, imageSize_); + EXPECT_DOUBLE_EQ(withTx.Tx(), -baseline*fx_); + + float x = 0.3f, y = -0.2f, z = 2.0f; + + float u, v, uNoTx, vNoTx; + withTx.reproject(x, y, z, u, v); + withoutTx.reproject(x, y, z, uNoTx, vNoTx); + + EXPECT_FLOAT_EQ(u, uNoTx); + EXPECT_FLOAT_EQ(v, vNoTx); + EXPECT_FLOAT_EQ(u, static_cast(fx_*x/z + cx_)); + EXPECT_FLOAT_EQ(v, static_cast(fy_*y/z + cy_)); +} + +TEST_F(CameraModelTest, ReprojectProjectRoundTripNoTx) +{ + CameraModel model(fx_, fy_, cx_, cy_, CameraModel::opticalRotation(), 0.0, imageSize_); + + float x = 0.35f, y = -0.15f, z = 2.5f; + + float u, v; + model.reproject(x, y, z, u, v); + + float x2, y2, z2; + model.project(u, v, z, x2, y2, z2); + EXPECT_NEAR(x2, x, 0.001f); + EXPECT_NEAR(y2, y, 0.001f); + EXPECT_FLOAT_EQ(z2, z); +} + // Field of View Tests TEST_F(CameraModelTest, FieldOfView) diff --git a/corelib/test/test_stereocameramodel.cpp b/corelib/test/test_stereocameramodel.cpp index 3f40b82a..c6c448c5 100644 --- a/corelib/test/test_stereocameramodel.cpp +++ b/corelib/test/test_stereocameramodel.cpp @@ -7,6 +7,7 @@ #include "rtabmap/utilite/UDirectory.h" #include "rtabmap/utilite/UFile.h" #include +#include using namespace rtabmap; @@ -236,6 +237,94 @@ TEST_F(StereoCameraModelTest, ComputeDisparityZeroDepth) EXPECT_EQ(disparityMM, 0.0f); } +// Reprojection Tests + +TEST_F(StereoCameraModelTest, Reproject) +{ + StereoCameraModel model(fx_, fy_, cx_, cy_, baseline_); + + // a point 2 m in front of the left camera, off its optical axis + float x = 0.3f, y = -0.2f, z = 2.0f; + + float uLeft, vLeft, uRight, vRight; + model.reproject(x, y, z, uLeft, vLeft, uRight, vRight); + + // the left camera has no Tx, so its image point is the one of the left model alone + float u, v; + model.left().reproject(x, y, z, u, v); + EXPECT_DOUBLE_EQ(model.left().Tx(), 0.0); + EXPECT_FLOAT_EQ(uLeft, u); + EXPECT_FLOAT_EQ(vLeft, v); + EXPECT_FLOAT_EQ(uLeft, static_cast(fx_*x/z + cx_)); + EXPECT_FLOAT_EQ(vLeft, static_cast(fy_*y/z + cy_)); + + // rectified pair: same row in both images, right point shifted by the disparity + EXPECT_FLOAT_EQ(vRight, vLeft); + EXPECT_NEAR(uLeft - uRight, model.computeDisparity(z), 0.001f); + EXPECT_NEAR(uLeft - uRight, static_cast(baseline_*fx_/z), 0.001f); +} + +TEST_F(StereoCameraModelTest, ReprojectDisparityDecreasesWithDepth) +{ + StereoCameraModel model(fx_, fy_, cx_, cy_, baseline_); + + float previousDisparity = std::numeric_limits::max(); + for(float z=1.0f; z<=10.0f; z+=1.0f) + { + float uLeft, vLeft, uRight, vRight; + model.reproject(0.0f, 0.0f, z, uLeft, vLeft, uRight, vRight); + + // on the optical axis, the left point is the principal point + EXPECT_FLOAT_EQ(uLeft, static_cast(cx_)); + EXPECT_FLOAT_EQ(vLeft, static_cast(cy_)); + + float disparity = uLeft - uRight; + EXPECT_GT(disparity, 0.0f); // right camera on the right of the left one + EXPECT_LT(disparity, previousDisparity); + EXPECT_NEAR(model.computeDepth(disparity), z, 0.001f); + previousDisparity = disparity; + } +} + +TEST_F(StereoCameraModelTest, ReprojectInt) +{ + StereoCameraModel model(fx_, fy_, cx_, cy_, baseline_); + + float x = 0.3f, y = -0.2f, z = 2.0f; + + float uLeftF, vLeftF, uRightF, vRightF; + model.reproject(x, y, z, uLeftF, vLeftF, uRightF, vRightF); + + int uLeft, vLeft, uRight, vRight; + model.reproject(x, y, z, uLeft, vLeft, uRight, vRight); + + EXPECT_EQ(uLeft, static_cast(uLeftF)); + EXPECT_EQ(vLeft, static_cast(vLeftF)); + EXPECT_EQ(uRight, static_cast(uRightF)); + EXPECT_EQ(vRight, static_cast(vRightF)); +} + +TEST_F(StereoCameraModelTest, ReprojectProjectRoundTrip) +{ + StereoCameraModel model(fx_, fy_, cx_, cy_, baseline_); + + float x = -0.45f, y = 0.25f, z = 3.7f; + + float uLeft, vLeft, uRight, vRight; + model.reproject(x, y, z, uLeft, vLeft, uRight, vRight); + + // the disparity of the reprojected pair gives the depth back... + float depth = model.computeDepth(uLeft - uRight); + EXPECT_NEAR(depth, z, 0.001f); + + // ... and the left image point gives the 3D point back + float x2, y2, z2; + model.left().project(uLeft, vLeft, depth, x2, y2, z2); + EXPECT_NEAR(x2, x, 0.001f); + EXPECT_NEAR(y2, y, 0.001f); + EXPECT_NEAR(z2, z, 0.001f); +} + // Getter Tests TEST_F(StereoCameraModelTest, Baseline) diff --git a/corelib/test/test_util3d_correspondences.cpp b/corelib/test/test_util3d_correspondences.cpp index 7887d451..1f8f9b9b 100644 --- a/corelib/test/test_util3d_correspondences.cpp +++ b/corelib/test/test_util3d_correspondences.cpp @@ -2,6 +2,7 @@ #include "rtabmap/core/util3d.h" #include "rtabmap/core/util3d_correspondences.h" #include "rtabmap/core/CameraModel.h" +#include "rtabmap/core/StereoCameraModel.h" #include "rtabmap/utilite/UException.h" #include "rtabmap/utilite/UConversion.h" #include @@ -82,44 +83,93 @@ TEST(Util3dCorrespondencesTest, ExtractXYZCorrespondencesNoCommonIDs) { EXPECT_TRUE(cloud2.empty()); } +// Reprojects a fixed non-planar 3D scene in both images of a rectified stereo +// camera. The two-view geometry must be generic: with a planar scene or a pure +// image translation, all correspondences are related by a homography and the +// fundamental matrix is then only defined up to a 1-parameter family +// (F = [e']x * H for any epipole e'). RANSAC can pick a member of that family +// which also fits an outlier, making the inlier count depend on floating-point +// details of the platform and of the OpenCV version. Here the points span a +// range of depths, so their disparities differ and the geometry is well +// constrained. +static void reprojectStereoPair(int index, pcl::PointXYZ & left, pcl::PointXYZ & right) +{ + static const float points3d[12][3] = { + {-0.50f, -0.40f, 2.0f}, { 0.40f, -0.30f, 3.5f}, {-0.20f, 0.50f, 2.8f}, + { 0.60f, 0.20f, 5.0f}, {-0.60f, 0.10f, 4.2f}, { 0.10f, -0.50f, 6.5f}, + { 0.30f, 0.45f, 3.0f}, {-0.35f, -0.15f, 7.5f}, { 0.50f, -0.05f, 2.2f}, + {-0.10f, 0.30f, 5.8f}, { 0.25f, 0.35f, 4.6f}, {-0.45f, 0.20f, 3.3f}}; + + static const StereoCameraModel model(500.0, 500.0, 320.0, 240.0, 0.12); + + float uLeft, vLeft, uRight, vRight; + model.reproject(points3d[index][0], points3d[index][1], points3d[index][2], + uLeft, vLeft, uRight, vRight); + + // extractXYZCorrespondencesRANSAC() only uses x and y, as image coordinates + left = pcl::PointXYZ(uLeft, vLeft, 0.0f); + right = pcl::PointXYZ(uRight, vRight, 0.0f); +} + TEST(Util3dCorrespondencesTest, ExtractXYZCorrespondencesRANSACAcceptsCleanMatches) { std::multimap words1; std::multimap words2; - // 10 consistent matches - for (int i = 0; i < 10; ++i) { - words1.insert({i, pcl::PointXYZ(i * 1.0f, exp2(i)/10.0f, 0.0f)}); - words2.insert({i, pcl::PointXYZ(i * 1.0f + 1.1f, exp2(i)/10.0f + 1.1f, 0.0f)}); // Slight noise + // 12 consistent matches + for (int i = 0; i < 12; ++i) { + pcl::PointXYZ left, right; + reprojectStereoPair(i, left, right); + words1.insert({i, left}); + words2.insert({i, right}); } pcl::PointCloud cloud1, cloud2; util3d::extractXYZCorrespondencesRANSAC(words1, words2, cloud1, cloud2); EXPECT_EQ(cloud1.size(), cloud2.size()); - EXPECT_GE(cloud1.size(), 8); // At least 8 inliers from 10 consistent matches + EXPECT_EQ(cloud1.size(), 12); // every match is on its epipolar line } TEST(Util3dCorrespondencesTest, ExtractXYZCorrespondencesRANSACRejectsOutliers) { std::multimap words1; std::multimap words2; - // 8 inliers - for (int i = 0; i < 8; ++i) { - words1.insert({i, pcl::PointXYZ(i * 1.0f, exp2(i)/10.0f, 0.0f)}); - words2.insert({i, pcl::PointXYZ(i * 1.0f + 1.1f, exp2(i)/10.0f + 1.1f, 0.0f)}); // Slight noise + // 12 inliers + for (int i = 0; i < 12; ++i) { + pcl::PointXYZ left, right; + reprojectStereoPair(i, left, right); + words1.insert({i, left}); + words2.insert({i, right}); } - // 2 outliers - words1.insert({100, pcl::PointXYZ(0.0f, 0.0f, 0.0f)}); - words2.insert({100, pcl::PointXYZ(100.0f, 100.0f, 0.0f)}); - words1.insert({101, pcl::PointXYZ(1.0f, 1.0f, 0.0f)}); - words2.insert({101, pcl::PointXYZ(200.0f, -50.0f, 0.0f)}); + // 3 outliers: correct point in the left image, right point moved far away from + // the corresponding epipolar line (horizontal on a rectified stereo camera) + const int outlierSources[3] = {0, 4, 8}; + const float outlierOffsets[3][2] = {{0.0f, 120.0f}, {0.0f, -150.0f}, {40.0f, 90.0f}}; + for (int i = 0; i < 3; ++i) { + pcl::PointXYZ left, right; + reprojectStereoPair(outlierSources[i], left, right); + right.x += outlierOffsets[i][0]; + right.y += outlierOffsets[i][1]; + words1.insert({100+i, left}); + words2.insert({100+i, right}); + } pcl::PointCloud cloud1, cloud2; util3d::extractXYZCorrespondencesRANSAC(words1, words2, cloud1, cloud2); EXPECT_EQ(cloud1.size(), cloud2.size()); - EXPECT_EQ(cloud1.size(), 8); // RANSAC should reject 2 outliers + EXPECT_EQ(cloud1.size(), 12); // RANSAC should reject the 3 outliers + + // none of the outliers should have survived + for (unsigned int i = 0; i < cloud2.size(); ++i) { + for (int j = 0; j < 3; ++j) { + pcl::PointXYZ left, right; + reprojectStereoPair(outlierSources[j], left, right); + EXPECT_FALSE(cloud2[i].x == right.x + outlierOffsets[j][0] && + cloud2[i].y == right.y + outlierOffsets[j][1]); + } + } } TEST(Util3dCorrespondencesTest, ExtractXYZCorrespondencesRANSACFailsGracefullyOnTooFewMatches) { From 6aab607a3caa09dd8add9e0c7f4ed86cf98b4e49 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Tue, 22 Sep 2026 11:56:19 -0700 Subject: [PATCH 10/10] increased windows timeout (probably caused by gftt fix now extracting more features) --- corelib/test/CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/corelib/test/CMakeLists.txt b/corelib/test/CMakeLists.txt index b5ca0d4b..ddec5cb7 100644 --- a/corelib/test/CMakeLists.txt +++ b/corelib/test/CMakeLists.txt @@ -169,6 +169,12 @@ target_link_libraries(test_rtabmap_integration gtest_main rtabmap_core) # `ctest -L long`. add_test(NAME test_rtabmap_integration COMMAND test_rtabmap_integration) math(EXPR _integration_timeout "1800 * ${_test_timeout_scale}") +# The Windows runners replay the sample DBs far slower than the Linux and macOS +# ones (the rest of the suite takes ~3 min there, this test alone went over 30), +# so they get twice the budget rather than lowering the bar for every platform. +IF(WIN32) + math(EXPR _integration_timeout "${_integration_timeout} * 2") +ENDIF(WIN32) set_tests_properties(test_rtabmap_integration PROPERTIES TIMEOUT ${_integration_timeout} LABELS "long")