From 94e31dcad1f2b820de9665e814ca27d704f47f07 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 13 Jul 2025 04:15:03 -0700 Subject: [PATCH 001/126] Windows: fixed missing exported symbols (PythonInterface) --- corelib/include/rtabmap/core/PythonInterface.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/corelib/include/rtabmap/core/PythonInterface.h b/corelib/include/rtabmap/core/PythonInterface.h index f2f3a6a2..3129c75e 100644 --- a/corelib/include/rtabmap/core/PythonInterface.h +++ b/corelib/include/rtabmap/core/PythonInterface.h @@ -8,6 +8,7 @@ #ifndef CORELIB_SRC_PYTHON_PYTHONINTERFACE_H_ #define CORELIB_SRC_PYTHON_PYTHONINTERFACE_H_ +#include "rtabmap/core/rtabmap_core_export.h" // DLL export/import defines #include #include @@ -23,7 +24,7 @@ namespace rtabmap { * Create a single PythonInterface on main thread at * global scope before any Python classes. */ -class PythonInterface +class RTABMAP_CORE_EXPORT PythonInterface { public: PythonInterface(); @@ -34,7 +35,7 @@ private: pybind11::gil_scoped_release* release_; }; -std::string getPythonTraceback(); +std::string RTABMAP_CORE_EXPORT getPythonTraceback(); } From df6300e0ba3e90058f90b09c4d646279366d3516 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 13 Jul 2025 16:56:55 -0700 Subject: [PATCH 002/126] Windows-deploy: added missing zed's dll file. --- app/src/CMakeLists.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/CMakeLists.txt b/app/src/CMakeLists.txt index 7ec7e01c..2ed54b57 100644 --- a/app/src/CMakeLists.txt +++ b/app/src/CMakeLists.txt @@ -110,6 +110,16 @@ IF(BUILD_AS_BUNDLE AND (APPLE OR WIN32)) ENDIF(WIN32) ENDIF(k4a_FOUND) + IF(ZED_FOUND) + # Install needed zlibwapi.dll + IF(WIN32) + file(TO_CMAKE_PATH "$ENV{ZED_SDK_ROOT_DIR}" ENV_ZED_SDK_ROOT_DIR) + INSTALL(FILES "${ENV_ZED_SDK_ROOT_DIR}/bin/zlibwapi.dll" + DESTINATION ${thirdparty_dest_dir} + COMPONENT runtime) + ENDIF(WIN32) + ENDIF(ZED_FOUND) + IF(Torch_FOUND) # Install needed cudnn_ops_infer64_8.dll and cudnn_cnn_infer64_8.dll # TODO: should be a more general way to include them if version is different From 9650b71d8eb48e1bf9f96ee3e164106601754263 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 13 Jul 2025 23:15:06 -0700 Subject: [PATCH 003/126] CMake/Eigen: increased minimum major version to 3 if version not set (fixing g2o dep searching eigen without specifying version) --- cmake_modules/FindEigen3.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmake_modules/FindEigen3.cmake b/cmake_modules/FindEigen3.cmake index 0b36805e..460de2ae 100644 --- a/cmake_modules/FindEigen3.cmake +++ b/cmake_modules/FindEigen3.cmake @@ -27,10 +27,10 @@ if(NOT Eigen3_FIND_VERSION) if(NOT Eigen3_FIND_VERSION_MAJOR) - set(Eigen3_FIND_VERSION_MAJOR 2) + set(Eigen3_FIND_VERSION_MAJOR 3) endif() if(NOT Eigen3_FIND_VERSION_MINOR) - set(Eigen3_FIND_VERSION_MINOR 91) + set(Eigen3_FIND_VERSION_MINOR 0) endif() if(NOT Eigen3_FIND_VERSION_PATCH) set(Eigen3_FIND_VERSION_PATCH 0) From 330fda65220856d0e10d42375e7335b4788ecc3e Mon Sep 17 00:00:00 2001 From: Borong Yuan Date: Tue, 29 Jul 2025 14:09:42 +0800 Subject: [PATCH 004/126] Minor correction on using tilted sensor model for OAK camera --- corelib/src/camera/CameraDepthAI.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/corelib/src/camera/CameraDepthAI.cpp b/corelib/src/camera/CameraDepthAI.cpp index 9eb01791..8106d467 100644 --- a/corelib/src/camera/CameraDepthAI.cpp +++ b/corelib/src/camera/CameraDepthAI.cpp @@ -371,7 +371,7 @@ bool CameraDepthAI::init(const std::string & calibrationFolder, const std::strin std::vector coeffs = calibHandler.getDistortionCoefficients(cameraId); if(calibHandler.getDistortionModel(cameraId) == dai::CameraModel::Perspective) - distCoeffs = (cv::Mat_(1,8) << coeffs[0], coeffs[1], coeffs[2], coeffs[3], coeffs[4], coeffs[5], coeffs[6], coeffs[7]); + distCoeffs = (cv::Mat_(1,14) << coeffs[0], coeffs[1], coeffs[2], coeffs[3], coeffs[4], coeffs[5], coeffs[6], coeffs[7], coeffs[8], coeffs[9], coeffs[10], coeffs[11], coeffs[12], coeffs[13]); if(alphaScaling_>-1.0f) newCameraMatrix = cv::getOptimalNewCameraMatrix(cameraMatrix, distCoeffs, targetSize_, alphaScaling_); From 8328b22c3f69f5181a95a02815d20c407f0e08f7 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Mon, 4 Aug 2025 19:40:52 -0700 Subject: [PATCH 005/126] depthai: Adding assert for min expected coeffs --- corelib/src/camera/CameraDepthAI.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/corelib/src/camera/CameraDepthAI.cpp b/corelib/src/camera/CameraDepthAI.cpp index 8106d467..beaca874 100644 --- a/corelib/src/camera/CameraDepthAI.cpp +++ b/corelib/src/camera/CameraDepthAI.cpp @@ -370,9 +370,10 @@ bool CameraDepthAI::init(const std::string & calibrationFolder, const std::strin matrix[2][0], matrix[2][1], matrix[2][2]); std::vector coeffs = calibHandler.getDistortionCoefficients(cameraId); - if(calibHandler.getDistortionModel(cameraId) == dai::CameraModel::Perspective) + if(calibHandler.getDistortionModel(cameraId) == dai::CameraModel::Perspective) { + UASSERT(coeffs.size()>=14); distCoeffs = (cv::Mat_(1,14) << coeffs[0], coeffs[1], coeffs[2], coeffs[3], coeffs[4], coeffs[5], coeffs[6], coeffs[7], coeffs[8], coeffs[9], coeffs[10], coeffs[11], coeffs[12], coeffs[13]); - + } if(alphaScaling_>-1.0f) newCameraMatrix = cv::getOptimalNewCameraMatrix(cameraMatrix, distCoeffs, targetSize_, alphaScaling_); else From f2b9cc1cddd3cb61d063683b215e3bf70dc29572 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Wed, 6 Aug 2025 20:29:22 -0700 Subject: [PATCH 006/126] illumination invariant dataset: fixed typo --- archive/2022-IlluminationInvariant/scripts/export_stats.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/archive/2022-IlluminationInvariant/scripts/export_stats.sh b/archive/2022-IlluminationInvariant/scripts/export_stats.sh index ec0128fb..fd31f67b 100755 --- a/archive/2022-IlluminationInvariant/scripts/export_stats.sh +++ b/archive/2022-IlluminationInvariant/scripts/export_stats.sh @@ -13,7 +13,7 @@ source rtabmap_latest.bash for d in "${DETECTOR[@]}" do - rtabmap-report --export --export_prefix "Stat$d" --loc 32 Loop/Odom_correction_norm/m Loop/Visual_inliers/ Timing/Total/ms Timing/Proximity_by_space_visual/ms Timing/Likelihood_computation/ms Timing/Posterior_computation/ms TimingMem/Keypoints_detection/ms TimingMem/Descriptors_extraction/ms TimingMem/Add_new_words/ms Loop/Map_id/ Keypoint/Current_frame/words Memory/RAM_usage/MB Memory/RAM_estimated/MB Memory/Distance_travelled/m Loop/Distance_since_last_loc/ Memory/Local_graph_size/ Keypoint/Dictionary_size/words "$DATA/$d/loc" - rtabmap-report --export --export_prefix "Consecutive$d" --loc 32 Loop/Map_id/ Loop/Distance_since_last_loc/ "$DATA/$d/consecutive_loc" + rtabmap-report --export --export_prefix "Stat$d" --loc 32 Loop/Odom_correction_norm/m Loop/Visual_inliers/ Timing/Total/ms Timing/Proximity_by_space_visual/ms Timing/Likelihood_computation/ms Timing/Posterior_computation/ms TimingMem/Keypoints_detection/ms TimingMem/Descriptors_extraction/ms TimingMem/Add_new_words/ms Loop/Map_id/ Keypoint/Current_frame/words Memory/RAM_usage/MB Memory/RAM_estimated/MB Memory/Distance_travelled/m Loop/Distance_since_last_loc/m Memory/Local_graph_size/ Keypoint/Dictionary_size/words "$DATA/$d/loc" + rtabmap-report --export --export_prefix "Consecutive$d" --loc 32 Loop/Map_id/ Loop/Distance_since_last_loc/m "$DATA/$d/consecutive_loc" done From 49704743209316f13889a39a0ac617a5ad67aad5 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Wed, 6 Aug 2025 20:53:14 -0700 Subject: [PATCH 007/126] results_common.m fixed typo --- archive/2022-IlluminationInvariant/results_common.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/archive/2022-IlluminationInvariant/results_common.m b/archive/2022-IlluminationInvariant/results_common.m index e7cb03df..289305d5 100644 --- a/archive/2022-IlluminationInvariant/results_common.m +++ b/archive/2022-IlluminationInvariant/results_common.m @@ -15,7 +15,7 @@ RAMaddOverhead = 0; % Inliers_ratio = 'Loop/Visual_inliers/' ./ 'Keypoint/Current_frame/words' % Odometry_average = 'Memory/Distance_travelled/m'(2:end) - 'Memory/Distance_travelled/m'(1:end-1) -statNames = {'Loop/Odom_correction_norm/m', 'Loop/Visual_inliers/', 'Inliers_ratio_%', 'Timing/Total/ms', 'Memory/RAM_usage/MB', 'Memory/RAM_estimated/MB', 'Keypoint/Current_frame/words', 'Loop/Map_id/', 'Memory/Local_graph_size/', 'Keypoint/Dictionary_size/words', 'Loop/Distance_since_last_loc/'}; % 'Odometry_average' +statNames = {'Loop/Odom_correction_norm/m', 'Loop/Visual_inliers/', 'Inliers_ratio_%', 'Timing/Total/ms', 'Memory/RAM_usage/MB', 'Memory/RAM_estimated/MB', 'Keypoint/Current_frame/words', 'Loop/Map_id/', 'Memory/Local_graph_size/', 'Keypoint/Dictionary_size/words', 'Loop/Distance_since_last_loc/m'}; % 'Odometry_average' datasets = [ 0 1 6 7 9 14 11 111 ]; % 0 1 6 7 9 12 14 11 @@ -26,7 +26,7 @@ if resultsToShow == 2 sep = [0, 1000, 3000, 5000, 7000, 9000]; sepName = {'17:27', '17:54', '18:27', '18:56', '19:35'}; prefix = 'Consecutive'; - statNames = {'Loop/Distance_since_last_loc/', 'Distance_since_last_loc_under_50cm'}; + statNames = {'Loop/Distance_since_last_loc/m', 'Distance_since_last_loc_under_50cm'}; endif MapsN = length(sepName); @@ -52,7 +52,7 @@ if strcmp(statName,'Inliers_ratio_%') elseif strcmp(statName, 'Odometry_average') data = dlmread([dataDir '/' prefix num2str(datasets(d)) '-' 'Memory-Distance_travelled-m' '.txt'], '\t', 1, 0, "emptyvalue", 0); elseif strcmp(statName, 'Distance_since_last_loc_under_50cm') - data = dlmread([dataDir '/' prefix num2str(datasets(d)) '-' 'Loop-Distance_since_last_loc-' '.txt'], '\t', 1, 0, "emptyvalue", 0); + data = dlmread([dataDir '/' prefix num2str(datasets(d)) '-' 'Loop-Distance_since_last_loc-m' '.txt'], '\t', 1, 0, "emptyvalue", 0); else data = dlmread([dataDir '/' prefix num2str(datasets(d)) '-' statName '.txt'], '\t', 1, 0, "emptyvalue", 0); endif From 7d1a987d5d7b5689098b43602754b89262e6b191 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Thu, 7 Aug 2025 21:16:40 -0700 Subject: [PATCH 008/126] Planning: fix robot stuck when RGBD/LocalRadius is smaller than a link on the planned path. --- corelib/src/Rtabmap.cpp | 67 ++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 41 deletions(-) diff --git a/corelib/src/Rtabmap.cpp b/corelib/src/Rtabmap.cpp index 1c05b8da..38df8fde 100644 --- a/corelib/src/Rtabmap.cpp +++ b/corelib/src/Rtabmap.cpp @@ -2395,24 +2395,22 @@ bool Rtabmap::process( distanceSoFar += _path[i-1].second.getDistance(_path[i].second); } - if(distanceSoFar <= _localRadius) + if(_memory->getSignature(_path[i].first) != 0) { - if(_memory->getSignature(_path[i].first) != 0) + if(immunizedLocations.insert(_path[i].first).second) { - if(immunizedLocations.insert(_path[i].first).second) - { - ++immunizedLocally; - } - UDEBUG("Path immunization: node %d (dist=%fm)", _path[i].first, distanceSoFar); - } - else if(retrievalLocalIds.size() < _maxLocalRetrieved) - { - UINFO("retrieval of node %d on path (dist=%fm)", _path[i].first, distanceSoFar); - retrievalLocalIds.push_back(_path[i].first); - // retrieved locations are automatically immunized + ++immunizedLocally; } + UDEBUG("Path immunization: node %d (dist=%fm)", _path[i].first, distanceSoFar); } - else + else if(retrievalLocalIds.size() < _maxLocalRetrieved) + { + UINFO("retrieval of node %d on path (dist=%fm)", _path[i].first, distanceSoFar); + retrievalLocalIds.push_back(_path[i].first); + // retrieved locations are automatically immunized + } + + if(distanceSoFar > _localRadius) { UDEBUG("Stop on node %d (dist=%fm > %fm)", _path[i].first, distanceSoFar, _localRadius); @@ -3158,17 +3156,7 @@ bool Rtabmap::process( UASSERT(uContains(_optimizedPoses, signature->id())); UASSERT_MSG(uContains(_optimizedPoses, _path[_pathCurrentIndex].first), uFormat("id=%d", _path[_pathCurrentIndex].first).c_str()); Transform virtualLoop = _optimizedPoses.at(signature->id()).inverse() * _optimizedPoses.at(_path[_pathCurrentIndex].first); - - if(_localRadius == 0.0f || virtualLoop.getNorm() < _localRadius) - { - _memory->addLink(Link(signature->id(), _path[_pathCurrentIndex].first, Link::kVirtualClosure, virtualLoop, cv::Mat::eye(6,6,CV_64FC1)*0.01)); // set high variance - } - else - { - UERROR("Virtual link larger than local radius (%fm > %fm). Aborting the plan!", - virtualLoop.getNorm(), _localRadius); - this->clearPath(-1); - } + _memory->addLink(Link(signature->id(), _path[_pathCurrentIndex].first, Link::kVirtualClosure, virtualLoop, cv::Mat::eye(6,6,CV_64FC1)*0.01)); // set high variance } } @@ -6927,24 +6915,24 @@ void Rtabmap::updateGoalIndex() { distanceSoFar += _path[i-1].second.getDistance(_path[i].second); } - if(distanceSoFar <= _localRadius) + + if(_path[i].first != _path[i-1].first) { - if(_path[i].first != _path[i-1].first) + const Signature * s = _memory->getSignature(_path[i].first); + if(s) { - const Signature * s = _memory->getSignature(_path[i].first); - if(s) + if(!s->hasLink(_path[i-1].first) && _memory->getSignature(_path[i-1].first) != 0) { - if(!s->hasLink(_path[i-1].first) && _memory->getSignature(_path[i-1].first) != 0) - { - Transform virtualLoop = _path[i].second.inverse() * _path[i-1].second; - _memory->addLink(Link(_path[i].first, _path[i-1].first, Link::kVirtualClosure, virtualLoop, cv::Mat::eye(6,6,CV_64FC1)*0.01)); // on the optimized path - UINFO("Added Virtual link between %d and %d", _path[i-1].first, _path[i].first); - } + Transform virtualLoop = _path[i].second.inverse() * _path[i-1].second; + _memory->addLink(Link(_path[i].first, _path[i-1].first, Link::kVirtualClosure, virtualLoop, cv::Mat::eye(6,6,CV_64FC1)*0.01)); // on the optimized path + UINFO("Added Virtual link between %d and %d", _path[i-1].first, _path[i].first); } } } - else + + if(distanceSoFar > _localRadius) { + UDEBUG("Farthest goal=%d : %f m", _path[i].first, distanceSoFar); break; } } @@ -7004,11 +6992,8 @@ void Rtabmap::updateGoalIndex() if((goalIndex == _pathCurrentIndex && i == _path.size()-1) || _pathUnreachableNodes.find(i) == _pathUnreachableNodes.end()) { - if(distanceFromCurrentNode <= _localRadius) - { - goalIndex = i; - } - else + goalIndex = i; + if(distanceFromCurrentNode > _localRadius) { break; } From be5cd3998d9ceeb20b18ee3572ce21c222f04032 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 10 Aug 2025 17:00:34 -0700 Subject: [PATCH 009/126] Report: fixed gui not built with Qt6. Improved dev containers. Add latest deps dev container for convenience. --- .devcontainer/focal/Dockerfile | 21 ++++ .devcontainer/focal/devcontainer.json | 14 ++- .devcontainer/jammy/Dockerfile | 21 ++++ .devcontainer/jammy/devcontainer.json | 14 ++- .../latest-deps-from-source/Dockerfile | 98 +++++++++++++++++++ .../latest-deps-from-source/devcontainer.json | 21 ++++ .devcontainer/noble/Dockerfile | 25 +++++ .devcontainer/noble/devcontainer.json | 14 ++- .devcontainer/rolling/Dockerfile | 2 + .devcontainer/rolling/devcontainer.json | 1 + tools/Report/CMakeLists.txt | 4 +- 11 files changed, 227 insertions(+), 8 deletions(-) create mode 100644 .devcontainer/focal/Dockerfile create mode 100644 .devcontainer/jammy/Dockerfile create mode 100644 .devcontainer/latest-deps-from-source/Dockerfile create mode 100644 .devcontainer/latest-deps-from-source/devcontainer.json create mode 100644 .devcontainer/noble/Dockerfile diff --git a/.devcontainer/focal/Dockerfile b/.devcontainer/focal/Dockerfile new file mode 100644 index 00000000..841a7ff0 --- /dev/null +++ b/.devcontainer/focal/Dockerfile @@ -0,0 +1,21 @@ +FROM introlab3it/rtabmap:focal-deps + +RUN apt-get update && apt-get install -y sudo && \ + apt-get clean && rm -rf /var/lib/apt/lists/ + +ARG USERNAME=vscode +ARG USER_UID=1000 +ARG USER_GID=1000 + +RUN set -ex && \ + groupadd --gid ${USER_GID} ${USERNAME} && \ + useradd --uid ${USER_UID} --gid ${USER_GID} -m ${USERNAME} && \ + usermod -a -G sudo ${USERNAME} && \ + echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/${USERNAME} && \ + chmod 0440 /etc/sudoers.d/${USERNAME} + +RUN mkdir -p /home/${USERNAME}/Documents/RTAB-Map && chown -R ${USERNAME} /home/${USERNAME} + +RUN echo "source /usr/share/bash-completion/completions/git" >> /home/${USERNAME}/.bashrc + + diff --git a/.devcontainer/focal/devcontainer.json b/.devcontainer/focal/devcontainer.json index 8b96029c..30c64759 100644 --- a/.devcontainer/focal/devcontainer.json +++ b/.devcontainer/focal/devcontainer.json @@ -1,8 +1,18 @@ { - "image": "introlab3it/rtabmap:20.04", + "build": { + "dockerfile": "Dockerfile" + }, "customizations": { "vscode": { "extensions": ["ms-vscode.cpptools-themes", "ms-vscode.cmake-tools"] } - } + }, + "workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/rtabmap,type=bind", + "workspaceFolder": "/home/vscode/rtabmap", + "mounts": ["source=${localEnv:HOME}/Documents/RTAB-Map,target=/home/vscode/Documents/RTAB-Map,type=bind,consistency=cached"], + "settings": { + "terminal.integrated.defaultProfile.linux": "bash" + }, + "remoteUser": "vscode", + "runArgs": ["--privileged", "--network=host"] } diff --git a/.devcontainer/jammy/Dockerfile b/.devcontainer/jammy/Dockerfile new file mode 100644 index 00000000..83d58d64 --- /dev/null +++ b/.devcontainer/jammy/Dockerfile @@ -0,0 +1,21 @@ +FROM introlab3it/rtabmap:jammy-deps + +RUN apt-get update && apt-get install -y sudo && \ + apt-get clean && rm -rf /var/lib/apt/lists/ + +ARG USERNAME=vscode +ARG USER_UID=1000 +ARG USER_GID=1000 + +RUN set -ex && \ + groupadd --gid ${USER_GID} ${USERNAME} && \ + useradd --uid ${USER_UID} --gid ${USER_GID} -m ${USERNAME} && \ + usermod -a -G sudo ${USERNAME} && \ + echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/${USERNAME} && \ + chmod 0440 /etc/sudoers.d/${USERNAME} + +RUN mkdir -p /home/${USERNAME}/Documents/RTAB-Map && chown -R ${USERNAME} /home/${USERNAME} + +RUN echo "source /usr/share/bash-completion/completions/git" >> /home/${USERNAME}/.bashrc + + diff --git a/.devcontainer/jammy/devcontainer.json b/.devcontainer/jammy/devcontainer.json index 21c9f2ed..30c64759 100644 --- a/.devcontainer/jammy/devcontainer.json +++ b/.devcontainer/jammy/devcontainer.json @@ -1,8 +1,18 @@ { - "image": "introlab3it/rtabmap:22.04", + "build": { + "dockerfile": "Dockerfile" + }, "customizations": { "vscode": { "extensions": ["ms-vscode.cpptools-themes", "ms-vscode.cmake-tools"] } - } + }, + "workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/rtabmap,type=bind", + "workspaceFolder": "/home/vscode/rtabmap", + "mounts": ["source=${localEnv:HOME}/Documents/RTAB-Map,target=/home/vscode/Documents/RTAB-Map,type=bind,consistency=cached"], + "settings": { + "terminal.integrated.defaultProfile.linux": "bash" + }, + "remoteUser": "vscode", + "runArgs": ["--privileged", "--network=host"] } diff --git a/.devcontainer/latest-deps-from-source/Dockerfile b/.devcontainer/latest-deps-from-source/Dockerfile new file mode 100644 index 00000000..176d5a59 --- /dev/null +++ b/.devcontainer/latest-deps-from-source/Dockerfile @@ -0,0 +1,98 @@ +ARG ROS_DISTRO=noble + +FROM osrf/ros:${ROS_DISTRO}-desktop + +# Install build dependencies +RUN apt-get update && \ + apt-get install -y git software-properties-common ros-${ROS_DISTRO}-rtabmap-ros libqt6* qt6* qml6* && \ + apt-get remove -y ros-${ROS_DISTRO}-rtabmap* ros-${ROS_DISTRO}-gtsam ros-${ROS_DISTRO}-libg2o libpcl* libqt5* qt5* libvtk* libopencv* && \ + apt-get clean && rm -rf /var/lib/apt/lists/ + +# remove ubuntu user +RUN touch /var/mail/ubuntu && chown ubuntu /var/mail/ubuntu && userdel -r ubuntu + +RUN apt-get update && apt-get install -y sudo && \ + apt-get clean && rm -rf /var/lib/apt/lists/ + +ARG USERNAME=vscode +ARG USER_UID=1000 +ARG USER_GID=1000 + +RUN set -ex && \ + groupadd --gid ${USER_GID} ${USERNAME} && \ + useradd --uid ${USER_UID} --gid ${USER_GID} -m ${USERNAME} && \ + usermod -a -G sudo ${USERNAME} && \ + echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/${USERNAME} && \ + chmod 0440 /etc/sudoers.d/${USERNAME} + +RUN mkdir -p /home/${USERNAME}/Documents/RTAB-Map + +RUN echo "source /usr/share/bash-completion/completions/git" >> /home/${USERNAME}/.bashrc + +WORKDIR /home/${USERNAME}/ + +RUN rm /bin/sh && ln -s /bin/bash /bin/sh + +# We build main dependencies from source, cleaning up the +# build directory after install but keeping the source code +# to change version/reinstall from inside the dev container +# if needed. + +# Build latest VTK with Qt6 +RUN git clone https://github.com/Kitware/VTK.git +RUN cd VTK && \ + mkdir build && \ + cd build && \ + cmake -DVTK_GROUP_ENABLE_Qt=YES .. && \ + make -j$(nproc) && \ + make install && \ + make clean + +# Build latest PCL with latest VTK +# Make sure all libraries depending on Eigen are built with same CXX standard (17) +RUN git clone https://github.com/PointCloudLibrary/pcl.git +RUN cd pcl && \ + mkdir build && \ + cd build && \ + cmake -DCMAKE_CXX_STANDARD=17 -DBUILD_tools=ON -DPCL_ENABLE_AVX=OFF -DPCL_ENABLE_MARCHNATIVE=OFF -DPCL_ENABLE_SSE=OFF .. && \ + make -j$(nproc) && \ + make install && \ + make clean + +# Build latest OpenCV +RUN git clone https://github.com/opencv/opencv.git +RUN git clone https://github.com/opencv/opencv_contrib.git +RUN cd opencv && \ + mkdir build && \ + cd build && \ + cmake -DBUILD_opencv_python3=OFF -DBUILD_opencv_python_bindings_generator=OFF -DBUILD_opencv_python_tests=OFF -DBUILD_PERF_TESTS=OFF -DBUILD_TESTS=OFF -DOPENCV_ENABLE_NONFREE=ON -DOPENCV_EXTRA_MODULES_PATH=/root/opencv_contrib/modules .. && \ + make -j$(nproc) && \ + make install && \ + make clean + +# Build latest gtsam +RUN git clone https://github.com/borglab/gtsam.git +RUN cd gtsam && \ + mkdir build && \ + cd build && \ + cmake -DCMAKE_CXX_STANDARD=17 -DGTSAM_BUILD_WITH_MARCH_NATIVE=OFF -DGTSAM_BUILD_EXAMPLES_ALWAYS=OFF -DGTSAM_BUILD_TESTS=OFF -DGTSAM_BUILD_STATIC_LIBRARY=OFF -DGTSAM_BUILD_UNSTABLE=OFF -DGTSAM_INSTALL_CPPUNILITE=OFF -DGTSAM_USE_SYSTEM_EIGEN=ON -DCMAKE_BUILD_TYPE=Release .. && \ + make -j$(nproc) && \ + make install && \ + make clean + +# Build latest g2o +RUN git clone https://github.com/RainerKuemmerle/g2o.git +RUN cd g2o && \ + mkdir build && \ + cd build && \ + cmake -DCMAKE_CXX_STANDARD=17 -DBUILD_WITH_MARCH_NATIVE=OFF -DG2O_BUILD_APPS=OFF -DG2O_BUILD_EXAMPLES=OFF -DG2O_USE_OPENGL=OFF -DCMAKE_BUILD_TYPE=Release .. && \ + make -j$(nproc) && \ + make install && \ + make clean + +# ros2 seems not sourcing by default its multi-arch folders +ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/ros/${ROS_DISTRO}/lib/x86_64-linux-gnu + +RUN ldconfig + +RUN chown -R ${USERNAME} /home/${USERNAME} diff --git a/.devcontainer/latest-deps-from-source/devcontainer.json b/.devcontainer/latest-deps-from-source/devcontainer.json new file mode 100644 index 00000000..dc626632 --- /dev/null +++ b/.devcontainer/latest-deps-from-source/devcontainer.json @@ -0,0 +1,21 @@ +{ + "build": { + "dockerfile": "Dockerfile", + "args": { + "ROS_DISTRO": "jazzy" + } + }, + "customizations": { + "vscode": { + "extensions": ["ms-vscode.cpptools-themes", "ms-vscode.cmake-tools"] + } + }, + "workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/rtabmap,type=bind", + "workspaceFolder": "/home/vscode/rtabmap", + "mounts": ["source=${localEnv:HOME}/Documents/RTAB-Map,target=/home/vscode/Documents/RTAB-Map,type=bind,consistency=cached"], + "settings": { + "terminal.integrated.defaultProfile.linux": "bash" + }, + "remoteUser": "vscode", + "runArgs": ["--privileged", "--network=host"] +} diff --git a/.devcontainer/noble/Dockerfile b/.devcontainer/noble/Dockerfile new file mode 100644 index 00000000..84792a66 --- /dev/null +++ b/.devcontainer/noble/Dockerfile @@ -0,0 +1,25 @@ +FROM introlab3it/rtabmap:noble-deps + +# For devcontainer +# remove ubuntu user +RUN touch /var/mail/ubuntu && chown ubuntu /var/mail/ubuntu && userdel -r ubuntu + +RUN apt-get update && apt-get install -y sudo && \ + apt-get clean && rm -rf /var/lib/apt/lists/ + +ARG USERNAME=vscode +ARG USER_UID=1000 +ARG USER_GID=1000 + +RUN set -ex && \ + groupadd --gid ${USER_GID} ${USERNAME} && \ + useradd --uid ${USER_UID} --gid ${USER_GID} -m ${USERNAME} && \ + usermod -a -G sudo ${USERNAME} && \ + echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/${USERNAME} && \ + chmod 0440 /etc/sudoers.d/${USERNAME} + +RUN mkdir -p /home/${USERNAME}/Documents/RTAB-Map && chown -R ${USERNAME} /home/${USERNAME} + +RUN echo "source /usr/share/bash-completion/completions/git" >> /home/${USERNAME}/.bashrc + + diff --git a/.devcontainer/noble/devcontainer.json b/.devcontainer/noble/devcontainer.json index 66e9a222..30c64759 100644 --- a/.devcontainer/noble/devcontainer.json +++ b/.devcontainer/noble/devcontainer.json @@ -1,8 +1,18 @@ { - "image": "introlab3it/rtabmap:24.04", + "build": { + "dockerfile": "Dockerfile" + }, "customizations": { "vscode": { "extensions": ["ms-vscode.cpptools-themes", "ms-vscode.cmake-tools"] } - } + }, + "workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/rtabmap,type=bind", + "workspaceFolder": "/home/vscode/rtabmap", + "mounts": ["source=${localEnv:HOME}/Documents/RTAB-Map,target=/home/vscode/Documents/RTAB-Map,type=bind,consistency=cached"], + "settings": { + "terminal.integrated.defaultProfile.linux": "bash" + }, + "remoteUser": "vscode", + "runArgs": ["--privileged", "--network=host"] } diff --git a/.devcontainer/rolling/Dockerfile b/.devcontainer/rolling/Dockerfile index 38c18c24..38434a50 100644 --- a/.devcontainer/rolling/Dockerfile +++ b/.devcontainer/rolling/Dockerfile @@ -73,5 +73,7 @@ RUN set -ex && \ echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/${USERNAME} && \ chmod 0440 /etc/sudoers.d/${USERNAME} +RUN mkdir -p /home/${USERNAME}/Documents/RTAB-Map && chown -R ${USERNAME} /home/${USERNAME} + RUN echo "source /usr/share/bash-completion/completions/git" >> /home/${USERNAME}/.bashrc diff --git a/.devcontainer/rolling/devcontainer.json b/.devcontainer/rolling/devcontainer.json index 95463406..30c64759 100644 --- a/.devcontainer/rolling/devcontainer.json +++ b/.devcontainer/rolling/devcontainer.json @@ -9,6 +9,7 @@ }, "workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/rtabmap,type=bind", "workspaceFolder": "/home/vscode/rtabmap", + "mounts": ["source=${localEnv:HOME}/Documents/RTAB-Map,target=/home/vscode/Documents/RTAB-Map,type=bind,consistency=cached"], "settings": { "terminal.integrated.defaultProfile.linux": "bash" }, diff --git a/tools/Report/CMakeLists.txt b/tools/Report/CMakeLists.txt index b25242c2..6965f2c5 100644 --- a/tools/Report/CMakeLists.txt +++ b/tools/Report/CMakeLists.txt @@ -1,10 +1,10 @@ set(LIBRARIES rtabmap_core) -IF(WITH_QT AND (QT4_FOUND OR Qt5_FOUND)) +IF(WITH_QT AND (QT4_FOUND OR Qt5_FOUND OR Qt6_FOUND)) ADD_DEFINITIONS("-DWITH_QT") set(LIBRARIES ${LIBRARIES} rtabmap_gui) -ENDIF(WITH_QT AND (QT4_FOUND OR Qt5_FOUND)) +ENDIF(WITH_QT AND (QT4_FOUND OR Qt5_FOUND OR Qt6_FOUND)) ADD_EXECUTABLE(report main.cpp) From b9db73a2394cb9772bbeb5a1f51280dd7b798266 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 10 Aug 2025 19:03:56 -0700 Subject: [PATCH 010/126] devcontainer: don't mount Documents/RTAB-Map by default. latest_deps: using qt5 instead of qt6 because back window issue --- .devcontainer/focal/devcontainer.json | 2 +- .devcontainer/jammy/devcontainer.json | 2 +- .devcontainer/latest-deps-from-source/Dockerfile | 14 ++++++++++---- .../latest-deps-from-source/devcontainer.json | 2 +- .devcontainer/noble/devcontainer.json | 2 +- .devcontainer/rolling/devcontainer.json | 2 +- 6 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.devcontainer/focal/devcontainer.json b/.devcontainer/focal/devcontainer.json index 30c64759..7684b5f4 100644 --- a/.devcontainer/focal/devcontainer.json +++ b/.devcontainer/focal/devcontainer.json @@ -9,7 +9,7 @@ }, "workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/rtabmap,type=bind", "workspaceFolder": "/home/vscode/rtabmap", - "mounts": ["source=${localEnv:HOME}/Documents/RTAB-Map,target=/home/vscode/Documents/RTAB-Map,type=bind,consistency=cached"], + //"mounts": ["source=${localEnv:HOME}/Documents/RTAB-Map,target=/home/vscode/Documents/RTAB-Map,type=bind,consistency=cached"], "settings": { "terminal.integrated.defaultProfile.linux": "bash" }, diff --git a/.devcontainer/jammy/devcontainer.json b/.devcontainer/jammy/devcontainer.json index 30c64759..7684b5f4 100644 --- a/.devcontainer/jammy/devcontainer.json +++ b/.devcontainer/jammy/devcontainer.json @@ -9,7 +9,7 @@ }, "workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/rtabmap,type=bind", "workspaceFolder": "/home/vscode/rtabmap", - "mounts": ["source=${localEnv:HOME}/Documents/RTAB-Map,target=/home/vscode/Documents/RTAB-Map,type=bind,consistency=cached"], + //"mounts": ["source=${localEnv:HOME}/Documents/RTAB-Map,target=/home/vscode/Documents/RTAB-Map,type=bind,consistency=cached"], "settings": { "terminal.integrated.defaultProfile.linux": "bash" }, diff --git a/.devcontainer/latest-deps-from-source/Dockerfile b/.devcontainer/latest-deps-from-source/Dockerfile index 176d5a59..29d7d88f 100644 --- a/.devcontainer/latest-deps-from-source/Dockerfile +++ b/.devcontainer/latest-deps-from-source/Dockerfile @@ -2,10 +2,16 @@ ARG ROS_DISTRO=noble FROM osrf/ros:${ROS_DISTRO}-desktop -# Install build dependencies +# Install build dependencies with Qt6 (issue: rtabmap has black window) +#RUN apt-get update && \ +# apt-get install -y git software-properties-common ros-${ROS_DISTRO}-rtabmap-ros libqt6* qt6* qml6* && \ +# apt-get remove -y ros-${ROS_DISTRO}-rtabmap* ros-${ROS_DISTRO}-gtsam ros-${ROS_DISTRO}-libg2o libpcl* libqt5* qt5* libvtk* libopencv* && \ +# apt-get clean && rm -rf /var/lib/apt/lists/ + +# Install build dependencies with Qt5 RUN apt-get update && \ - apt-get install -y git software-properties-common ros-${ROS_DISTRO}-rtabmap-ros libqt6* qt6* qml6* && \ - apt-get remove -y ros-${ROS_DISTRO}-rtabmap* ros-${ROS_DISTRO}-gtsam ros-${ROS_DISTRO}-libg2o libpcl* libqt5* qt5* libvtk* libopencv* && \ + apt-get install -y git software-properties-common ros-${ROS_DISTRO}-rtabmap-ros && \ + apt-get remove -y ros-${ROS_DISTRO}-rtabmap* ros-${ROS_DISTRO}-gtsam ros-${ROS_DISTRO}-libg2o libpcl* libvtk* libopencv* && \ apt-get clean && rm -rf /var/lib/apt/lists/ # remove ubuntu user @@ -65,7 +71,7 @@ RUN git clone https://github.com/opencv/opencv_contrib.git RUN cd opencv && \ mkdir build && \ cd build && \ - cmake -DBUILD_opencv_python3=OFF -DBUILD_opencv_python_bindings_generator=OFF -DBUILD_opencv_python_tests=OFF -DBUILD_PERF_TESTS=OFF -DBUILD_TESTS=OFF -DOPENCV_ENABLE_NONFREE=ON -DOPENCV_EXTRA_MODULES_PATH=/root/opencv_contrib/modules .. && \ + cmake -DBUILD_opencv_python3=OFF -DBUILD_opencv_python_bindings_generator=OFF -DBUILD_opencv_python_tests=OFF -DBUILD_PERF_TESTS=OFF -DBUILD_TESTS=OFF -DOPENCV_ENABLE_NONFREE=ON -DOPENCV_EXTRA_MODULES_PATH=../../opencv_contrib/modules .. && \ make -j$(nproc) && \ make install && \ make clean diff --git a/.devcontainer/latest-deps-from-source/devcontainer.json b/.devcontainer/latest-deps-from-source/devcontainer.json index dc626632..c63155a5 100644 --- a/.devcontainer/latest-deps-from-source/devcontainer.json +++ b/.devcontainer/latest-deps-from-source/devcontainer.json @@ -12,7 +12,7 @@ }, "workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/rtabmap,type=bind", "workspaceFolder": "/home/vscode/rtabmap", - "mounts": ["source=${localEnv:HOME}/Documents/RTAB-Map,target=/home/vscode/Documents/RTAB-Map,type=bind,consistency=cached"], + //"mounts": ["source=${localEnv:HOME}/Documents/RTAB-Map,target=/home/vscode/Documents/RTAB-Map,type=bind,consistency=cached"], "settings": { "terminal.integrated.defaultProfile.linux": "bash" }, diff --git a/.devcontainer/noble/devcontainer.json b/.devcontainer/noble/devcontainer.json index 30c64759..7684b5f4 100644 --- a/.devcontainer/noble/devcontainer.json +++ b/.devcontainer/noble/devcontainer.json @@ -9,7 +9,7 @@ }, "workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/rtabmap,type=bind", "workspaceFolder": "/home/vscode/rtabmap", - "mounts": ["source=${localEnv:HOME}/Documents/RTAB-Map,target=/home/vscode/Documents/RTAB-Map,type=bind,consistency=cached"], + //"mounts": ["source=${localEnv:HOME}/Documents/RTAB-Map,target=/home/vscode/Documents/RTAB-Map,type=bind,consistency=cached"], "settings": { "terminal.integrated.defaultProfile.linux": "bash" }, diff --git a/.devcontainer/rolling/devcontainer.json b/.devcontainer/rolling/devcontainer.json index 30c64759..7684b5f4 100644 --- a/.devcontainer/rolling/devcontainer.json +++ b/.devcontainer/rolling/devcontainer.json @@ -9,7 +9,7 @@ }, "workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/rtabmap,type=bind", "workspaceFolder": "/home/vscode/rtabmap", - "mounts": ["source=${localEnv:HOME}/Documents/RTAB-Map,target=/home/vscode/Documents/RTAB-Map,type=bind,consistency=cached"], + //"mounts": ["source=${localEnv:HOME}/Documents/RTAB-Map,target=/home/vscode/Documents/RTAB-Map,type=bind,consistency=cached"], "settings": { "terminal.integrated.defaultProfile.linux": "bash" }, From 52e770e2d46a95c739c1890bd499f2a9f2912e61 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Tue, 12 Aug 2025 00:13:19 -0700 Subject: [PATCH 011/126] Update Dockerfile --- .devcontainer/latest-deps-from-source/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/latest-deps-from-source/Dockerfile b/.devcontainer/latest-deps-from-source/Dockerfile index 29d7d88f..941967ae 100644 --- a/.devcontainer/latest-deps-from-source/Dockerfile +++ b/.devcontainer/latest-deps-from-source/Dockerfile @@ -1,4 +1,4 @@ -ARG ROS_DISTRO=noble +ARG ROS_DISTRO=jazzy FROM osrf/ros:${ROS_DISTRO}-desktop From 349580299cf9f115ad414f144a6792a8109cecce Mon Sep 17 00:00:00 2001 From: matlabbe Date: Wed, 3 Sep 2025 15:59:37 -0700 Subject: [PATCH 012/126] Fixing chessboard generation #1572 --- guilib/src/CalibrationDialog.cpp | 17 +++++++++-------- guilib/src/ui/calibrationDialog.ui | 4 ++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/guilib/src/CalibrationDialog.cpp b/guilib/src/CalibrationDialog.cpp index dd4a0523..f6b27fe7 100644 --- a/guilib/src/CalibrationDialog.cpp +++ b/guilib/src/CalibrationDialog.cpp @@ -250,17 +250,18 @@ void CalibrationDialog::resetSettings() cv::Mat drawChessboard(int squareSize, int boardWidth, int boardHeight, int borderSize) { - int imageWidth = squareSize*boardWidth + borderSize; - int imageHeight = squareSize*boardHeight + borderSize; - cv::Mat chessboard(imageWidth, imageHeight, CV_8UC1, 255); - unsigned char color = 0; + int imageWidth = squareSize*(boardWidth+1) + 2*borderSize; + int imageHeight = squareSize*(boardHeight+1) + 2*borderSize; + cv::Mat chessboard(imageHeight, imageWidth, CV_8UC1, 255); + unsigned char rowColor = 0; for(int i=borderSize;ifalse - 2 + 3 8 @@ -373,7 +373,7 @@ false - 2 + 3 6 From 4603f093893c88ba87e30b7f158aacc07cf8a36e Mon Sep 17 00:00:00 2001 From: matlabbe Date: Wed, 3 Sep 2025 16:55:54 -0700 Subject: [PATCH 013/126] Loading time optimization (#1569) * On init, rebuild the dictionnary only once * Fixed first dictionary update to avoid rebuilding multiple times. Commented some very verbose debug logs (should create a new level: UVERBOSE or UTRACE) * bump version 0.23: added parameters "Mem/LoadVisualLocalFeaturesOnInit", "Mem/FlannIndexSaved", "Kp/SerializeWithChecksum". * Renamed Mem/FlannIndexSaved to Kp/FlannIndexSaved. Update UI Preferences with new parameters. * commented some very verbose debug logs * Warn flann index serialization not implemented on windows * Warn flann index deserialization not implemented on windows * Removing some verbose logs * missing header (win32) * Added GlobalMap::fullUpdateNeeded() function * Adding log * Save flann index even if links changed * log time to serialize flann index * Make dictionary modified only after we update --------- Co-authored-by: Mathieu Labbe --- CMakeLists.txt | 4 +- corelib/include/rtabmap/core/DBDriver.h | 18 +- .../include/rtabmap/core/DBDriverSqlite3.h | 11 +- corelib/include/rtabmap/core/FlannIndex.h | 49 +- corelib/include/rtabmap/core/GlobalMap.h | 1 + corelib/include/rtabmap/core/Memory.h | 3 + corelib/include/rtabmap/core/Parameters.h | 3 + corelib/include/rtabmap/core/VWDictionary.h | 6 + corelib/src/CMakeLists.txt | 1 + corelib/src/CameraModel.cpp | 2 +- corelib/src/DBDriver.cpp | 63 +- corelib/src/DBDriverSqlite3.cpp | 565 +++++++++----- corelib/src/DBReader.cpp | 2 +- corelib/src/FlannIndex.cpp | 449 ++++++++--- corelib/src/GlobalMap.cpp | 17 +- corelib/src/LocalGrid.cpp | 4 +- corelib/src/Memory.cpp | 178 +++-- corelib/src/Optimizer.cpp | 4 +- corelib/src/RegistrationVis.cpp | 10 +- corelib/src/SensorData.cpp | 6 +- corelib/src/Signature.cpp | 6 +- corelib/src/VWDictionary.cpp | 221 ++++-- corelib/src/VisualWord.cpp | 2 +- corelib/src/odometry/OdometryF2F.cpp | 2 + corelib/src/odometry/OdometryF2M.cpp | 1 + corelib/src/resources/DatabaseSchema.sql.in | 4 +- .../DatabaseSchema_0_22_0.sql | 183 +++++ corelib/src/rtflann/flann.hpp | 33 +- guilib/src/DatabaseViewer.cpp | 10 +- guilib/src/MainWindow.cpp | 16 +- guilib/src/MultiSessionLocWidget.cpp | 10 +- guilib/src/PreferencesDialog.cpp | 3 + guilib/src/ProgressDialog.cpp | 2 +- guilib/src/ui/preferencesDialog.ui | 711 ++++++++++-------- tools/Matcher/main.cpp | 2 + 35 files changed, 1811 insertions(+), 791 deletions(-) create mode 100644 corelib/src/resources/backward_compatibility/DatabaseSchema_0_22_0.sql diff --git a/CMakeLists.txt b/CMakeLists.txt index b7d36de7..936a7a6f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,8 +19,8 @@ SET(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake_modules") # VERSION ####################### SET(RTABMAP_MAJOR_VERSION 0) -SET(RTABMAP_MINOR_VERSION 22) -SET(RTABMAP_PATCH_VERSION 1) +SET(RTABMAP_MINOR_VERSION 23) +SET(RTABMAP_PATCH_VERSION 0) SET(RTABMAP_VERSION ${RTABMAP_MAJOR_VERSION}.${RTABMAP_MINOR_VERSION}.${RTABMAP_PATCH_VERSION}) diff --git a/corelib/include/rtabmap/core/DBDriver.h b/corelib/include/rtabmap/core/DBDriver.h index fdbfe285..54fe0a06 100644 --- a/corelib/include/rtabmap/core/DBDriver.h +++ b/corelib/include/rtabmap/core/DBDriver.h @@ -129,6 +129,7 @@ public: std::vector > * texCoords = 0, #endif cv::Mat * textures = 0) const; + void saveFlannIndex(const std::vector & indexData) const; public: // Mutex-protected methods of abstract versions below @@ -161,19 +162,20 @@ public: void executeNoResult(const std::string & sql) const; // Load objects - void load(VWDictionary * dictionary, bool lastStateOnly = true) const; - void loadLastNodes(std::list & signatures) const; // returned signatures must be freed after usage + void load(VWDictionary & dictionary, bool lastStateOnly = true) const; + void loadLastNodes(std::list & signatures, bool loadWordIdsOnly = false) const; // returned signatures must be freed after usage Signature * loadSignature(int id, bool * loadedFromTrash = 0); // returned signature must be freed after usage, call loadSignatures() instead if more than one signature should be loaded - void loadSignatures(const std::list & ids, std::list & signatures, std::set * loadedFromTrash = 0); // returned signatures must be freed after usage + void loadSignatures(const std::list & ids, std::list & signatures, std::set * loadedFromTrash = 0, bool loadWordIdsOnly = false); // returned signatures must be freed after usage void loadWords(const std::set & wordIds, std::list & vws); // returned words must be freed after usage // Specific queries... - void loadNodeData(Signature * signature, bool images = true, bool scan = true, bool userData = true, bool occupancyGrid = true) const; + void loadNodeData(Signature & signature, bool images = true, bool scan = true, bool userData = true, bool occupancyGrid = true) const; void loadNodeData(std::list & signatures, bool images = true, bool scan = true, bool userData = true, bool occupancyGrid = true) const; void getNodeData(int signatureId, SensorData & data, bool images = true, bool scan = true, bool userData = true, bool occupancyGrid = true) const; bool getCalibration(int signatureId, std::vector & models, std::vector & stereoModels) const; bool getLaserScanInfo(int signatureId, LaserScan & info) const; bool getNodeInfo(int signatureId, Transform & pose, int & mapId, int & weight, std::string & label, double & stamp, Transform & groundTruthPose, std::vector & velocity, GPS & gps, EnvSensors & sensors) const; + void getLocalFeatures(int signatureId, std::multimap & words, std::vector & keypoints, std::vector & points, cv::Mat & descriptors) const; void loadLinks(int signatureId, std::multimap & links, Link::Type type = Link::kUndef) const; void getWeight(int signatureId, int & weight) const; void getLastNodeIds(std::set & ids) const; @@ -274,11 +276,12 @@ protected: std::vector > * texCoords, #endif cv::Mat * textures) const = 0; + virtual void saveFlannIndexQuery(const std::vector & indexData) const = 0; // Load objects - virtual void loadQuery(VWDictionary * dictionary, bool lastStateOnly = true) const = 0; - virtual void loadLastNodesQuery(std::list & signatures) const = 0; - virtual void loadSignaturesQuery(const std::list & ids, std::list & signatures) const = 0; + virtual void loadQuery(VWDictionary & dictionary, bool lastStateOnly = true) const = 0; + virtual void loadLastNodesQuery(std::list & signatures, bool loadWordIdsOnly) const = 0; + virtual void loadSignaturesQuery(const std::list & ids, std::list & signatures, bool loadWordIdsOnly) const = 0; virtual void loadWordsQuery(const std::set & wordIds, std::list & vws) const = 0; virtual void loadLinksQuery(int signatureId, std::multimap & links, Link::Type type = Link::kUndef) const = 0; @@ -286,6 +289,7 @@ protected: virtual bool getCalibrationQuery(int signatureId, std::vector & models, std::vector & stereoModels) const = 0; virtual bool getLaserScanInfoQuery(int signatureId, LaserScan & info) const = 0; virtual bool getNodeInfoQuery(int signatureId, Transform & pose, int & mapId, int & weight, std::string & label, double & stamp, Transform & groundTruthPose, std::vector & velocity, GPS & gps, EnvSensors & sensors) const = 0; + virtual void getLocalFeaturesQuery(int signatureId, std::multimap & words, std::vector & keypoints, std::vector & points, cv::Mat & descriptors) const = 0; virtual void getLastNodeIdsQuery(std::set & ids) const = 0; virtual void getAllNodeIdsQuery(std::set & ids, bool ignoreChildren, bool ignoreBadSignatures, bool ignoreIntermediateNodes) const = 0; virtual void getAllOdomPosesQuery(std::map & poses, bool ignoreChildren, bool ignoreIntermediateNodes) const = 0; diff --git a/corelib/include/rtabmap/core/DBDriverSqlite3.h b/corelib/include/rtabmap/core/DBDriverSqlite3.h index 407ec7e0..052ffb8e 100644 --- a/corelib/include/rtabmap/core/DBDriverSqlite3.h +++ b/corelib/include/rtabmap/core/DBDriverSqlite3.h @@ -135,10 +135,12 @@ protected: #endif cv::Mat * textures) const; + virtual void saveFlannIndexQuery(const std::vector & indexData) const; + // Load objects - virtual void loadQuery(VWDictionary * dictionary, bool lastStateOnly = true) const; - virtual void loadLastNodesQuery(std::list & signatures) const; - virtual void loadSignaturesQuery(const std::list & ids, std::list & signatures) const; + virtual void loadQuery(VWDictionary & dictionary, bool lastStateOnly = true) const; + virtual void loadLastNodesQuery(std::list & signatures, bool loadWordIdsOnly) const; + virtual void loadSignaturesQuery(const std::list & ids, std::list & signatures, bool loadWordIdsOnly) const; virtual void loadWordsQuery(const std::set & wordIds, std::list & vws) const; virtual void loadLinksQuery(int signatureId, std::multimap & links, Link::Type type = Link::kUndef) const; @@ -146,6 +148,7 @@ protected: virtual bool getCalibrationQuery(int signatureId, std::vector & models, std::vector & stereoModels) const; virtual bool getLaserScanInfoQuery(int signatureId, LaserScan & info) const; virtual bool getNodeInfoQuery(int signatureId, Transform & pose, int & mapId, int & weight, std::string & label, double & stamp, Transform & groundTruthPose, std::vector & velocity, GPS & gps, EnvSensors & sensors) const; + virtual void getLocalFeaturesQuery(int signatureId, std::multimap & words, std::vector & keypoints, std::vector & points, cv::Mat & descriptors) const; virtual void getLastNodeIdsQuery(std::set & ids) const; virtual void getAllNodeIdsQuery(std::set & ids, bool ignoreChildren, bool ignoreBadSignatures, bool ignoreIntermediateNodes) const; virtual void getAllOdomPosesQuery(std::map & poses, bool ignoreChildren, bool ignoreIntermediateNodes) const; @@ -190,6 +193,8 @@ private: const cv::Point3f & viewpoint) const; private: + void loadWordsQuery(std::list & signatures) const; + void loadWordIdsQuery(std::list & signatures) const; void loadLinksQuery(std::list & signatures) const; int loadOrSaveDb(sqlite3 *pInMemory, const std::string & fileName, int isSave) const; diff --git a/corelib/include/rtabmap/core/FlannIndex.h b/corelib/include/rtabmap/core/FlannIndex.h index a1cd253e..8e387400 100644 --- a/corelib/include/rtabmap/core/FlannIndex.h +++ b/corelib/include/rtabmap/core/FlannIndex.h @@ -37,37 +37,48 @@ namespace rtabmap { class RTABMAP_CORE_EXPORT FlannIndex { public: + // A forward of the internal enum, indexes should match. See src/rtflann/defines.h + enum flann_algorithm_t + { + FLANN_INDEX_LINEAR = 0, + FLANN_INDEX_KDTREE = 1, + FLANN_INDEX_KDTREE_SINGLE = 4, + FLANN_INDEX_LSH = 6, + }; + FlannIndex(); virtual ~FlannIndex(); void release(); + std::vector serializeIndex(bool computeChecksum = true) const; + size_t indexedFeatures() const; // return Bytes size_t memoryUsed() const; // Note that useDistanceL1 doesn't have any effect if LSH is used - void buildLinearIndex( + void buildIndex( + flann_algorithm_t algorithm, const cv::Mat & features, bool useDistanceL1 = false, float rebalancingFactor = 2.0f); - void buildKDTreeIndex( - const cv::Mat & features, - int trees = 4, - bool useDistanceL1 = false, - float rebalancingFactor = 2.0f); - void buildKDTreeSingleIndex( - const cv::Mat & features, - int leafMaxSize = 10, - bool reorder = true, - bool useDistanceL1 = false, - float rebalancingFactor = 2.0f); - void buildLSHIndex( - const cv::Mat & features, - unsigned int table_number = 12, - unsigned int key_size = 20, - unsigned int multi_probe_level = 2, - float rebalancingFactor = 2.0f); + // Return false if the indexData doesn't correspond to expected features used and parameters. + bool loadIndex( + const std::vector & indexData, + flann_algorithm_t algorithm, + const cv::Mat & features, + bool useDistanceL1 = false, + float rebalancingFactor = 2.0f, + std::string * errorMsg = NULL); + bool loadIndex( + const unsigned char * indexData, + size_t indexDataSize, + flann_algorithm_t algorithm, + const cv::Mat & features, + bool useDistanceL1 = false, + float rebalancingFactor = 2.0f, + std::string * errorMsg = NULL); bool isBuilt(); @@ -104,9 +115,9 @@ private: unsigned int nextIndex_; int featuresType_; int featuresDim_; - bool isLSH_; bool useDistanceL1_; // true=EUCLEDIAN_L2 false=MANHATTAN_L1 float rebalancingFactor_; + flann_algorithm_t algorithm_; // keep feature in memory until the tree is rebuilt // (in case the word is deleted when removed from the VWDictionary) diff --git a/corelib/include/rtabmap/core/GlobalMap.h b/corelib/include/rtabmap/core/GlobalMap.h index 14092132..8f9e4a2e 100644 --- a/corelib/include/rtabmap/core/GlobalMap.h +++ b/corelib/include/rtabmap/core/GlobalMap.h @@ -53,6 +53,7 @@ public: public: virtual ~GlobalMap(); + bool fullUpdateNeeded(const std::map & poses) const; bool update(const std::map & poses); // return true if map has changed virtual void clear(); diff --git a/corelib/include/rtabmap/core/Memory.h b/corelib/include/rtabmap/core/Memory.h index 39996d92..dc1dd43f 100644 --- a/corelib/include/rtabmap/core/Memory.h +++ b/corelib/include/rtabmap/core/Memory.h @@ -264,6 +264,7 @@ private: void addSignatureToStm(Signature * signature, const cv::Mat & covariance); void clear(); void loadDataFromDb(bool postInitClosingEvents); + void saveFlannIndex(bool postInitClosingEvents); void moveToTrash(Signature * s, bool keepLinkedToGraph = true, std::list * deletedWords = 0); void moveSignatureToWMFromSTM(int id, int * reducedTo = 0); @@ -299,6 +300,7 @@ private: float _similarityThreshold; bool _binDataKept; bool _rawDescriptorsKept; + bool _loadVisualLocalFeaturesOnInit; bool _saveDepth16Format; bool _notLinkedNodesKeptInDb; bool _saveIntermediateNodeData; @@ -306,6 +308,7 @@ private: std::string _depthCompressionFormat; bool _incrementalMemory; bool _localizationDataSaved; + bool _flannIndexSaved; bool _reduceGraph; int _maxStMemSize; float _recentWmRatio; diff --git a/corelib/include/rtabmap/core/Parameters.h b/corelib/include/rtabmap/core/Parameters.h index 9ddc4f37..69a941b8 100644 --- a/corelib/include/rtabmap/core/Parameters.h +++ b/corelib/include/rtabmap/core/Parameters.h @@ -204,6 +204,7 @@ class RTABMAP_CORE_EXPORT Parameters RTABMAP_PARAM(Mem, ImageKept, bool, false, "Keep raw images in RAM."); RTABMAP_PARAM(Mem, BinDataKept, bool, true, "Keep binary data in db."); RTABMAP_PARAM(Mem, RawDescriptorsKept, bool, true, "Raw descriptors kept in memory."); + RTABMAP_PARAM(Mem, LoadVisualLocalFeaturesOnInit, bool, true, "Load all local visual features (keypoints, descriptors and 3D points) in RAM when loading an existing database. This can add significant time to initialize the memory but the features will be already loaded before computing loop closure transforms. If false, the features are loaded on-demand from the database when a loop closure transformation should be estimated."); RTABMAP_PARAM(Mem, MapLabelsAdded, bool, true, "Create map labels. The first node of a map will be labeled as \"map#\" where # is the map ID."); RTABMAP_PARAM(Mem, SaveDepth16Format, bool, false, "Save depth image into 16 bits format to reduce memory used. Warning: values over ~65 meters are ignored (maximum 65535 millimeters)."); RTABMAP_PARAM(Mem, NotLinkedNodesKept, bool, true, "Keep not linked nodes in db (rehearsed nodes and deleted nodes)."); @@ -260,6 +261,8 @@ class RTABMAP_CORE_EXPORT Parameters RTABMAP_PARAM_STR(Kp, RoiRatios, "0.0 0.0 0.0 0.0", "Region of interest ratios [left, right, top, bottom]."); RTABMAP_PARAM_STR(Kp, DictionaryPath, "", "Path of the pre-computed dictionary"); RTABMAP_PARAM(Kp, NewWordsComparedTogether, bool, true, "When adding new words to dictionary, they are compared also with each other (to detect same words in the same signature)."); + RTABMAP_PARAM(Kp, FlannIndexSaved, bool, false, uFormat("Save FLANN index during localization session (when %s=false). The FLANN index will be saved to database after the first time localization mode is used, then on next sessions, the index is reloaded from the database instead of being rebuilt again. This can save significant loading time when the visual word dictionary is big (>1M words). Note that if the dictionary is modified (parameters or data), the index will be rebuilt and saved again on the next session.", kMemIncrementalMemory().c_str()).c_str()); + RTABMAP_PARAM(Kp, SerializeWithChecksum, bool, true, "On serialization of the FLANN index, compute checksum of the data used by the FLANN index. This adds a slight overhead on serialization/deserialization to make sure that the dictionary data correspond to same data used when the index was built."); RTABMAP_PARAM(Kp, SubPixWinSize, int, 3, "See cv::cornerSubPix()."); RTABMAP_PARAM(Kp, SubPixIterations, int, 0, "See cv::cornerSubPix(). 0 disables sub pixel refining."); RTABMAP_PARAM(Kp, SubPixEps, double, 0.02, "See cv::cornerSubPix()."); diff --git a/corelib/include/rtabmap/core/VWDictionary.h b/corelib/include/rtabmap/core/VWDictionary.h index fbfa7ba5..7b58209d 100644 --- a/corelib/include/rtabmap/core/VWDictionary.h +++ b/corelib/include/rtabmap/core/VWDictionary.h @@ -107,7 +107,11 @@ public: bool isIncrementalFlann() const {return _incrementalFlann;} void setIncrementalDictionary(); void setFixedDictionary(const std::string & dictionaryPath); + bool isModified() const; + std::vector serializeIndex() const; + void deserializeIndex(const std::vector & data); + void deserializeIndex(const unsigned char * data, size_t size); void exportDictionary(const char * fileNameReferences, const char * fileNameDescriptors) const; void clear(bool printWarningsIfNotEmpty = true); @@ -137,10 +141,12 @@ private: std::string _dictionaryPath; // a pre-computed dictionary (.txt or .db) std::string _newDictionaryPath; // a pre-computed dictionary (.txt or .db) bool _newWordsComparedTogether; + bool _serializeWithChecksum; int _lastWordId; bool useDistanceL1_; FlannIndex * _flannIndex; cv::Mat _dataTree; + bool _modified; NNStrategy _strategy; std::map _mapIndexId; std::map _mapIdIndex; diff --git a/corelib/src/CMakeLists.txt b/corelib/src/CMakeLists.txt index b758ad0f..39288a03 100644 --- a/corelib/src/CMakeLists.txt +++ b/corelib/src/CMakeLists.txt @@ -816,6 +816,7 @@ CONFIGURE_FILE(${CMAKE_CURRENT_SOURCE_DIR}/resources/DatabaseSchema.sql.in ${CMA SET(RESOURCES ${CMAKE_CURRENT_SOURCE_DIR}/resources/DatabaseSchema.sql + ${CMAKE_CURRENT_SOURCE_DIR}/resources/backward_compatibility/DatabaseSchema_0_22_0.sql ${CMAKE_CURRENT_SOURCE_DIR}/resources/backward_compatibility/DatabaseSchema_0_20_0.sql ${CMAKE_CURRENT_SOURCE_DIR}/resources/backward_compatibility/DatabaseSchema_0_18_3.sql ${CMAKE_CURRENT_SOURCE_DIR}/resources/backward_compatibility/DatabaseSchema_0_18_0.sql diff --git a/corelib/src/CameraModel.cpp b/corelib/src/CameraModel.cpp index 3e80d25a..7c1980c5 100644 --- a/corelib/src/CameraModel.cpp +++ b/corelib/src/CameraModel.cpp @@ -554,7 +554,7 @@ unsigned int CameraModel::deserialize(const unsigned char * data, unsigned int d int iR = 8; int iP = 9; int iL = 10; - UDEBUG("Header: %d %d %d %d %d %d %d %d %d %d %d", header[0],header[1],header[2],header[3],header[4],header[5],header[6],header[7],header[8],header[9],header[10]); + //UDEBUG("Header: %d %d %d %d %d %d %d %d %d %d %d", header[0],header[1],header[2],header[3],header[4],header[5],header[6],header[7],header[8],header[9],header[10]); unsigned int requiredDataSize = sizeof(int)*headerSize + sizeof(double)*(header[iK]+header[iD]+header[iR]+header[iP]) + sizeof(float)*header[iL]; diff --git a/corelib/src/DBDriver.cpp b/corelib/src/DBDriver.cpp index 2c381164..5e645584 100644 --- a/corelib/src/DBDriver.cpp +++ b/corelib/src/DBDriver.cpp @@ -383,7 +383,7 @@ void DBDriver::asyncSave(Signature * s) { if(s) { - UDEBUG("s=%d", s->id()); + //UDEBUG("s=%d", s->id()); _trashesMutex.lock(); { _trashSignatures.insert(std::pair(s->id(), s)); @@ -531,17 +531,17 @@ void DBDriver::updateLaserScan(int nodeId, const LaserScan & scan) _dbSafeAccessMutex.unlock(); } -void DBDriver::load(VWDictionary * dictionary, bool lastStateOnly) const +void DBDriver::load(VWDictionary & dictionary, bool lastStateOnly) const { _dbSafeAccessMutex.lock(); this->loadQuery(dictionary, lastStateOnly); _dbSafeAccessMutex.unlock(); } -void DBDriver::loadLastNodes(std::list & signatures) const +void DBDriver::loadLastNodes(std::list & signatures, bool loadWordIdsOnly) const { _dbSafeAccessMutex.lock(); - this->loadLastNodesQuery(signatures); + this->loadLastNodesQuery(signatures, loadWordIdsOnly); _dbSafeAccessMutex.unlock(); } @@ -564,7 +564,8 @@ Signature * DBDriver::loadSignature(int id, bool * loadedFromTrash) } void DBDriver::loadSignatures(const std::list & signIds, std::list & signatures, - std::set * loadedFromTrash) + std::set * loadedFromTrash, + bool loadWordIdsOnly) { UDEBUG(""); // look up in the trash before the database @@ -609,7 +610,7 @@ void DBDriver::loadSignatures(const std::list & signIds, if(ids.size()) { _dbSafeAccessMutex.lock(); - this->loadSignaturesQuery(ids, signatures); + this->loadSignaturesQuery(ids, signatures, loadWordIdsOnly); _dbSafeAccessMutex.unlock(); } } @@ -656,10 +657,10 @@ void DBDriver::loadWords(const std::set & wordIds, std::list } } -void DBDriver::loadNodeData(Signature * signature, bool images, bool scan, bool userData, bool occupancyGrid) const +void DBDriver::loadNodeData(Signature & signature, bool images, bool scan, bool userData, bool occupancyGrid) const { std::list signatures; - signatures.push_back(signature); + signatures.push_back(&signature); this->loadNodeData(signatures, images, scan, userData, occupancyGrid); } @@ -823,6 +824,45 @@ bool DBDriver::getNodeInfo( return found; } +void DBDriver::getLocalFeatures( + int signatureId, + std::multimap & words, + std::vector & keypoints, + std::vector & points, + cv::Mat & descriptors) const +{ + bool found = false; + // look in the trash + _trashesMutex.lock(); + if(uContains(_trashSignatures, signatureId)) + { + const Signature * s = _trashSignatures.at(signatureId); + UASSERT(s != 0); + found = true; + if(!s->getWords().empty()) + { + words = s->getWords(); + if(s->getWordsKpts().empty()){ + found = false; // Force checking the database in case the local features were not loaded in RAM + } + else + { + words = s->getWords(); + keypoints = s->getWordsKpts(); + points = s->getWords3(); + descriptors = s->getWordsDescriptors().clone(); + } + } + } + _trashesMutex.unlock(); + + if(!found) + { + UScopeMutex lock(_dbSafeAccessMutex); + getLocalFeaturesQuery(signatureId, words, keypoints, points, descriptors); + } +} + void DBDriver::loadLinks(int signatureId, std::multimap & links, Link::Type type) const { bool found = false; @@ -1287,6 +1327,13 @@ cv::Mat DBDriver::loadOptimizedMesh( return cloud; } +void DBDriver::saveFlannIndex(const std::vector & indexData) const +{ + _dbSafeAccessMutex.lock(); + saveFlannIndexQuery(indexData); + _dbSafeAccessMutex.unlock(); +} + void DBDriver::generateGraph( const std::string & fileName, const std::set & idsInput, diff --git a/corelib/src/DBDriverSqlite3.cpp b/corelib/src/DBDriverSqlite3.cpp index 85a02637..7c250fa0 100644 --- a/corelib/src/DBDriverSqlite3.cpp +++ b/corelib/src/DBDriverSqlite3.cpp @@ -34,6 +34,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/core/util3d.h" #include "rtabmap/core/Compression.h" #include "DatabaseSchema_sql.h" +#include "DatabaseSchema_0_22_0_sql.h" #include "DatabaseSchema_0_20_0_sql.h" #include "DatabaseSchema_0_18_3_sql.h" #include "DatabaseSchema_0_18_0_sql.h" @@ -404,6 +405,7 @@ bool DBDriverSqlite3::connectDatabaseQuery(const std::string & url, bool overwri schemas.push_back(std::make_pair("0.18.0", DATABASESCHEMA_0_18_0_SQL)); schemas.push_back(std::make_pair("0.18.3", DATABASESCHEMA_0_18_3_SQL)); schemas.push_back(std::make_pair("0.20.0", DATABASESCHEMA_0_20_0_SQL)); + schemas.push_back(std::make_pair("0.22.0", DATABASESCHEMA_0_22_0_SQL)); schemas.push_back(std::make_pair(uNumber2Str(RTABMAP_VERSION_MAJOR)+"."+uNumber2Str(RTABMAP_VERSION_MINOR), DATABASESCHEMA_SQL)); for(size_t i=0; i > DBDriverSqlite3::getAllStatisticsWmStatesQuery( void DBDriverSqlite3::loadNodeDataQuery(std::list & signatures, bool images, bool scan, bool userData, bool occupancyGrid) const { - UDEBUG("load data for %d signatures images=%d scan=%d userData=%d, grid=%d", - (int)signatures.size(), images?1:0, scan?1:0, userData?1:0, occupancyGrid?1:0); + //UDEBUG("load data for %d signatures images=%d scan=%d userData=%d, grid=%d", + // (int)signatures.size(), images?1:0, scan?1:0, userData?1:0, occupancyGrid?1:0); if(!images && !scan && !userData && !occupancyGrid) { @@ -1445,7 +1447,7 @@ void DBDriverSqlite3::loadNodeDataQuery(std::list & signatures, boo { UASSERT(*iter != 0); - ULOGGER_DEBUG("Loading data for %d...", (*iter)->id()); + //ULOGGER_DEBUG("Loading data for %d...", (*iter)->id()); // bind id rc = sqlite3_bind_int(ppStmt, 1, (*iter)->id()); UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); @@ -1874,7 +1876,7 @@ void DBDriverSqlite3::loadNodeDataQuery(std::list & signatures, boo // Finalize (delete) the statement rc = sqlite3_finalize(ppStmt); UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); - ULOGGER_DEBUG("Time=%fs", timer.ticks()); + //ULOGGER_DEBUG("Time=%fs", timer.ticks()); } } @@ -2391,6 +2393,23 @@ bool DBDriverSqlite3::getNodeInfoQuery(int signatureId, return found; } +void DBDriverSqlite3::getLocalFeaturesQuery( + int signatureId, + std::multimap & words, + std::vector & keypoints, + std::vector & points, + cv::Mat & descriptors) const +{ + Signature s(signatureId); + std::list ids; + ids.push_back(&s); + this->loadWordsQuery(ids); + words = ids.front()->getWords(); + keypoints = ids.front()->getWordsKpts(); + points = ids.front()->getWords3(); + descriptors = ids.front()->getWordsDescriptors().clone(); +} + void DBDriverSqlite3::getLastNodeIdsQuery(std::set & ids) const { if(_ppDb) @@ -2987,7 +3006,7 @@ void DBDriverSqlite3::getWeightQuery(int nodeId, int & weight) const } //may be slower than the previous version but don't have a limit of words that can be loaded at the same time -void DBDriverSqlite3::loadSignaturesQuery(const std::list & ids, std::list & nodes) const +void DBDriverSqlite3::loadSignaturesQuery(const std::list & ids, std::list & nodes, bool loadWordIdsOnly) const { ULOGGER_DEBUG("count=%d", (int)ids.size()); if(_ppDb && ids.size()) @@ -3151,7 +3170,7 @@ void DBDriverSqlite3::loadSignaturesQuery(const std::list & ids, std::list< // create the node if(id) { - ULOGGER_DEBUG("Creating %d (map=%d, pose=%s)", *iter, mapId, pose.prettyPrint().c_str()); + //ULOGGER_DEBUG("Creating %d (map=%d, pose=%s)", *iter, mapId, pose.prettyPrint().c_str()); Signature * s = new Signature( id, mapId, @@ -3190,175 +3209,17 @@ void DBDriverSqlite3::loadSignaturesQuery(const std::list & ids, std::list< ULOGGER_DEBUG("Time=%fs", timer.ticks()); // Prepare the query... Get the map from signature and visual words - std::stringstream query2; - if(uStrNumCmp(_version, "0.13.0") >= 0) - { - query2 << "SELECT word_id, pos_x, pos_y, size, dir, response, octave, depth_x, depth_y, depth_z, descriptor_size, descriptor " - "FROM Feature " - "WHERE node_id = ? "; + UDEBUG("Loading local features (ids only=%s)....", loadWordIdsOnly?"true":"false"); + if(loadWordIdsOnly) { + this->loadWordIdsQuery(nodes); } - else if(uStrNumCmp(_version, "0.12.0") >= 0) - { - query2 << "SELECT word_id, pos_x, pos_y, size, dir, response, octave, depth_x, depth_y, depth_z, descriptor_size, descriptor " - "FROM Map_Node_Word " - "WHERE node_id = ? "; + else { + this->loadWordsQuery(nodes); } - else if(uStrNumCmp(_version, "0.11.2") >= 0) - { - query2 << "SELECT word_id, pos_x, pos_y, size, dir, response, depth_x, depth_y, depth_z, descriptor_size, descriptor " - "FROM Map_Node_Word " - "WHERE node_id = ? "; - } - else - { - query2 << "SELECT word_id, pos_x, pos_y, size, dir, response, depth_x, depth_y, depth_z " - "FROM Map_Node_Word " - "WHERE node_id = ? "; - } - - query2 << " ORDER BY word_id"; // Needed for fast insertion below - query2 << ";"; - - rc = sqlite3_prepare_v2(_ppDb, query2.str().c_str(), -1, &ppStmt, 0); - UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); - - float nanFloat = std::numeric_limits::quiet_NaN (); - - for(std::list::const_iterator iter=nodes.begin(); iter!=nodes.end(); ++iter) - { - //ULOGGER_DEBUG("Loading words of %d...", (*iter)->id()); - // bind id - rc = sqlite3_bind_int(ppStmt, 1, (*iter)->id()); - UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); - - int visualWordId = 0; - int descriptorSize = 0; - const void * descriptor = 0; - int dRealSize = 0; - cv::KeyPoint kpt; - std::multimap visualWords; - std::vector visualWordsKpts; - std::vector visualWords3; - cv::Mat descriptors; - bool allWords3NaN = true; - cv::Point3f depth(0,0,0); - - // Process the result if one - rc = sqlite3_step(ppStmt); - while(rc == SQLITE_ROW) - { - int index = 0; - visualWordId = sqlite3_column_int(ppStmt, index++); - kpt.pt.x = sqlite3_column_double(ppStmt, index++); - kpt.pt.y = sqlite3_column_double(ppStmt, index++); - kpt.size = sqlite3_column_int(ppStmt, index++); - kpt.angle = sqlite3_column_double(ppStmt, index++); - kpt.response = sqlite3_column_double(ppStmt, index++); - if(uStrNumCmp(_version, "0.12.0") >= 0) - { - kpt.octave = sqlite3_column_int(ppStmt, index++); - } - - if(sqlite3_column_type(ppStmt, index) == SQLITE_NULL) - { - depth.x = nanFloat; - ++index; - } - else - { - depth.x = sqlite3_column_double(ppStmt, index++); - } - - if(sqlite3_column_type(ppStmt, index) == SQLITE_NULL) - { - depth.y = nanFloat; - ++index; - } - else - { - depth.y = sqlite3_column_double(ppStmt, index++); - } - - if(sqlite3_column_type(ppStmt, index) == SQLITE_NULL) - { - depth.z = nanFloat; - ++index; - } - else - { - depth.z = sqlite3_column_double(ppStmt, index++); - } - - visualWordsKpts.push_back(kpt); - visualWords.insert(visualWords.end(), std::make_pair(visualWordId, visualWordsKpts.size()-1)); - visualWords3.push_back(depth); - - if(allWords3NaN && util3d::isFinite(depth)) - { - allWords3NaN = false; - } - - if(uStrNumCmp(_version, "0.11.2") >= 0) - { - descriptorSize = sqlite3_column_int(ppStmt, index++); // VisualWord descriptor size - descriptor = sqlite3_column_blob(ppStmt, index); // VisualWord descriptor array - dRealSize = sqlite3_column_bytes(ppStmt, index++); - - if(descriptor && descriptorSize>0 && dRealSize>0) - { - cv::Mat d; - if(dRealSize == descriptorSize) - { - // CV_8U binary descriptors - d = cv::Mat(1, descriptorSize, CV_8U); - } - else if(dRealSize/int(sizeof(float)) == descriptorSize) - { - // CV_32F - d = cv::Mat(1, descriptorSize, CV_32F); - } - else - { - UFATAL("Saved buffer size (%d bytes) is not the same as descriptor size (%d)", dRealSize, descriptorSize); - } - - memcpy(d.data, descriptor, dRealSize); - - descriptors.push_back(d); - } - } - - rc = sqlite3_step(ppStmt); - } - UASSERT_MSG(rc == SQLITE_DONE, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); - - if(visualWords.size()==0) - { - UDEBUG("Empty signature detected! (id=%d)", (*iter)->id()); - } - else - { - if(allWords3NaN) - { - visualWords3.clear(); - } - (*iter)->setWords(visualWords, visualWordsKpts, visualWords3, descriptors); - ULOGGER_DEBUG("Add %d keypoints, %d 3d points and %d descriptors to node %d", (int)visualWords.size(), allWords3NaN?0:(int)visualWords3.size(), (int)descriptors.rows, (*iter)->id()); - } - - //reset - rc = sqlite3_reset(ppStmt); - UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); - } - - // Finalize (delete) the statement - rc = sqlite3_finalize(ppStmt); - UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); - - ULOGGER_DEBUG("Time=%fs", timer.ticks()); + UDEBUG("Loading local features.... done! (in %f s)", timer.ticks()); this->loadLinksQuery(nodes); - ULOGGER_DEBUG("Time load links=%fs", timer.ticks()); + ULOGGER_DEBUG("Time loading links=%fs", timer.ticks()); for(std::list::iterator iter = nodes.begin(); iter!=nodes.end(); ++iter) { @@ -3626,7 +3487,7 @@ void DBDriverSqlite3::loadSignaturesQuery(const std::list & ids, std::list< } } -void DBDriverSqlite3::loadLastNodesQuery(std::list & nodes) const +void DBDriverSqlite3::loadLastNodesQuery(std::list & nodes, bool loadWordIdsOnly) const { ULOGGER_DEBUG(""); if(_ppDb) @@ -3672,15 +3533,15 @@ void DBDriverSqlite3::loadLastNodesQuery(std::list & nodes) const UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); ULOGGER_DEBUG("Loading %d signatures...", ids.size()); - this->loadSignaturesQuery(ids, nodes); + this->loadSignaturesQuery(ids, nodes, loadWordIdsOnly); ULOGGER_DEBUG("loaded=%d, Time=%fs", nodes.size(), timer.ticks()); } } -void DBDriverSqlite3::loadQuery(VWDictionary * dictionary, bool lastStateOnly) const +void DBDriverSqlite3::loadQuery(VWDictionary & dictionary, bool lastStateOnly) const { ULOGGER_DEBUG(""); - if(_ppDb && dictionary) + if(_ppDb) { std::string type; UTimer timer; @@ -3743,11 +3604,11 @@ void DBDriverSqlite3::loadQuery(VWDictionary * dictionary, bool lastStateOnly) c memcpy(d.data, descriptor, dRealSize); VisualWord * vw = new VisualWord(id, d); vw->setSaved(true); - dictionary->addWord(vw); + dictionary.addWord(vw); if(++count % 5000 == 0) { - ULOGGER_DEBUG("Loaded %d words...", count); + //ULOGGER_DEBUG("Loaded %d words...", count); } rc = sqlite3_step(ppStmt); // next result... } @@ -3758,9 +3619,50 @@ void DBDriverSqlite3::loadQuery(VWDictionary * dictionary, bool lastStateOnly) c // Get Last word id getLastWordId(id); - dictionary->setLastWordId(id); + dictionary.setLastWordId(id); - ULOGGER_DEBUG("Time=%fs", timer.ticks()); + if(uStrNumCmp(_version, "0.23.0") >= 0) { + // load dictionary index + std::stringstream query3; + query3 << "SELECT dictionary_index " + << "FROM Admin " + << "WHERE version='" << _version.c_str() + <<"';"; + + rc = sqlite3_prepare_v2(_ppDb, query3.str().c_str(), -1, &ppStmt, 0); + UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); + + // Process the result if one + rc = sqlite3_step(ppStmt); + UASSERT_MSG(rc == SQLITE_ROW, uFormat("DB error (%s): Not found first Admin row: query=\"%s\"", _version.c_str(), query3.str().c_str()).c_str()); + if(rc == SQLITE_ROW) + { + const void * data = 0; + int dataSize = 0; + int index = 0; + + //opt_poses + data = sqlite3_column_blob(ppStmt, index); + dataSize = sqlite3_column_bytes(ppStmt, index++); + if(dataSize>4 && data) + { + UDEBUG("A flann index was saved in the database (size=%ld).", dataSize); + dictionary.deserializeIndex((const unsigned char*)data, dataSize); + } + else { + UDEBUG("No flann index was saved in the database."); + } + + rc = sqlite3_step(ppStmt); // next result... + } + UASSERT_MSG(rc == SQLITE_DONE, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); + + // Finalize (delete) the statement + rc = sqlite3_finalize(ppStmt); + UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); + } + + ULOGGER_DEBUG("Loaded %d words... time=%fs", count, timer.ticks()); } } @@ -3860,6 +3762,262 @@ void DBDriverSqlite3::loadWordsQuery(const std::set & wordIds, std::list & signatures) const +{ + if(_ppDb) + { + int rc = SQLITE_OK; + sqlite3_stmt * ppStmt = 0; + std::stringstream query; + + if(uStrNumCmp(_version, "0.13.0") >= 0) + { + query << "SELECT word_id " + "FROM Feature " + "WHERE node_id = ? "; + } + else if(uStrNumCmp(_version, "0.12.0") >= 0) + { + query << "SELECT word_id " + "FROM Map_Node_Word " + "WHERE node_id = ? "; + } + else if(uStrNumCmp(_version, "0.11.2") >= 0) + { + query << "SELECT word_id " + "FROM Map_Node_Word " + "WHERE node_id = ? "; + } + else + { + query << "SELECT word_id " + "FROM Map_Node_Word " + "WHERE node_id = ? "; + } + + query << " ORDER BY word_id"; // Needed for fast insertion below + query << ";"; + + rc = sqlite3_prepare_v2(_ppDb, query.str().c_str(), -1, &ppStmt, 0); + UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); + + for(std::list::const_iterator iter=signatures.begin(); iter!=signatures.end(); ++iter) + { + //ULOGGER_DEBUG("Loading words of %d...", (*iter)->id()); + // bind id + rc = sqlite3_bind_int(ppStmt, 1, (*iter)->id()); + UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); + + int visualWordId = 0; + std::multimap visualWords; + + // Process the result if one + rc = sqlite3_step(ppStmt); + while(rc == SQLITE_ROW) + { + int index = 0; + visualWordId = sqlite3_column_int(ppStmt, index++); + visualWords.insert(visualWords.end(), std::make_pair(visualWordId, -1)); + + rc = sqlite3_step(ppStmt); + } + UASSERT_MSG(rc == SQLITE_DONE, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); + + if(visualWords.size()==0) + { + UDEBUG("Empty signature detected! (id=%d)", (*iter)->id()); + } + else + { + (*iter)->setWords(visualWords, std::vector(), std::vector(), cv::Mat()); + //ULOGGER_DEBUG("Add %d keypoints, %d 3d points and %d descriptors to node %d", (int)visualWords.size(), allWords3NaN?0:(int)visualWords3.size(), (int)descriptors.rows, (*iter)->id()); + } + + //reset + rc = sqlite3_reset(ppStmt); + UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); + } + + // Finalize (delete) the statement + rc = sqlite3_finalize(ppStmt); + UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); + } +} + +void DBDriverSqlite3::loadWordsQuery(std::list & signatures) const +{ + if(_ppDb) + { + int rc = SQLITE_OK; + sqlite3_stmt * ppStmt = 0; + std::stringstream query; + + if(uStrNumCmp(_version, "0.13.0") >= 0) + { + query << "SELECT word_id, pos_x, pos_y, size, dir, response, octave, depth_x, depth_y, depth_z, descriptor_size, descriptor " + "FROM Feature " + "WHERE node_id = ? "; + } + else if(uStrNumCmp(_version, "0.12.0") >= 0) + { + query << "SELECT word_id, pos_x, pos_y, size, dir, response, octave, depth_x, depth_y, depth_z, descriptor_size, descriptor " + "FROM Map_Node_Word " + "WHERE node_id = ? "; + } + else if(uStrNumCmp(_version, "0.11.2") >= 0) + { + query << "SELECT word_id, pos_x, pos_y, size, dir, response, depth_x, depth_y, depth_z, descriptor_size, descriptor " + "FROM Map_Node_Word " + "WHERE node_id = ? "; + } + else + { + query << "SELECT word_id, pos_x, pos_y, size, dir, response, depth_x, depth_y, depth_z " + "FROM Map_Node_Word " + "WHERE node_id = ? "; + } + + query << " ORDER BY word_id"; // Needed for fast insertion below + query << ";"; + + rc = sqlite3_prepare_v2(_ppDb, query.str().c_str(), -1, &ppStmt, 0); + UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); + + float nanFloat = std::numeric_limits::quiet_NaN (); + + for(std::list::const_iterator iter=signatures.begin(); iter!=signatures.end(); ++iter) + { + //ULOGGER_DEBUG("Loading words of %d...", (*iter)->id()); + // bind id + rc = sqlite3_bind_int(ppStmt, 1, (*iter)->id()); + UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); + + int visualWordId = 0; + int descriptorSize = 0; + const void * descriptor = 0; + int dRealSize = 0; + cv::KeyPoint kpt; + std::multimap visualWords; + std::vector visualWordsKpts; + std::vector visualWords3; + cv::Mat descriptors; + bool allWords3NaN = true; + cv::Point3f depth(0,0,0); + + // Process the result if one + rc = sqlite3_step(ppStmt); + while(rc == SQLITE_ROW) + { + int index = 0; + visualWordId = sqlite3_column_int(ppStmt, index++); + kpt.pt.x = sqlite3_column_double(ppStmt, index++); + kpt.pt.y = sqlite3_column_double(ppStmt, index++); + kpt.size = sqlite3_column_int(ppStmt, index++); + kpt.angle = sqlite3_column_double(ppStmt, index++); + kpt.response = sqlite3_column_double(ppStmt, index++); + if(uStrNumCmp(_version, "0.12.0") >= 0) + { + kpt.octave = sqlite3_column_int(ppStmt, index++); + } + + if(sqlite3_column_type(ppStmt, index) == SQLITE_NULL) + { + depth.x = nanFloat; + ++index; + } + else + { + depth.x = sqlite3_column_double(ppStmt, index++); + } + + if(sqlite3_column_type(ppStmt, index) == SQLITE_NULL) + { + depth.y = nanFloat; + ++index; + } + else + { + depth.y = sqlite3_column_double(ppStmt, index++); + } + + if(sqlite3_column_type(ppStmt, index) == SQLITE_NULL) + { + depth.z = nanFloat; + ++index; + } + else + { + depth.z = sqlite3_column_double(ppStmt, index++); + } + + visualWordsKpts.push_back(kpt); + visualWords.insert(visualWords.end(), std::make_pair(visualWordId, visualWordsKpts.size()-1)); + visualWords3.push_back(depth); + + if(allWords3NaN && util3d::isFinite(depth)) + { + allWords3NaN = false; + } + + if(uStrNumCmp(_version, "0.11.2") >= 0) + { + descriptorSize = sqlite3_column_int(ppStmt, index++); // VisualWord descriptor size + descriptor = sqlite3_column_blob(ppStmt, index); // VisualWord descriptor array + dRealSize = sqlite3_column_bytes(ppStmt, index++); + + if(descriptor && descriptorSize>0 && dRealSize>0) + { + cv::Mat d; + if(dRealSize == descriptorSize) + { + // CV_8U binary descriptors + d = cv::Mat(1, descriptorSize, CV_8U); + } + else if(dRealSize/int(sizeof(float)) == descriptorSize) + { + // CV_32F + d = cv::Mat(1, descriptorSize, CV_32F); + } + else + { + UFATAL("Saved buffer size (%d bytes) is not the same as descriptor size (%d)", dRealSize, descriptorSize); + } + + memcpy(d.data, descriptor, dRealSize); + + descriptors.push_back(d); + } + } + + rc = sqlite3_step(ppStmt); + } + UASSERT_MSG(rc == SQLITE_DONE, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); + + if(visualWords.size()==0) + { + UDEBUG("Empty signature detected! (id=%d)", (*iter)->id()); + } + else + { + if(allWords3NaN) + { + visualWords3.clear(); + } + (*iter)->setWords(visualWords, visualWordsKpts, visualWords3, descriptors); + //ULOGGER_DEBUG("Add %d keypoints, %d 3d points and %d descriptors to node %d", (int)visualWords.size(), allWords3NaN?0:(int)visualWords3.size(), (int)descriptors.rows, (*iter)->id()); + } + + //reset + rc = sqlite3_reset(ppStmt); + UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); + } + + // Finalize (delete) the statement + rc = sqlite3_finalize(ppStmt); + UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); + } +} + void DBDriverSqlite3::loadLinksQuery( int signatureId, std::multimap & links, @@ -4174,7 +4332,7 @@ void DBDriverSqlite3::loadLinksQuery(std::list & signatures) const //reset rc = sqlite3_reset(ppStmt); UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); - UDEBUG("time=%fs, node=%d, links.size=%d", timer.ticks(), (*iter)->id(), links.size()); + //UDEBUG("time=%fs, node=%d, links.size=%d", timer.ticks(), (*iter)->id(), links.size()); } // Finalize (delete) the statement @@ -5117,8 +5275,8 @@ std::map DBDriverSqlite3::loadOptimizedPosesQuery(Transform * la Transform t(serializedPoses.at(i*12), serializedPoses.at(i*12+1), serializedPoses.at(i*12+2), serializedPoses.at(i*12+3), serializedPoses.at(i*12+4), serializedPoses.at(i*12+5), serializedPoses.at(i*12+6), serializedPoses.at(i*12+7), serializedPoses.at(i*12+8), serializedPoses.at(i*12+9), serializedPoses.at(i*12+10), serializedPoses.at(i*12+11)); - poses.insert(std::make_pair(serializedIds.at(i), t)); - UDEBUG("Optimized pose %d: %s", serializedIds.at(i), t.prettyPrint().c_str()); + poses.insert(poses.end(), std::make_pair(serializedIds.at(i), t)); + //UDEBUG("Optimized pose %d: %s", serializedIds.at(i), t.prettyPrint().c_str()); } } @@ -5591,6 +5749,47 @@ cv::Mat DBDriverSqlite3::loadOptimizedMeshQuery( return cloud; } +void DBDriverSqlite3::saveFlannIndexQuery(const std::vector & data) const +{ + UDEBUG(""); + if(_ppDb && uStrNumCmp(_version, "0.23.0") >= 0) + { + UTimer timer; + timer.start(); + int rc = SQLITE_OK; + sqlite3_stmt * ppStmt = 0; + std::string query; + + // Update table Admin + query = uFormat("UPDATE Admin SET dictionary_index=? WHERE version='%s';", _version.c_str()); + rc = sqlite3_prepare_v2(_ppDb, query.c_str(), -1, &ppStmt, 0); + UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); + + int index = 1; + + if(data.empty()) + { + rc = sqlite3_bind_null(ppStmt, index++); + UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); + } + else + { + rc = sqlite3_bind_blob(ppStmt, index++, data.data(), data.size(), SQLITE_STATIC); + UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); + } + + //execute query + rc=sqlite3_step(ppStmt); + UASSERT_MSG(rc == SQLITE_DONE, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); + + // Finalize (delete) the statement + rc = sqlite3_finalize(ppStmt); + UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); + + UDEBUG("Time=%fs", timer.ticks()); + } +} + std::string DBDriverSqlite3::queryStepNode() const { if(uStrNumCmp(_version, "0.18.0") >= 0) @@ -6598,7 +6797,7 @@ void DBDriverSqlite3::stepLink( { UFATAL(""); } - UDEBUG("Save link from %d to %d, type=%d", link.from(), link.to(), link.type()); + //UDEBUG("Save link from %d to %d, type=%d", link.from(), link.to(), link.type()); // Don't save virtual links if(link.type()==Link::kVirtualClosure) diff --git a/corelib/src/DBReader.cpp b/corelib/src/DBReader.cpp index aa4d4e44..bc5272c7 100644 --- a/corelib/src/DBReader.cpp +++ b/corelib/src/DBReader.cpp @@ -260,7 +260,7 @@ bool DBReader::init( else { Signature * s = _dbDriver->loadSignature(*_ids.begin()); - _dbDriver->loadNodeData(s); + _dbDriver->loadNodeData(*s); if( s->sensorData().imageCompressed().empty() && s->getWords().empty() && !s->sensorData().laserScanCompressed().empty()) diff --git a/corelib/src/FlannIndex.cpp b/corelib/src/FlannIndex.cpp index a5932577..cf2cf406 100644 --- a/corelib/src/FlannIndex.cpp +++ b/corelib/src/FlannIndex.cpp @@ -27,8 +27,16 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include +#include +#include +#include +#include +#ifdef WIN32 +#include +#endif #include "rtflann/flann.hpp" +#include namespace rtabmap { @@ -37,7 +45,6 @@ FlannIndex::FlannIndex(): nextIndex_(0), featuresType_(0), featuresDim_(0), - isLSH_(false), useDistanceL1_(false), rebalancingFactor_(2.0f) { @@ -49,9 +56,9 @@ FlannIndex::~FlannIndex() void FlannIndex::release() { - UDEBUG(""); if(index_) { + UDEBUG("Clearing flann index..."); if(featuresType_ == CV_8UC1) { delete (rtflann::Index >*)index_; @@ -72,12 +79,139 @@ void FlannIndex::release() } } index_ = 0; + UDEBUG("Clearing flann index... done!"); } nextIndex_ = 0; - isLSH_ = false; addedDescriptors_.clear(); removedIndexes_.clear(); - UDEBUG(""); +} + +#define FLANN_INDEX_HEADER_SIZE 12 + +std::vector FlannIndex::serializeIndex(bool computeChecksum) const { + if(index_ && !addedDescriptors_.empty()) + { +#ifdef WIN32 + UERROR("FLANN index serialization is not yet implemented on Windows. Parameter \"%s\" cannot be used.", Parameters::kKpFlannIndexSaved().c_str()); +#else + UTimer timer; + const int headerSizeBytes = sizeof(int)*FLANN_INDEX_HEADER_SIZE; + std::vector indexData(1024*1024*100 + headerSizeBytes); // Max 100 MB + FILE* indexDataPtr = fmemopen(indexData.data()+headerSizeBytes, indexData.size() - headerSizeBytes, "wb"); + long bytes_written = 0; + if (indexDataPtr) { + if(featuresType_ == CV_8UC1) + { + ((rtflann::Index >*)index_)->save(indexDataPtr); + } + else + { + if(useDistanceL1_) + { + ((rtflann::Index >*)index_)->save(indexDataPtr);; + } + else if(featuresDim_ <= 3) + { + ((rtflann::Index >*)index_)->save(indexDataPtr);; + } + else + { + ((rtflann::Index >*)index_)->save(indexDataPtr);; + } + } + bytes_written = ftell(indexDataPtr); + fclose(indexDataPtr); + } + if(bytes_written < long(indexData.size()-headerSizeBytes)) + { + //Expected data size and type + int dataRows = 0; + int dataCols = 0; + int dataType = -1; + cv::Mat dataset; + std::set removedDescriptors; + if(computeChecksum){ + removedDescriptors.insert(removedIndexes_.begin(), removedIndexes_.end()); + } + for(const auto & iter: addedDescriptors_) + { + UASSERT(!iter.second.empty()); + dataRows += iter.second.rows; + if(dataCols <= 0) { + dataCols = iter.second.cols; + } + else { + UASSERT(dataCols == iter.second.cols); + } + if(dataType < 0) { + dataType = iter.second.type(); + } + else { + UASSERT(dataType == iter.second.type()); + } + if(computeChecksum){ + if(removedDescriptors.find(iter.first) == removedDescriptors.end()) { + if(dataset.empty()) { + dataset = iter.second.clone(); + } + else { + dataset.push_back(iter.second); + } + } + else { + dataRows -= iter.second.rows; + } + } + } + if(!computeChecksum) { + for(const auto & index: removedIndexes_) + { + dataRows -= addedDescriptors_.at(index).rows; + } + } + + unsigned int crcValue = 0; + if(computeChecksum) { + boost::crc_32_type result; + result.process_bytes(dataset.data, dataset.total()*dataset.elemSize()); + crcValue = result.checksum(); + } + + indexData.resize(bytes_written+headerSizeBytes); + indexData.shrink_to_fit(); + int rebalancingFactorAsInt; + memcpy(&rebalancingFactorAsInt, &rebalancingFactor_, sizeof(rebalancingFactor_)); + int crcValueAsInt; + memcpy(&crcValueAsInt, &crcValue, sizeof(crcValue)); + int header[FLANN_INDEX_HEADER_SIZE] = { + RTABMAP_VERSION_MAJOR, RTABMAP_VERSION_MINOR, RTABMAP_VERSION_PATCH, // 0,1,2 + algorithm_, // 3, + featuresDim_, // 4, + useDistanceL1_?1:0, // 5, + rebalancingFactorAsInt, // 6, + dataRows, // 7, + dataCols, // 8, + dataType, // 9, + crcValueAsInt, // 10 + (int)bytes_written}; // 11 + UDEBUG("Header: \"%d.%d.%d\" alg=%d dim=%d L1=%d factor=%f data(%dx%d type=%d, crc=%X) %d", + header[0],header[1],header[2], + header[3], + header[4], + header[5], + rebalancingFactor_, + header[7], header[8], header[9], crcValueAsInt, + header[11]); + memcpy(indexData.data(), header, headerSizeBytes); + return indexData; + } + else { + UERROR("Target buffer too small to serialize index, aborting."); + } + UDEBUG("Flann serialization: %fs", timer.ticks()); +#endif + } + return std::vector(); } size_t FlannIndex::indexedFeatures() const @@ -139,12 +273,13 @@ size_t FlannIndex::memoryUsed() const return memoryUsage; } -void FlannIndex::buildLinearIndex( +void FlannIndex::buildIndex( + flann_algorithm_t algorithm, const cv::Mat & features, bool useDistanceL1, float rebalancingFactor) { - UDEBUG(""); + UDEBUG("algorithm=%d", (int)algorithm); this->release(); UASSERT(index_ == 0); UASSERT(features.type() == CV_32FC1 || features.type() == CV_8UC1); @@ -152,8 +287,29 @@ void FlannIndex::buildLinearIndex( featuresDim_ = features.cols; useDistanceL1_ = useDistanceL1; rebalancingFactor_ = rebalancingFactor; + algorithm_ = algorithm; - rtflann::LinearIndexParams params; + rtflann::IndexParams params; + + switch (algorithm) + { + case FLANN_INDEX_LINEAR: + params = rtflann::LinearIndexParams(); + break; + case FLANN_INDEX_KDTREE: + params = rtflann::KDTreeIndexParams(4); + break; + case FLANN_INDEX_KDTREE_SINGLE: + params = rtflann::KDTreeSingleIndexParams(10, true); + break; + case FLANN_INDEX_LSH: + UASSERT(features.type() == CV_8UC1); + params = rtflann::LshIndexParams(12, 20, 2); + break; + default: + UFATAL("The flann algorithm type %d is not supported!", (int)algorithm); + break; + } if(featuresType_ == CV_8UC1) { @@ -199,13 +355,140 @@ void FlannIndex::buildLinearIndex( UDEBUG(""); } -void FlannIndex::buildKDTreeIndex( - const cv::Mat & features, - int trees, - bool useDistanceL1, - float rebalancingFactor) +bool FlannIndex::loadIndex( + const std::vector & indexData, + flann_algorithm_t algorithm, + const cv::Mat & features, + bool useDistanceL1, + float rebalancingFactor, + std::string * error) { - UDEBUG(""); + return loadIndex( + indexData.data(), + indexData.size(), + algorithm, + features, + useDistanceL1, + rebalancingFactor), + error; +} +bool FlannIndex::loadIndex( + const unsigned char * indexData, + size_t indexDataSize, + flann_algorithm_t algorithm, + const cv::Mat & features, + bool useDistanceL1, + float rebalancingFactor, + std::string * error) +{ + UASSERT(indexData!=NULL); + if(indexDataSize == 0) { + UWARN("Trying to load empty index...."); + return false; + } + +#ifdef WIN32 + UERROR("FLANN index deserialization is not yet implemented on Windows. Index cannot be loaded from memory buffer."); + return false; +#else + + // Check if the features match the expected data from the index + size_t headerSizeBytes = sizeof(int)*FLANN_INDEX_HEADER_SIZE; + if(indexDataSize < headerSizeBytes) { + if(error) { + *error = uFormat("Wrong header size detected (%ld vs expected %ld).", indexDataSize, headerSizeBytes); + } + return false; + } + const int * header = (const int *)indexData; + + int savedAlgorithm = header[3]; + int savedDim = header[4]; + bool savedDistanceL1 = header[5]==1; + float savedRebalancingFactor; + memcpy(&savedRebalancingFactor, &header[6], sizeof(header[6])); + int savedRows = header[7]; + int savedCols = header[8]; + int savedType = header[9]; + unsigned int savedCrc; + memcpy(&savedCrc, &header[10], sizeof(header[10])); + int savedIndexSize = header[11]; + + UDEBUG("Header: \"%d.%d.%d\" alg=%d dim=%d L1=%d factor=%f data(%dx%d type=%d, crc=%X) %d", + header[0],header[1],header[2], + header[3], + header[4], + header[5], + savedRebalancingFactor, + header[7], header[8], header[9], savedCrc, + header[11]); + + if(savedAlgorithm != algorithm) { + if(error) { + *error = uFormat("Serialized flann algorithm (%d) doesn't match the expected one (%d).", savedAlgorithm, algorithm); + } + return false; + } + if(savedDim != features.cols) { + if(error) { + *error = uFormat("Serialized feature dimension (%d) doesn't match the expected one (%d).", savedDim, features.cols); + } + return false; + } + if(savedDistanceL1 != useDistanceL1) { + if(error) { + *error = uFormat("Serialized \"use distance L1\" (%s) doesn't match the expected one (%s).", savedDistanceL1?"true":"false", useDistanceL1?"true":"false"); + } + return false; + } + if(savedRebalancingFactor != rebalancingFactor) { + if(error) { + *error = uFormat("Serialized \"rebalancing factor\" (%f) doesn't match the expected one (%f).", savedRebalancingFactor, rebalancingFactor); + } + return false; + } + if(savedRows != features.rows) { + if(error) { + *error = uFormat("Serialized feature count (%d) doesn't match the expected one (%d).", savedRows, features.rows); + } + return false; + } + if(savedCols != features.cols) { + if(error) { + *error = uFormat("Serialized feature dimension (%d) doesn't match the expected one (%d).", savedCols, features.cols); + } + return false; + } + if(savedType != features.type()) { + if(error) { + *error = uFormat("Serialized feature type (%d) doesn't match the expected one (%d).", savedType, features.type()); + } + return false; + } + if(savedCrc != 0) { + // Compute checksum and compare + boost::crc_32_type result; + result.process_bytes(features.data, features.total()*features.elemSize()); + if(savedCrc != result.checksum()) { + if(error) { + *error = uFormat("Serialized feature crc (%X) doesn't match the expected one (%X).", savedCrc, result.checksum()); + } + return false; + } + } + if(savedIndexSize != int(indexDataSize - headerSizeBytes)) { + if(error) { + *error = uFormat("Serialized flann index size (%ld) doesn't match the expected one (%ld).", savedIndexSize, indexDataSize - headerSizeBytes); + } + return false; + } + if(savedIndexSize == 0) { + if(error) { + *error = "Serialized flann index is empty."; + } + return false; + } + this->release(); UASSERT(index_ == 0); UASSERT(features.type() == CV_32FC1 || features.type() == CV_8UC1); @@ -213,14 +496,39 @@ void FlannIndex::buildKDTreeIndex( featuresDim_ = features.cols; useDistanceL1_ = useDistanceL1; rebalancingFactor_ = rebalancingFactor; + algorithm_ = algorithm; - rtflann::KDTreeIndexParams params(trees); + UDEBUG("algorithm=%d", (int)algorithm); + + rtflann::IndexParams params; + + switch (algorithm) + { + case FLANN_INDEX_LINEAR: + params = rtflann::LinearIndexParams(); + break; + case FLANN_INDEX_KDTREE: + params = rtflann::KDTreeIndexParams(4); + break; + case FLANN_INDEX_KDTREE_SINGLE: + params = rtflann::KDTreeSingleIndexParams(10, true); + break; + case FLANN_INDEX_LSH: + UASSERT(features.type() == CV_8UC1); + params = rtflann::LshIndexParams(12, 20, 2); + break; + default: + UFATAL("The flann algorithm type %d is not supported!", (int)algorithm); + break; + } + + FILE* indexDataPtr = fmemopen((void*)(indexData+headerSizeBytes), indexDataSize - headerSizeBytes, "r"); if(featuresType_ == CV_8UC1) { rtflann::Matrix dataset(features.data, features.rows, features.cols); index_ = new rtflann::Index >(dataset, params); - ((rtflann::Index >*)index_)->buildIndex(); + ((rtflann::Index >*)index_)->load_saved_index(indexDataPtr); } else { @@ -228,22 +536,24 @@ void FlannIndex::buildKDTreeIndex( if(useDistanceL1_) { index_ = new rtflann::Index >(dataset, params); - ((rtflann::Index >*)index_)->buildIndex(); + ((rtflann::Index >*)index_)->load_saved_index(indexDataPtr); } else if(featuresDim_ <=3) { index_ = new rtflann::Index >(dataset, params); - ((rtflann::Index >*)index_)->buildIndex(); + ((rtflann::Index >*)index_)->load_saved_index(indexDataPtr); } else { index_ = new rtflann::Index >(dataset, params); - ((rtflann::Index >*)index_)->buildIndex(); + ((rtflann::Index >*)index_)->load_saved_index(indexDataPtr); } } + fclose(indexDataPtr); // incremental FLANN: we should add all headers separately in case we remove // some indexes (to keep underlying matrix data allocated) + if(rebalancingFactor_ > 1.0f) { for(int i=0; irelease(); - UASSERT(index_ == 0); - UASSERT(features.type() == CV_32FC1 || features.type() == CV_8UC1); - featuresType_ = features.type(); - featuresDim_ = features.cols; - useDistanceL1_ = useDistanceL1; - rebalancingFactor_ = rebalancingFactor; - - rtflann::KDTreeSingleIndexParams params(leafMaxSize, reorder); - - if(featuresType_ == CV_8UC1) - { - rtflann::Matrix dataset(features.data, features.rows, features.cols); - index_ = new rtflann::Index >(dataset, params); - ((rtflann::Index >*)index_)->buildIndex(); - } - else - { - rtflann::Matrix dataset((float*)features.data, features.rows, features.cols); - if(useDistanceL1_) - { - index_ = new rtflann::Index >(dataset, params); - ((rtflann::Index >*)index_)->buildIndex(); - } - else if(featuresDim_ <=3) - { - index_ = new rtflann::Index >(dataset, params); - ((rtflann::Index >*)index_)->buildIndex(); - } - else - { - index_ = new rtflann::Index >(dataset, params); - ((rtflann::Index >*)index_)->buildIndex(); - } - } - - // incremental FLANN: we should add all headers separately in case we remove - // some indexes (to keep underlying matrix data allocated) - if(rebalancingFactor_ > 1.0f) - { - for(int i=0; irelease(); - UASSERT(index_ == 0); - UASSERT(features.type() == CV_8UC1); - featuresType_ = features.type(); - featuresDim_ = features.cols; - useDistanceL1_ = true; - rebalancingFactor_ = rebalancingFactor; - - rtflann::Matrix dataset(features.data, features.rows, features.cols); - index_ = new rtflann::Index >(dataset, rtflann::LshIndexParams(12, 20, 2)); - ((rtflann::Index >*)index_)->buildIndex(); - - // incremental FLANN: we should add all headers separately in case we remove - // some indexes (to keep underlying matrix data allocated) - if(rebalancingFactor_ > 1.0f) - { - for(int i=0; i & poses) +bool GlobalMap::fullUpdateNeeded(const std::map & poses) const { - UDEBUG("Update (poses=%d addedNodes_=%d)", (int)poses.size(), (int)addedNodes_.size()); - - // First, check of the graph has changed. If so, re-create the octree by moving all occupied nodes. bool graphOptimized = false; // If a loop closure happened (e.g., poses are modified) bool graphChanged = addedNodes_.size()>0; // If the new map doesn't have any node from the previous map float updateErrorSqrd = updateError_*updateError_; - for(std::map::iterator iter=addedNodes_.begin(); iter!=addedNodes_.end(); ++iter) + for(std::map::const_iterator iter=addedNodes_.begin(); iter!=addedNodes_.end(); ++iter) { std::map::const_iterator jter = poses.find(iter->first); if(jter != poses.end()) @@ -125,7 +122,15 @@ bool GlobalMap::update(const std::map & poses) } } - if(graphOptimized || graphChanged) + return graphOptimized || graphChanged; +} + +bool GlobalMap::update(const std::map & poses) +{ + UDEBUG("Update (poses=%d addedNodes_=%d)", (int)poses.size(), (int)addedNodes_.size()); + + // First, check of the graph has changed. If so, re-create the octree by moving all occupied nodes. + if(fullUpdateNeeded(poses)) { // clear all but keep cache clear(); diff --git a/corelib/src/LocalGrid.cpp b/corelib/src/LocalGrid.cpp index 77e98781..22dbb920 100644 --- a/corelib/src/LocalGrid.cpp +++ b/corelib/src/LocalGrid.cpp @@ -64,8 +64,8 @@ void LocalGridCache::add(int nodeId, void LocalGridCache::add(int nodeId, const LocalGrid & localGrid) { - UDEBUG("nodeId=%d (ground=%d/%d obstacles=%d/%d empty=%d/%d)", - nodeId, localGrid.groundCells.cols, localGrid.groundCells.channels(), localGrid.obstacleCells.cols, localGrid.obstacleCells.channels(), localGrid.emptyCells.cols, localGrid.emptyCells.channels()); + //UDEBUG("nodeId=%d (ground=%d/%d obstacles=%d/%d empty=%d/%d)", + // nodeId, localGrid.groundCells.cols, localGrid.groundCells.channels(), localGrid.obstacleCells.cols, localGrid.obstacleCells.channels(), localGrid.emptyCells.cols, localGrid.emptyCells.channels()); if(nodeId < 0) { UWARN("Cannot add nodes with negative id (nodeId=%d)", nodeId); diff --git a/corelib/src/Memory.cpp b/corelib/src/Memory.cpp index 05563120..27721c28 100644 --- a/corelib/src/Memory.cpp +++ b/corelib/src/Memory.cpp @@ -76,6 +76,7 @@ Memory::Memory(const ParametersMap & parameters) : _similarityThreshold(Parameters::defaultMemRehearsalSimilarity()), _binDataKept(Parameters::defaultMemBinDataKept()), _rawDescriptorsKept(Parameters::defaultMemRawDescriptorsKept()), + _loadVisualLocalFeaturesOnInit(Parameters::defaultMemLoadVisualLocalFeaturesOnInit()), _saveDepth16Format(Parameters::defaultMemSaveDepth16Format()), _notLinkedNodesKeptInDb(Parameters::defaultMemNotLinkedNodesKept()), _saveIntermediateNodeData(Parameters::defaultMemIntermediateNodeDataKept()), @@ -83,6 +84,7 @@ Memory::Memory(const ParametersMap & parameters) : _depthCompressionFormat(Parameters::defaultMemDepthCompressionFormat()), _incrementalMemory(Parameters::defaultMemIncrementalMemory()), _localizationDataSaved(Parameters::defaultMemLocalizationDataSaved()), + _flannIndexSaved(Parameters::defaultKpFlannIndexSaved()), _reduceGraph(Parameters::defaultMemReduceGraph()), _maxStMemSize(Parameters::defaultMemSTMSize()), _recentWmRatio(Parameters::defaultMemRecentWmRatio()), @@ -245,13 +247,13 @@ void Memory::loadDataFromDb(bool postInitClosingEvents) if(postInitClosingEvents) UEventsManager::post(new RtabmapEventInit(std::string("Loading all nodes to WM..."))); std::set ids; _dbDriver->getAllNodeIds(ids, true); - _dbDriver->loadSignatures(std::list(ids.begin(), ids.end()), dbSignatures); + _dbDriver->loadSignatures(std::list(ids.begin(), ids.end()), dbSignatures, 0, !_loadVisualLocalFeaturesOnInit); } else { // load previous session working memory if(postInitClosingEvents) UEventsManager::post(new RtabmapEventInit(std::string("Loading last nodes to WM..."))); - _dbDriver->loadLastNodes(dbSignatures); + _dbDriver->loadLastNodes(dbSignatures, !_loadVisualLocalFeaturesOnInit); } for(std::list::reverse_iterator iter=dbSignatures.rbegin(); iter!=dbSignatures.rend(); ++iter) { @@ -417,20 +419,22 @@ void Memory::loadDataFromDb(bool postInitClosingEvents) } else { - _dbDriver->load(_vwd, false); + _dbDriver->load(*_vwd, false); } } else { UDEBUG("load words"); // load the last dictionary - _dbDriver->load(_vwd, _vwd->isIncremental()); + _dbDriver->load(*_vwd, _vwd->isIncremental()); } UDEBUG("%d words loaded!", _vwd->getUnusedWordsSize()); _vwd->update(); if(postInitClosingEvents) UEventsManager::post(new RtabmapEventInit(uFormat("Loading dictionary, done! (%d words)", (int)_vwd->getUnusedWordsSize()))); if(postInitClosingEvents) UEventsManager::post(new RtabmapEventInit(std::string("Adding word references..."))); + UDEBUG("Adding word references..."); + UTimer timer; // Enable loaded signatures const std::map & signatures = this->getSignatures(); for(std::map::const_iterator i=signatures.begin(); i!=signatures.end(); ++i) @@ -441,7 +445,7 @@ void Memory::loadDataFromDb(bool postInitClosingEvents) const std::multimap & words = s->getWords(); if(words.size()) { - UDEBUG("node=%d, word references=%d", s->id(), words.size()); + //UDEBUG("node=%d, word references=%d", s->id(), words.size()); for(std::multimap::const_iterator iter = words.begin(); iter!=words.end(); ++iter) { if(iter->first > 0) @@ -458,7 +462,7 @@ void Memory::loadDataFromDb(bool postInitClosingEvents) { UWARN("_vwd->getUnusedWordsSize() must be empty... size=%d", _vwd->getUnusedWordsSize()); } - UDEBUG("Total word references added = %d", _vwd->getTotalActiveReferences()); + UDEBUG("Total word references added = %d (in %f s)", _vwd->getTotalActiveReferences(), timer.ticks()); if(_lastSignature == 0) { @@ -482,6 +486,37 @@ void Memory::loadDataFromDb(bool postInitClosingEvents) UDEBUG("map ids start with %d", _idMapCount); } +void Memory::saveFlannIndex(bool postInitClosingEvents) +{ + if(!_dbDriver) { + return; + } + if(uStrNumCmp(_dbDriver->getDatabaseVersion(), "0.23.0") >= 0) { + if(_flannIndexSaved && !_incrementalMemory) { + if(_vwd->isModified()) { + UINFO("Saving flann index to database... (%s=true)", Parameters::kKpFlannIndexSaved().c_str()); + if(postInitClosingEvents) UEventsManager::post(new RtabmapEventInit("Saving flann index to database...")); + _dbDriver->saveFlannIndex(_vwd->serializeIndex()); + if(postInitClosingEvents) UEventsManager::post(new RtabmapEventInit("Saving flann index to database, done!")); + } + else + { + UDEBUG("The dictionary didn't change since loaded, do not need to save again to database."); + } + } + else { + // clear if exists + _dbDriver->saveFlannIndex(std::vector()); + } + } + else if(_flannIndexSaved) + { + UWARN("Parameter %s is enabled, but database version is too old (%s < 0.23). Flann index cannot be saved.", + Parameters::kKpFlannIndexSaved().c_str(), + _dbDriver->getDatabaseVersion().c_str()); + } +} + void Memory::close(bool databaseSaved, bool postInitClosingEvents, const std::string & ouputDatabasePath) { UINFO("databaseSaved=%d, postInitClosingEvents=%d", databaseSaved?1:0, postInitClosingEvents?1:0); @@ -493,6 +528,8 @@ void Memory::close(bool databaseSaved, bool postInitClosingEvents, const std::st databaseNameChanged = ouputDatabasePath.size() && _dbDriver->getUrl().size() && _dbDriver->getUrl().compare(ouputDatabasePath) != 0?true:false; } + UDEBUG("_memoryChanged=%d _linksChanged=%d databaseNameChanged=%d", _memoryChanged?1:0, _linksChanged?1:0, databaseNameChanged?1:0); + if(!databaseSaved || (!_memoryChanged && !_linksChanged && !databaseNameChanged)) { if(postInitClosingEvents) UEventsManager::post(new RtabmapEventInit(uFormat("No changes added to database."))); @@ -500,6 +537,7 @@ void Memory::close(bool databaseSaved, bool postInitClosingEvents, const std::st UINFO("No changes added to database."); if(_dbDriver) { + saveFlannIndex(postInitClosingEvents); if(postInitClosingEvents) UEventsManager::post(new RtabmapEventInit(uFormat("Closing database \"%s\"...", _dbDriver->getUrl().c_str()))); _dbDriver->closeConnection(false, ouputDatabasePath); delete _dbDriver; @@ -514,11 +552,15 @@ void Memory::close(bool databaseSaved, bool postInitClosingEvents, const std::st { UINFO("Saving memory..."); if(postInitClosingEvents) UEventsManager::post(new RtabmapEventInit("Saving memory...")); - if(!_memoryChanged && _linksChanged && _dbDriver) + if(!_memoryChanged && _dbDriver) { - // don't update the time stamps! - UDEBUG(""); - _dbDriver->setTimestampUpdateEnabled(false); + saveFlannIndex(postInitClosingEvents); + + if(_linksChanged) { + // don't update the time stamps! + UDEBUG(""); + _dbDriver->setTimestampUpdateEnabled(false); + } } this->clear(); if(_dbDriver) @@ -565,6 +607,7 @@ void Memory::parseParameters(const ParametersMap & parameters) Parameters::parse(params, Parameters::kMemBinDataKept(), _binDataKept); Parameters::parse(params, Parameters::kMemRawDescriptorsKept(), _rawDescriptorsKept); + Parameters::parse(params, Parameters::kMemLoadVisualLocalFeaturesOnInit(), _loadVisualLocalFeaturesOnInit); Parameters::parse(params, Parameters::kMemSaveDepth16Format(), _saveDepth16Format); Parameters::parse(params, Parameters::kMemReduceGraph(), _reduceGraph); Parameters::parse(params, Parameters::kMemNotLinkedNodesKept(), _notLinkedNodesKeptInDb); @@ -618,6 +661,7 @@ void Memory::parseParameters(const ParametersMap & parameters) Parameters::parse(params, Parameters::kMarkerVarianceAngular(), _markerAngVariance); Parameters::parse(params, Parameters::kMarkerVarianceOrientationIgnored(), _markerOrientationIgnored); Parameters::parse(params, Parameters::kMemLocalizationDataSaved(), _localizationDataSaved); + Parameters::parse(params, Parameters::kKpFlannIndexSaved(), _flannIndexSaved); if(_markerAngVariance>=9999) { @@ -654,10 +698,7 @@ void Memory::parseParameters(const ParametersMap & parameters) } // Keypoint stuff - if(_vwd) - { - _vwd->parseParameters(params); - } + _vwd->parseParameters(params); Parameters::parse(params, Parameters::kKpTfIdfLikelihoodUsed(), _tfIdfLikelihoodUsed); Parameters::parse(params, Parameters::kKpParallelized(), _parallelized); @@ -856,7 +897,7 @@ void Memory::preUpdate() { this->cleanUnusedWords(); } - if(_vwd && !_parallelized) + if(!_parallelized) { //When parallelized, it is done in CreateSignature _vwd->update(); @@ -1114,10 +1155,7 @@ void Memory::addSignatureToStm(Signature * signature, const cv::Mat & covariance } ++_signaturesAdded; - if(_vwd) - { - UDEBUG("%d words ref for the signature %d (weight=%d)", signature->getWords().size(), signature->id(), signature->getWeight()); - } + UDEBUG("%d words ref for the signature %d (weight=%d)", signature->getWords().size(), signature->id(), signature->getWeight()); if(signature->getWords().size()) { signature->setEnabled(true); @@ -1836,13 +1874,24 @@ void Memory::clear() UDEBUG(""); //Get the tree root (parents) - std::map mem = _signatures; - for(std::map::iterator i=mem.begin(); i!=mem.end(); ++i) - { - if(i->second) + if(!_dbDriver) { + // We are not saving to database anyway, just delete now. + for(std::map::iterator iter=_signatures.begin(); iter!=_signatures.end(); ++iter) { - UDEBUG("deleting from the working and the short-term memory: %d", i->first); - this->moveToTrash(i->second); + delete iter->second; + } + _workingMem.clear(); + _signatures.clear(); + } + else { + std::map mem = _signatures; + for(std::map::iterator i=mem.begin(); i!=mem.end(); ++i) + { + if(i->second) + { + //UDEBUG("deleting from the working and the short-term memory: %d", i->first); + this->moveToTrash(i->second); + } } } @@ -1866,6 +1915,7 @@ void Memory::clear() UDEBUG(""); _lastSignature = 0; _lastGlobalLoopClosureId = 0; + _signaturesAdded = 0; _idCount = kIdStart; _idMapCount = kIdStart; _memoryChanged = false; @@ -1886,14 +1936,7 @@ void Memory::clear() cleanUnusedWords(); _dbDriver->emptyTrashes(); } - else - { - cleanUnusedWords(); - } - if(_vwd) - { - _vwd->clear(); - } + _vwd->clear(_dbDriver!=NULL); UDEBUG(""); } @@ -2428,7 +2471,7 @@ std::list Memory::getRemovableSignatures(int count, const std::set< */ void Memory::moveToTrash(Signature * s, bool keepLinkedToGraph, std::list * deletedWords) { - UDEBUG("id=%d", s?s->id():0); + //UDEBUG("id=%d", s?s->id():0); if(s) { // Cleanup landmark indexes @@ -2921,6 +2964,37 @@ Transform Memory::computeTransform( _registrationPipeline->isScanRequired()?&laserBuf:0, _registrationPipeline->isUserDataRequired()?&userBuf:0); + // Load word descriptors and keypoints on-demand if necessary + if( !_reextractLoopClosureFeatures && + (_registrationPipeline->isImageRequired() || guess.isNull()) && + !fromS.getWords().empty() && fromS.getWordsKpts().empty() && + _dbDriver) + { + // We assume "toS" has already features in RAM, so just lookup "fromS" + UDEBUG("Loading local visual features for signature %d", fromS.id()); + std::multimap words; + std::vector keypoints; + std::vector points; + cv::Mat descriptors; + UTimer timer; + _dbDriver->getLocalFeatures(fromS.id(), words, keypoints, points, descriptors); + if(!words.empty() && !keypoints.empty()) { + UASSERT(words.size() == fromS.getWords().size()); + std::map wordsChanged = fromS.getWordsChanged(); + bool wasEnabled = fromS.isEnabled(); + fromS.setWords(words, keypoints, points, descriptors); + for(const auto & iter: wordsChanged) { + fromS.changeWordsRef(iter.first, iter.second); + } + fromS.setEnabled(wasEnabled); + UDEBUG("Loaded %ld local visual features for signature %d! (in %f s)", words.size(), fromS.id(), timer.ticks()); + } + else + { + UDEBUG("Failed to load local visual features for signature %d.", fromS.id()); + } + + } // compute transform fromId -> toId std::vector inliersV; @@ -2980,8 +3054,10 @@ Transform Memory::computeTransform( !_invertedReg && !tmpTo.getWordsDescriptors().empty() && !tmpTo.getWords().empty() && + !tmpTo.getWordsKpts().empty() && !tmpFrom.getWordsDescriptors().empty() && !tmpFrom.getWords().empty() && + !tmpFrom.getWordsKpts().empty() && !tmpFrom.getWords3().empty() && fromS.hasLink(0, Link::kNeighbor)) // If doesn't have neighbors, skip bundle { @@ -3015,8 +3091,12 @@ Transform Memory::computeTransform( if(id != fromS.id() && iter->second.type() == Link::kNeighbor) // assemble only neighbors for the local feature map { const Signature * s = this->getSignature(id); - if(s && !s->getWords3().empty()) + if(s) { + if(s->getWordsKpts().empty() && s->getWords3().empty() && s->getWordsDescriptors().empty()) { + UDEBUG("Signature %d doesn't have features set. Cannot be added in the local feature map.", s->id()); + continue; + } const std::map & wordsTo = uMultimapToMapUnique(s->getWords()); for(std::map::const_iterator jter=wordsTo.begin(); jter!=wordsTo.end(); ++jter) { @@ -3115,6 +3195,11 @@ Transform Memory::computeTransform( bundlePoses.insert(std::make_pair(id, iter->second.transform())); } + if(s->getWordsKpts().empty()) + { + UDEBUG("Signature %d doesn't have features set. Keypoints won't be added in local bundle adjustment.", s->id()); + continue; + } const std::map & words = uMultimapToMapUnique(s->getWords()); for(std::map::const_iterator jter=words.begin(); jter!=words.end(); ++jter) { @@ -3590,7 +3675,7 @@ void Memory::removeAllVirtualLinks() void Memory::removeVirtualLinks(int signatureId) { - UDEBUG(""); + //UDEBUG(""); Signature * s = this->_getSignature(signatureId); if(s) { @@ -3629,10 +3714,7 @@ void Memory::dumpMemory(std::string directory) const void Memory::dumpDictionary(const char * fileNameRef, const char * fileNameDesc) const { - if(_vwd) - { - _vwd->exportDictionary(fileNameRef, fileNameDesc); - } + _vwd->exportDictionary(fileNameRef, fileNameDesc); } void Memory::dumpSignatures(const char * fileNameSign, bool words3D) const @@ -3753,10 +3835,7 @@ unsigned long Memory::getMemoryUsed() const { memoryUsage += iter->second->getMemoryUsed(true); } - if(_vwd) - { - memoryUsage += _vwd->getMemoryUsed(); - } + memoryUsage += _vwd->getMemoryUsed(); memoryUsage += _stMem.size() * (sizeof(int)+sizeof(std::set::iterator)) + sizeof(std::set); memoryUsage += _workingMem.size() * (sizeof(int)+sizeof(double)+sizeof(std::map::iterator)) + sizeof(std::map); memoryUsage += _groundTruths.size() * (sizeof(int)+sizeof(Transform)+12*sizeof(float) + sizeof(std::map::iterator)) + sizeof(std::map); @@ -4221,6 +4300,11 @@ void Memory::getNodeWordsAndGlobalDescriptors(int nodeId, } } } + if(!words.empty() && wordsKpts.empty() && _dbDriver) + { + std::multimap tmpWords; + _dbDriver->getLocalFeatures(nodeId, tmpWords, wordsKpts, words3, wordsDescriptors); + } } void Memory::getNodeCalibration(int nodeId, @@ -6330,7 +6414,7 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor void Memory::disableWordsRef(int signatureId) { - UDEBUG("id=%d", signatureId); + //UDEBUG("id=%d", signatureId); Signature * ss = this->_getSignature(signatureId); if(ss && ss->isEnabled()) @@ -6346,7 +6430,7 @@ void Memory::disableWordsRef(int signatureId) count -= _vwd->getTotalActiveReferences(); ss->setEnabled(false); - UDEBUG("%d words total ref removed from signature %d... (total active ref = %d)", count, ss->id(), _vwd->getTotalActiveReferences()); + //UDEBUG("%d words total ref removed from signature %d... (total active ref = %d)", count, ss->id(), _vwd->getTotalActiveReferences()); } } @@ -6409,7 +6493,7 @@ void Memory::enableWordsRef(const std::list & signatureIds) UDEBUG("oldWordIds.size()=%d, getOldIds time=%fs", oldWordIds.size(), timer.ticks()); - // the words were deleted, so try to math it with an active word + // the words were deleted, so try to match it with an active word std::list vws; if(oldWordIds.size() && _dbDriver) { diff --git a/corelib/src/Optimizer.cpp b/corelib/src/Optimizer.cpp index f0b563ab..96a070fa 100644 --- a/corelib/src/Optimizer.cpp +++ b/corelib/src/Optimizer.cpp @@ -608,8 +608,8 @@ void Optimizer::computeBACorrespondences( } } - if(sFrom.getWords().size() && - sTo.getWords().size() && + if(sFrom.getWordsKpts().size() && + sTo.getWordsKpts().size() && sFrom.getWords3().size()) { if(!rematchFeatures) diff --git a/corelib/src/RegistrationVis.cpp b/corelib/src/RegistrationVis.cpp index 84f01016..3695980b 100644 --- a/corelib/src/RegistrationVis.cpp +++ b/corelib/src/RegistrationVis.cpp @@ -375,6 +375,9 @@ Transform RegistrationVis::computeTransformationImpl( { UDEBUG(""); // just some checks to make sure that input data are ok + UASSERT(fromSignature.getWords().empty() || + fromSignature.getWordsKpts().empty() || + (fromSignature.getWords().size() == fromSignature.getWordsKpts().size())); UASSERT(fromSignature.getWords().empty() || fromSignature.getWords3().empty() || (fromSignature.getWords().size() == fromSignature.getWords3().size())); @@ -382,8 +385,11 @@ Transform RegistrationVis::computeTransformationImpl( (int)fromSignature.getWords().size() == fromSignature.getWordsDescriptors().rows || fromSignature.sensorData().descriptors().empty() || fromSignature.getWordsDescriptors().empty() == 0); - UASSERT((toSignature.getWords().empty() && toSignature.getWords3().empty())|| - (toSignature.getWords().size() && toSignature.getWords3().empty())|| + UASSERT(toSignature.getWords().empty() || + toSignature.getWordsKpts().empty() || + (toSignature.getWords().size() == toSignature.getWordsKpts().size())); + UASSERT(toSignature.getWords().empty() || + toSignature.getWords3().empty() || (toSignature.getWords().size() == toSignature.getWords3().size())); UASSERT((int)toSignature.sensorData().keypoints().size() == toSignature.sensorData().descriptors().rows || (int)toSignature.getWords().size() == toSignature.getWordsDescriptors().rows || diff --git a/corelib/src/SensorData.cpp b/corelib/src/SensorData.cpp index f616df23..807af7ca 100644 --- a/corelib/src/SensorData.cpp +++ b/corelib/src/SensorData.cpp @@ -548,7 +548,7 @@ void SensorData::setOccupancyGrid( float cellSize, const cv::Point3f & viewPoint) { - UDEBUG("ground=%d obstacles=%d empty=%d", ground.cols, obstacles.cols, empty.cols); + //UDEBUG("ground=%d obstacles=%d empty=%d", ground.cols, obstacles.cols, empty.cols); if((!ground.empty() && (!_groundCellsCompressed.empty() || !_groundCellsRaw.empty())) || (!obstacles.empty() && (!_obstacleCellsCompressed.empty() || !_obstacleCellsRaw.empty())) || (!empty.empty() && (!_emptyCellsCompressed.empty() || !_emptyCellsRaw.empty()))) @@ -649,7 +649,7 @@ void SensorData::uncompressData( cv::Mat * emptyCellsRaw, cv::Mat * depthConfidenceRaw) { - UDEBUG("%d data(%d,%d,%d,%d,%d,%d,%d,%d)", + /*UDEBUG("%d data(%d,%d,%d,%d,%d,%d,%d,%d)", this->id(), imageRaw?1:0, depthRaw?1:0, @@ -658,7 +658,7 @@ void SensorData::uncompressData( groundCellsRaw?1:0, obstacleCellsRaw?1:0, emptyCellsRaw?1:0, - depthConfidenceRaw?1:0); + depthConfidenceRaw?1:0);*/ if(imageRaw == 0 && depthRaw == 0 && laserScanRaw == 0 && diff --git a/corelib/src/Signature.cpp b/corelib/src/Signature.cpp index 32fca300..1c1ea27a 100644 --- a/corelib/src/Signature.cpp +++ b/corelib/src/Signature.cpp @@ -118,7 +118,7 @@ void Signature::addLinks(const std::map & links) } void Signature::addLink(const Link & link) { - UDEBUG("Add link %d to %d (type=%d/%s var=%f,%f)", link.to(), this->id(), (int)link.type(), link.typeName().c_str(), link.transVariance(), link.rotVariance()); + //UDEBUG("Add link %d to %d (type=%d/%s var=%f,%f)", link.to(), this->id(), (int)link.type(), link.typeName().c_str(), link.transVariance(), link.rotVariance()); UASSERT_MSG(link.from() == this->id(), uFormat("%d->%d for signature %d (type=%d)", link.from(), link.to(), this->id(), link.type()).c_str()); UASSERT_MSG((link.to() != this->id()) || link.type()==Link::kPosePrior || link.type()==Link::kGravity, uFormat("%d->%d for signature %d (type=%d)", link.from(), link.to(), this->id(), link.type()).c_str()); UASSERT_MSG(link.to() == this->id() || _links.find(link.to()) == _links.end(), uFormat("Link %d (type=%d) already added to signature %d!", link.to(), link.type(), this->id()).c_str()); @@ -318,7 +318,7 @@ void Signature::setWords(const std::multimap & words, UASSERT_MSG(descriptors.empty() || descriptors.rows == (int)words.size(), uFormat("words=%d, descriptors=%d", (int)words.size(), descriptors.rows).c_str()); UASSERT_MSG(points.empty() || points.size() == words.size(), uFormat("words=%d, points=%d", (int)words.size(), (int)points.size()).c_str()); UASSERT_MSG(keypoints.empty() || keypoints.size() == words.size(), uFormat("words=%d, descriptors=%d", (int)words.size(), (int)keypoints.size()).c_str()); - UASSERT(words.empty() || !keypoints.empty() || !points.empty() || !descriptors.empty()); + //UASSERT(words.empty() || !keypoints.empty() || !points.empty() || !descriptors.empty()); _invalidWordsCount = 0; for(std::multimap::const_iterator iter=words.begin(); iter!=words.end(); ++iter) @@ -328,7 +328,7 @@ void Signature::setWords(const std::multimap & words, ++_invalidWordsCount; } // make sure indexes are all valid! - UASSERT_MSG(iter->second >=0 && iter->second < (int)words.size(), uFormat("iter->second=%d words.size()=%d", iter->second, (int)words.size()).c_str()); + UASSERT_MSG(iter->second<0 || iter->second < (int)words.size(), uFormat("iter->second=%d words.size()=%d", iter->second, (int)words.size()).c_str()); } _enabled = false; diff --git a/corelib/src/VWDictionary.cpp b/corelib/src/VWDictionary.cpp index fc1bb172..5193f4c5 100644 --- a/corelib/src/VWDictionary.cpp +++ b/corelib/src/VWDictionary.cpp @@ -51,7 +51,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include -#define KDTREE_SIZE 4 #define KNN_CHECKS 32 namespace rtabmap @@ -69,9 +68,11 @@ VWDictionary::VWDictionary(const ParametersMap & parameters) : _nndrRatio(Parameters::defaultKpNndrRatio()), _newDictionaryPath(Parameters::defaultKpDictionaryPath()), _newWordsComparedTogether(Parameters::defaultKpNewWordsComparedTogether()), + _serializeWithChecksum(Parameters::defaultKpSerializeWithChecksum()), _lastWordId(0), useDistanceL1_(false), _flannIndex(new FlannIndex()), + _modified(true), _strategy(kNNBruteForce) { this->setNNStrategy((NNStrategy)Parameters::defaultKpNNStrategy()); @@ -89,6 +90,7 @@ void VWDictionary::parseParameters(const ParametersMap & parameters) ParametersMap::const_iterator iter; Parameters::parse(parameters, Parameters::kKpNndrRatio(), _nndrRatio); Parameters::parse(parameters, Parameters::kKpNewWordsComparedTogether(), _newWordsComparedTogether); + Parameters::parse(parameters, Parameters::kKpSerializeWithChecksum(), _serializeWithChecksum); Parameters::parse(parameters, Parameters::kKpIncrementalFlann(), _incrementalFlann); Parameters::parse(parameters, Parameters::kKpFlannRebalancingFactor(), _rebalancingFactor); bool byteToFloat = _byteToFloat; @@ -160,7 +162,7 @@ void VWDictionary::setFixedDictionary(const std::string & dictionaryPath) DBDriver * driver = DBDriver::create(); if(driver->openConnection(dictionaryPath, false)) { - driver->load(this, false); + driver->load(*this, false); for(std::map::iterator iter=_visualWords.begin(); iter!=_visualWords.end(); ++iter) { iter->second->setSaved(true); @@ -289,6 +291,11 @@ void VWDictionary::setFixedDictionary(const std::string & dictionaryPath) _newDictionaryPath = dictionaryPath; } +bool VWDictionary::isModified() const +{ + return _modified; +} + bool VWDictionary::setNNStrategy(NNStrategy strategy) { #if CV_MAJOR_VERSION < 3 @@ -484,7 +491,13 @@ void VWDictionary::update() if(_notIndexedWords.size() || _visualWords.size() == 0 || _removedIndexedWords.size()) { - if(_incrementalFlann && + _modified = true; + bool firstUpdate = _removedIndexedWords.empty() && _visualWords.size() == _notIndexedWords.size(); + UDEBUG("firstUpdate=%s (_removedIndexedWords=%ld, _visualWords=%ld, _notIndexedWords=%ld)", + firstUpdate?"true":"false", _removedIndexedWords.size(), _visualWords.size(), _notIndexedWords.size()); + + if(!firstUpdate && + _incrementalFlann && _strategy < kNNBruteForce && _visualWords.size()) { @@ -501,7 +514,9 @@ void VWDictionary::update() if(_notIndexedWords.size()) { - ULOGGER_DEBUG("Incremental FLANN: Inserting %d words...", (int)_notIndexedWords.size()); + UTimer timer; + timer.start(); + ULOGGER_DEBUG("Incremental FLANN: Inserting %d words...", (int)_notIndexedWords.size(), _byteToFloat?"true":"false"); for(std::set::iterator iter=_notIndexedWords.begin(); iter!=_notIndexedWords.end(); ++iter) { VisualWord* w = uValue(_visualWords, *iter, (VisualWord*)0); @@ -528,24 +543,13 @@ void VWDictionary::update() int index = 0; if(!_flannIndex->isBuilt()) { - UDEBUG("Building FLANN index..."); - switch(_strategy) - { - case kNNFlannNaive: - _flannIndex->buildLinearIndex(descriptor, useDistanceL1_, _rebalancingFactor); - break; - case kNNFlannKdTree: - UASSERT_MSG(descriptor.type() == CV_32F, "To use KdTree dictionary, float descriptors are required!"); - _flannIndex->buildKDTreeIndex(descriptor, KDTREE_SIZE, useDistanceL1_, _rebalancingFactor); - break; - case kNNFlannLSH: - UASSERT_MSG(descriptor.type() == CV_8U, "To use LSH dictionary, binary descriptors are required!"); - _flannIndex->buildLSHIndex(descriptor, 12, 20, 2, _rebalancingFactor); - break; - default: - UFATAL("Not supposed to be here!"); - break; - } + UDEBUG("Building FLANN index... (strategy=%s, byteToFloat=%s, useDistanceL1=%s, rebalancingFactor=%f)", + nnStrategyName(_strategy).c_str(), _byteToFloat?"true":"false", useDistanceL1_?"true":"false", _rebalancingFactor); + _flannIndex->buildIndex( + _strategy == kNNFlannNaive ? FlannIndex::FLANN_INDEX_LINEAR: + _strategy == kNNFlannLSH ? FlannIndex::FLANN_INDEX_LSH: + FlannIndex::FLANN_INDEX_KDTREE, // kNNFlannKdTree + descriptor, useDistanceL1_, _rebalancingFactor); UDEBUG("Building FLANN index... done!"); } else @@ -561,7 +565,7 @@ void VWDictionary::update() inserted = _mapIdIndex.insert(std::pair(w->id(), index)); UASSERT(inserted.second); } - ULOGGER_DEBUG("Incremental FLANN: Inserting %d words... done!", (int)_notIndexedWords.size()); + ULOGGER_DEBUG("Incremental FLANN: Inserting %d words... done! (in %f s)", (int)_notIndexedWords.size(), timer.ticks()); } } else if(_strategy >= kNNBruteForce && @@ -657,23 +661,13 @@ void VWDictionary::update() ULOGGER_DEBUG("_mapIndexId.size() = %d, words.size()=%d, _dim=%d",_mapIndexId.size(), _visualWords.size(), dim); ULOGGER_DEBUG("copying data = %f s", timer.ticks()); - switch(_strategy) - { - case kNNFlannNaive: - _flannIndex->buildLinearIndex(_dataTree, useDistanceL1_, _incrementalDictionary&&_incrementalFlann?_rebalancingFactor:1); - break; - case kNNFlannKdTree: - UASSERT_MSG(type == CV_32F, "To use KdTree dictionary, float descriptors are required!"); - _flannIndex->buildKDTreeIndex(_dataTree, KDTREE_SIZE, useDistanceL1_, _incrementalDictionary&&_incrementalFlann?_rebalancingFactor:1); - break; - case kNNFlannLSH: - UASSERT_MSG(type == CV_8U, "To use LSH dictionary, binary descriptors are required!"); - _flannIndex->buildLSHIndex(_dataTree, 12, 20, 2, _incrementalDictionary&&_incrementalFlann?_rebalancingFactor:1); - break; - default: - break; - } - + _flannIndex->buildIndex( + _strategy == kNNFlannNaive ? FlannIndex::FLANN_INDEX_LINEAR: + _strategy == kNNFlannLSH ? FlannIndex::FLANN_INDEX_LSH: + FlannIndex::FLANN_INDEX_KDTREE, // kNNFlannKdTree + _dataTree, + useDistanceL1_, + _incrementalDictionary&&_incrementalFlann?_rebalancingFactor:1); ULOGGER_DEBUG("Time to create kd tree = %f s", timer.ticks()); } } @@ -689,6 +683,146 @@ void VWDictionary::update() UDEBUG(""); } +std::vector VWDictionary::serializeIndex() const +{ + if(_strategy >= kNNBruteForce) { + UINFO("Not flann strategy, ignoring serialization..."); + return std::vector(); + } + if(!_flannIndex->isBuilt() || !_removedIndexedWords.empty() || !_notIndexedWords.empty() || _visualWords.empty()) { + UWARN("Flann index is not buit, or there are words not indexed, cannot do serialization."); + return std::vector(); + } + + return _flannIndex->serializeIndex(_serializeWithChecksum); +} + +void VWDictionary::deserializeIndex(const std::vector & data) +{ + deserializeIndex(data.data(), data.size()); +} + +void VWDictionary::deserializeIndex(const unsigned char * data, size_t size) +{ + if(data== NULL || size == 0) + { + UWARN("Trying to deserialize empty data, aborting."); + return; + } + UDEBUG("Loading flann index... (data size=%ld bytes)", size); + if(_strategy >= kNNBruteForce) { + //ignore + return; + } + + if(_flannIndex->isBuilt()) { + UERROR("Flann index is already built, cannot deserialize data!"); + return; + } + + if(_visualWords.empty()) { + UERROR("Descriptors should be added before deserializing flann index! See VWDictionary::addWord()"); + return; + } + + if(!(_removedIndexedWords.empty() && _visualWords.size() == _notIndexedWords.size())) { + UERROR("State of dictionary not as expected before deserializing. (removed words=%ld, words=%ld, not indexed=%ld)", + _removedIndexedWords.size(), _visualWords.size(), _notIndexedWords.size()); + return; + } + + std::map mapIndexId; + std::map mapIdIndex; + cv::Mat dataTree; + + UTimer timer; + timer.start(); + + int dim = _visualWords.begin()->second->getDescriptor().cols; + int type; + if(_visualWords.begin()->second->getDescriptor().type() == CV_8U) + { + useDistanceL1_ = true; + if(_strategy == kNNFlannKdTree) + { + type = CV_32F; + if(!_byteToFloat) + { + dim *= 8; + } + } + else + { + type = _visualWords.begin()->second->getDescriptor().type(); + } + } + else + { + type = _visualWords.begin()->second->getDescriptor().type(); + } + + UASSERT(type == CV_32F || type == CV_8U); + UASSERT(dim > 0); + + // Create the data matrix + dataTree = cv::Mat(_visualWords.size(), dim, type); // SURF descriptors are CV_32F + std::map::const_iterator iter = _visualWords.begin(); + for(unsigned int i=0; i < _visualWords.size(); ++i, ++iter) + { + cv::Mat descriptor; + if(iter->second->getDescriptor().type() == CV_8U) + { + if(_strategy == kNNFlannKdTree) + { + descriptor = convertBinTo32F(iter->second->getDescriptor(), _byteToFloat); + } + else + { + descriptor = iter->second->getDescriptor(); + } + } + else + { + descriptor = iter->second->getDescriptor(); + } + + UASSERT_MSG(descriptor.type() == type, uFormat("%d vs %d", descriptor.type(), type).c_str()); + UASSERT_MSG(descriptor.cols == dim, uFormat("%d vs %d", descriptor.cols, dim).c_str()); + + descriptor.copyTo(dataTree.row(i)); + mapIndexId.insert(mapIndexId.end(), std::pair(i, iter->second->id())); + mapIdIndex.insert(mapIdIndex.end(), std::pair(iter->second->id(), i)); + } + + ULOGGER_DEBUG("mapIndexId.size() = %d, words.size()=%d, dim=%d", mapIndexId.size(), _visualWords.size(), dim); + ULOGGER_DEBUG("copying data = %f s", timer.ticks()); + + std::string errorMsg; + if(_flannIndex->loadIndex( + data, + size, + _strategy == kNNFlannNaive ? FlannIndex::FLANN_INDEX_LINEAR: + _strategy == kNNFlannLSH ? FlannIndex::FLANN_INDEX_LSH: + FlannIndex::FLANN_INDEX_KDTREE, + dataTree, + useDistanceL1_, + _incrementalDictionary && _incrementalFlann ? _rebalancingFactor:1, + &errorMsg)) + { + _mapIndexId = mapIndexId; + _mapIdIndex = mapIdIndex; + _dataTree = dataTree; + _notIndexedWords.clear(); + _modified = false; + } + else { + UWARN("Failed deserializing flann index data (error: %s), the index will be rebuilt on next update.", errorMsg.c_str()); + _flannIndex->release(); // reset to initial state + } + + ULOGGER_DEBUG("Time to load flann index = %f s", timer.ticks()); +} + void VWDictionary::clear(bool printWarningsIfNotEmpty) { ULOGGER_DEBUG(""); @@ -718,6 +852,7 @@ void VWDictionary::clear(bool printWarningsIfNotEmpty) _unusedWords.clear(); _flannIndex->release(); useDistanceL1_ = false; + _modified = true; } int VWDictionary::getNextId() @@ -1394,15 +1529,15 @@ void VWDictionary::addWord(VisualWord * vw) { if(vw) { - _visualWords.insert(std::pair(vw->id(), vw)); - _notIndexedWords.insert(vw->id()); + _visualWords.insert(_visualWords.end(), std::pair(vw->id(), vw)); + _notIndexedWords.insert(_notIndexedWords.end(), vw->id()); if(vw->getReferences().size()) { _totalActiveReferences += uSum(uValues(vw->getReferences())); } else { - _unusedWords.insert(std::pair(vw->id(), vw)); + _unusedWords.insert(_unusedWords.end(), std::pair(vw->id(), vw)); } if(_lastWordId < vw->id()) { diff --git a/corelib/src/VisualWord.cpp b/corelib/src/VisualWord.cpp index f1b1196c..70584430 100644 --- a/corelib/src/VisualWord.cpp +++ b/corelib/src/VisualWord.cpp @@ -57,7 +57,7 @@ void VisualWord::addRef(int signatureId) } else { - _references.insert(std::pair(signatureId, 1)); + _references.insert(_references.end(), std::pair(signatureId, 1)); } ++_totalReferences; } diff --git a/corelib/src/odometry/OdometryF2F.cpp b/corelib/src/odometry/OdometryF2F.cpp index 0f335fe9..86a0a983 100644 --- a/corelib/src/odometry/OdometryF2F.cpp +++ b/corelib/src/odometry/OdometryF2F.cpp @@ -176,6 +176,8 @@ Transform OdometryF2F::computeTransform( if(info && this->isInfoDataFilled()) { std::list > > pairs; + UASSERT(tmpRefFrame.getWords().size() == tmpRefFrame.getWordsKpts().size()); + UASSERT(newFrame.getWords().size() == newFrame.getWordsKpts().size()); EpipolarGeometry::findPairsUnique(tmpRefFrame.getWords(), newFrame.getWords(), pairs); info->refCorners.resize(pairs.size()); info->newCorners.resize(pairs.size()); diff --git a/corelib/src/odometry/OdometryF2M.cpp b/corelib/src/odometry/OdometryF2M.cpp index ba47ac15..f1bbb58b 100644 --- a/corelib/src/odometry/OdometryF2M.cpp +++ b/corelib/src/odometry/OdometryF2M.cpp @@ -793,6 +793,7 @@ Transform OdometryF2M::computeTransform( if(!lastFrameModels.empty()) { + UASSERT(lastFrame_->getWordsKpts().size() == lastFrame_->getWords().size()); for(std::multimap::const_iterator iter = lastFrame_->getWords().begin(); iter!=lastFrame_->getWords().end(); ++iter) { const cv::Point3f & pt = lastFrame_->getWords3()[iter->second]; diff --git a/corelib/src/resources/DatabaseSchema.sql.in b/corelib/src/resources/DatabaseSchema.sql.in index 0b2a37d8..ea905de5 100644 --- a/corelib/src/resources/DatabaseSchema.sql.in +++ b/corelib/src/resources/DatabaseSchema.sql.in @@ -131,7 +131,9 @@ CREATE TABLE Admin ( opt_map BLOB, -- compressed CV_8SC1 occupancy grid opt_map_x_min FLOAT, opt_map_y_min FLOAT, - opt_map_resolution FLOAT, + opt_map_resolution FLOAT, + + dictionary_index BLOB, -- serialized dictionary index time_enter DATE ); diff --git a/corelib/src/resources/backward_compatibility/DatabaseSchema_0_22_0.sql b/corelib/src/resources/backward_compatibility/DatabaseSchema_0_22_0.sql new file mode 100644 index 00000000..75cd7d19 --- /dev/null +++ b/corelib/src/resources/backward_compatibility/DatabaseSchema_0_22_0.sql @@ -0,0 +1,183 @@ +-- ******************************************************************* +-- DatabaseSchema: Script for creating the database +-- Usage: +-- $ sqlite3 LTM.db < DatabaseSchema.sql +-- +-- ******************************************************************* + +-- ******************************************************************* +-- CLEAN +-- ******************************************************************* +/*DROP TABLE Node;*/ + +-- ******************************************************************* +-- CREATE +-- ******************************************************************* +CREATE TABLE Node ( + id INTEGER NOT NULL, + map_id INTEGER NOT NULL, + weight INTEGER, + stamp FLOAT, + pose BLOB, -- 3x4 float + ground_truth_pose BLOB, -- 3x4 float + velocity BLOB, -- 6 float (vx,vy,vz,vroll,vpitch,vyaw) m/s and rad/s + label TEXT, + gps BLOB, -- 1x6 double: stamp, longitude (DD), latitude (DD), altitude (m), accuracy (m), bearing (North 0->360 deg clockwise) + env_sensors BLOB, -- Variable 3xdouble: (sensorId1, value, stamp, sensorId2, value, stamp, ...) + time_enter DATE, + PRIMARY KEY (id) +); + +CREATE TABLE Data ( + id INTEGER NOT NULL, + image BLOB, -- compressed image (Grayscale or RGB) + depth BLOB, -- compressed image (Depth or Right image) + depth_confidence BLOB, -- compressed data (low=0 high=100) + calibration BLOB, -- fx, fy, cx, cy, [baseline,] width, height, local_transform + + scan BLOB, -- compressed data (Laser scan) + scan_info BLOB, -- scan_max_pts, scan_max_range, scan_format, local_transform + + ground_cells BLOB, -- compressed data (occupancy grid) + obstacle_cells BLOB, -- compressed data (occupancy grid) + empty_cells BLOB, -- compressed data (occupancy grid) + cell_size FLOAT, + view_point_x FLOAT, + view_point_y FLOAT, + view_point_z FLOAT, + + user_data BLOB, -- compressed data (User data) + time_enter DATE, + PRIMARY KEY (id) +); + +CREATE TABLE Link ( + from_id INTEGER NOT NULL, + to_id INTEGER NOT NULL, + type INTEGER NOT NULL, -- kNeighbor=0, kGlobalClosure=1, kLocalSpaceClosure=2, kLocalTimeClosure=3, kUserClosure=4, kVirtualClosure=5, kNeighborMerged=6, kPosePrior=7, kLandmark=8 + information_matrix BLOB NOT NULL, -- 6x6 double (inverse covariance) + transform BLOB, -- 3x4 float + user_data BLOB, -- compressed data (User data) + FOREIGN KEY (from_id) REFERENCES Node(id), + FOREIGN KEY (to_id) REFERENCES Node(id) +); + +-- +CREATE TABLE Word ( + id INTEGER NOT NULL, + descriptor_size INTEGER NOT NULL, + descriptor BLOB NOT NULL, + time_enter DATE, + PRIMARY KEY (id) +); + +CREATE TABLE Feature ( + node_id INTEGER NOT NULL, + word_id INTEGER NOT NULL, + pos_x FLOAT NOT NULL, + pos_y FLOAT NOT NULL, + size INTEGER NOT NULL, + dir FLOAT NOT NULL, + response FLOAT NOT NULL, + octave INTEGER NOT NULL, + depth_x FLOAT, + depth_y FLOAT, + depth_z FLOAT, + descriptor_size INTEGER, + descriptor BLOB, + FOREIGN KEY (node_id) REFERENCES Node(id) +); + +CREATE TABLE GlobalDescriptor ( + node_id INTEGER NOT NULL, + type INTEGER NOT NULL, + info BLOB, + data BLOB NOT NULL, + FOREIGN KEY (node_id) REFERENCES Node(id) +); + +-- + +CREATE TABLE Info ( + STM_size INTEGER, + last_sign_added INTEGER, + process_mem_used INTEGER, + database_mem_used INTEGER, + dictionary_size INTEGER, + parameters TEXT, + time_enter DATE +); + +CREATE TABLE Statistics ( + id INTEGER NOT NULL, + stamp FLOAT, + data BLOB, -- compressed string + wm_state BLOB, -- compressed data + FOREIGN KEY (id) REFERENCES Node(id) +); + +CREATE TABLE Admin ( + version TEXT, + preview_image BLOB, -- compressed image + + opt_cloud BLOB, -- compressed data + opt_ids BLOB, -- Node ids used to generate the optimized cloud/mesh + opt_poses BLOB, -- compressed N*3x4 float + opt_last_localization BLOB, -- 3x4 float + opt_polygons_size INTEGER, -- e.g., 3 + opt_polygons BLOB, -- compressed data [length_v0, i0,i1,i3, length_v1, i0,i1,i3] + opt_tex_coords BLOB, -- compressed data [length_v0, u0,v0,u1,v1,u2,v2, length_v1, u0,v0,u1,v1,u2,v2] + opt_tex_materials BLOB, -- compressed image + opt_map BLOB, -- compressed CV_8SC1 occupancy grid + opt_map_x_min FLOAT, + opt_map_y_min FLOAT, + opt_map_resolution FLOAT, + + time_enter DATE +); + +-- ******************************************************************* +-- TRIGGERS +-- ******************************************************************* +CREATE TRIGGER insert_Feature BEFORE INSERT ON Feature +WHEN NOT EXISTS (SELECT Node.id FROM Node WHERE Node.id = NEW.node_id) +BEGIN + SELECT RAISE(ABORT, 'Foreign key constraint failed in Feature table'); +END; + + -- Creating a trigger for time_enter +CREATE TRIGGER insert_Node_timeEnter AFTER INSERT ON Node +BEGIN + UPDATE Node SET time_enter = DATETIME('NOW') WHERE rowid = new.rowid; +END; + +CREATE TRIGGER insert_Data_timeEnter AFTER INSERT ON Data +BEGIN + UPDATE Node SET time_enter = DATETIME('NOW') WHERE rowid = new.rowid; +END; + +CREATE TRIGGER insert_Word_timeEnter AFTER INSERT ON Word +BEGIN + UPDATE Word SET time_enter = DATETIME('NOW') WHERE rowid = new.rowid; +END; + +CREATE TRIGGER insert_Info_timeEnter AFTER INSERT ON Info +BEGIN + UPDATE Info SET time_enter = DATETIME('NOW') WHERE rowid = new.rowid; +END; + +-- ******************************************************************* +-- INDEXES +-- ******************************************************************* +CREATE UNIQUE INDEX IDX_Node_id on Node (id); +CREATE INDEX IDX_Feature_node_id on Feature (node_id); +CREATE INDEX IDX_GlobalDescriptor_node_id on GlobalDescriptor (node_id); +CREATE INDEX IDX_Link_from_id on Link (from_id); +CREATE UNIQUE INDEX IDX_node_label on Node (label); +CREATE UNIQUE INDEX IDX_Statistics_id on Statistics (id); + +-- ******************************************************************* +-- VERSION +-- ******************************************************************* +INSERT INTO Admin(version) VALUES('0.22.0'); + diff --git a/corelib/src/rtflann/flann.hpp b/corelib/src/rtflann/flann.hpp index 6507cf8d..11477cdf 100644 --- a/corelib/src/rtflann/flann.hpp +++ b/corelib/src/rtflann/flann.hpp @@ -103,7 +103,6 @@ public: { flann_algorithm_t index_type = get_param(params,"algorithm"); loaded_ = false; - if (index_type == FLANN_INDEX_SAVED) { nnIndex_ = load_saved_index(features, get_param(params,"filename"), distance); loaded_ = true; @@ -180,10 +179,20 @@ public: if (fout == NULL) { throw FLANNException("Cannot open file"); } - nnIndex_->saveIndex(fout); + save(fout); fclose(fout); } + /** + * Save index to file stream. + * Caller has to open file stream with "wb" and close it afterwards. + * @param filename + */ + void save(FILE * stream) + { + nnIndex_->saveIndex(stream); + } + /** * \returns number of features in this index. */ @@ -377,6 +386,26 @@ public: return nnIndex_->radiusSearch(queries, indices, dists, radius, params); } + void load_saved_index(FILE* fin) + { + if(loaded_) { + throw FLANNException("Index already loaded!"); + } + if(nnIndex_->sizeAtBuild() != 0) { + throw FLANNException("Index must not be already built to load data."); + } + if (fin == NULL) { + throw FLANNException("File pointer must be valid!"); + } + IndexHeader header = load_header(fin); + if (header.h.data_type != flann_datatype_value::value) { + throw FLANNException("Datatype of saved index is different than of the one to be loaded."); + } + rewind(fin); + nnIndex_->loadIndex(fin); + loaded_ = true; + } + private: IndexType* load_saved_index(const Matrix& dataset, const std::string& filename, Distance distance) { diff --git a/guilib/src/DatabaseViewer.cpp b/guilib/src/DatabaseViewer.cpp index 586a386b..fea57a6b 100644 --- a/guilib/src/DatabaseViewer.cpp +++ b/guilib/src/DatabaseViewer.cpp @@ -8361,7 +8361,7 @@ void DatabaseViewer::refineConstraint(int from, int to, Registration * reg, Regi } Transform toPoseInv = filteredScanPoses.at(currentLink.to()).inverse(); - dbDriver_->loadNodeData(fromS, !silent, true, !silent, !silent); + dbDriver_->loadNodeData(*fromS, !silent, true, !silent, !silent); fromS->sensorData().uncompressData(); LaserScan fromScan = fromS->sensorData().laserScanRaw(); int maxPoints = fromScan.size(); @@ -8507,8 +8507,8 @@ void DatabaseViewer::refineConstraint(int from, int to, Registration * reg, Regi reextractVisualFeatures || !silent) { - dbDriver_->loadNodeData(fromS, reextractVisualFeatures || !silent || (reg->isScanRequired() && ui_->checkBox_icp_from_depth->isChecked()), reg->isScanRequired() || !silent, reg->isUserDataRequired() || !silent, !silent); - dbDriver_->loadNodeData(toS, reextractVisualFeatures || !silent || (reg->isScanRequired() && ui_->checkBox_icp_from_depth->isChecked()), reg->isScanRequired() || !silent, reg->isUserDataRequired() || !silent, !silent); + dbDriver_->loadNodeData(*fromS, reextractVisualFeatures || !silent || (reg->isScanRequired() && ui_->checkBox_icp_from_depth->isChecked()), reg->isScanRequired() || !silent, reg->isUserDataRequired() || !silent, !silent); + dbDriver_->loadNodeData(*toS, reextractVisualFeatures || !silent || (reg->isScanRequired() && ui_->checkBox_icp_from_depth->isChecked()), reg->isScanRequired() || !silent, reg->isUserDataRequired() || !silent, !silent); if(!silent) { @@ -8796,9 +8796,9 @@ bool DatabaseViewer::addConstraint(int from, int to, Registration * reg, bool si !silent) { // Add sensor data to generate features - dbDriver_->loadNodeData(fromS, reextractVisualFeatures || !silent || (reg->isScanRequired() && ui_->checkBox_icp_from_depth->isChecked()), reg->isScanRequired() || !silent, reg->isUserDataRequired() || !silent, !silent); + dbDriver_->loadNodeData(*fromS, reextractVisualFeatures || !silent || (reg->isScanRequired() && ui_->checkBox_icp_from_depth->isChecked()), reg->isScanRequired() || !silent, reg->isUserDataRequired() || !silent, !silent); fromS->sensorData().uncompressData(); - dbDriver_->loadNodeData(toS, reextractVisualFeatures || !silent || (reg->isScanRequired() && ui_->checkBox_icp_from_depth->isChecked()), reg->isScanRequired() || !silent, reg->isUserDataRequired() || !silent, !silent); + dbDriver_->loadNodeData(*toS, reextractVisualFeatures || !silent || (reg->isScanRequired() && ui_->checkBox_icp_from_depth->isChecked()), reg->isScanRequired() || !silent, reg->isUserDataRequired() || !silent, !silent); toS->sensorData().uncompressData(); if(reextractVisualFeatures) { diff --git a/guilib/src/MainWindow.cpp b/guilib/src/MainWindow.cpp index 86e69244..df757ba9 100644 --- a/guilib/src/MainWindow.cpp +++ b/guilib/src/MainWindow.cpp @@ -2394,13 +2394,19 @@ void MainWindow::processStats(const rtabmap::Statistics & stat) // do it after scaling std::multimap wordsA; std::multimap wordsB; - for(std::map::const_iterator iter=signature.getWords().begin(); iter!=signature.getWords().end(); ++iter) + if(signature.getWords().size() == signature.getWordsKpts().size()) { - wordsA.insert(wordsA.end(), std::make_pair(iter->first, signature.getWordsKpts()[iter->second])); + for(std::map::const_iterator iter=signature.getWords().begin(); iter!=signature.getWords().end(); ++iter) + { + wordsA.insert(wordsA.end(), std::make_pair(iter->first, signature.getWordsKpts()[iter->second])); + } } - for(std::map::const_iterator iter=loopSignature.getWords().begin(); iter!=loopSignature.getWords().end(); ++iter) + if(loopSignature.getWords().size() == loopSignature.getWordsKpts().size()) { - wordsB.insert(wordsB.end(), std::make_pair(iter->first, loopSignature.getWordsKpts()[iter->second])); + for(std::map::const_iterator iter=loopSignature.getWords().begin(); iter!=loopSignature.getWords().end(); ++iter) + { + wordsB.insert(wordsB.end(), std::make_pair(iter->first, loopSignature.getWordsKpts()[iter->second])); + } } this->drawKeypoints(wordsA, wordsB); @@ -4363,7 +4369,7 @@ void MainWindow::createAndAddFeaturesToMap(int nodeId, const Transform & pose, i UASSERT(iter->getWords().size() == iter->getWords3().size()); float maxDepth = _preferencesDialog->getCloudMaxDepth(0); UDEBUG("rgb.channels()=%d"); - if(!iter->getWords3().empty() && !iter->getWordsKpts().empty()) + if(!iter->getWords3().empty() && iter->getWords3().size() == iter->getWordsKpts().size()) { Transform invLocalTransform = Transform::getIdentity(); if(iter.value().sensorData().cameraModels().size() == 1 && diff --git a/guilib/src/MultiSessionLocWidget.cpp b/guilib/src/MultiSessionLocWidget.cpp index 73549677..5fb254f8 100644 --- a/guilib/src/MultiSessionLocWidget.cpp +++ b/guilib/src/MultiSessionLocWidget.cpp @@ -149,11 +149,13 @@ void MultiSessionLocWidget::updateView( Link link = loopLinks.find(nodeId) != loopLinks.end()?loopLinks.find(nodeId)->second:Link(); std::multimap keypoints; - for(std::multimap::const_iterator jter=s.getWords().begin(); jter!=s.getWords().end(); ++jter) - { - if(jter->first>0 && lastSignature.getWords().find(jter->first) != lastSignature.getWords().end()) + if(s.getWords().size() == s.getWordsKpts().size()) { + for(std::multimap::const_iterator jter=s.getWords().begin(); jter!=s.getWords().end(); ++jter) { - keypoints.insert(std::make_pair(jter->first, s.getWordsKpts()[jter->second])); + if(jter->first>0 && lastSignature.getWords().find(jter->first) != lastSignature.getWords().end()) + { + keypoints.insert(std::make_pair(jter->first, s.getWordsKpts()[jter->second])); + } } } diff --git a/guilib/src/PreferencesDialog.cpp b/guilib/src/PreferencesDialog.cpp index 9f08f66a..92be1ca1 100644 --- a/guilib/src/PreferencesDialog.cpp +++ b/guilib/src/PreferencesDialog.cpp @@ -1005,6 +1005,7 @@ PreferencesDialog::PreferencesDialog(QWidget * parent) : _ui->lineEdit_rgbCompressionFormat->setObjectName(Parameters::kMemImageCompressionFormat().c_str()); _ui->lineEdit_depthCompressionFormat->setObjectName(Parameters::kMemDepthCompressionFormat().c_str()); _ui->general_checkBox_keepDescriptors->setObjectName(Parameters::kMemRawDescriptorsKept().c_str()); + _ui->general_checkBox_loadVisualLocalFeaturesOnInit->setObjectName(Parameters::kMemLoadVisualLocalFeaturesOnInit().c_str()); _ui->general_checkBox_saveDepth16bits->setObjectName(Parameters::kMemSaveDepth16Format().c_str()); _ui->general_checkBox_compressionParallelized->setObjectName(Parameters::kMemCompressionParallelized().c_str()); _ui->general_checkBox_reduceGraph->setObjectName(Parameters::kMemReduceGraph().c_str()); @@ -1079,6 +1080,8 @@ PreferencesDialog::PreferencesDialog(QWidget * parent) : _ui->lineEdit_dictionaryPath->setObjectName(Parameters::kKpDictionaryPath().c_str()); connect(_ui->toolButton_dictionaryPath, SIGNAL(clicked()), this, SLOT(changeDictionaryPath())); _ui->checkBox_kp_newWordsComparedTogether->setObjectName(Parameters::kKpNewWordsComparedTogether().c_str()); + _ui->checkBox_kp_flannIndexSaved->setObjectName(Parameters::kKpFlannIndexSaved().c_str()); + _ui->checkBox_kp_serializeWithChecksum->setObjectName(Parameters::kKpSerializeWithChecksum().c_str()); _ui->subpix_winSize_kp->setObjectName(Parameters::kKpSubPixWinSize().c_str()); _ui->subpix_iterations_kp->setObjectName(Parameters::kKpSubPixIterations().c_str()); _ui->subpix_eps_kp->setObjectName(Parameters::kKpSubPixEps().c_str()); diff --git a/guilib/src/ProgressDialog.cpp b/guilib/src/ProgressDialog.cpp index ac2bdd0d..de921438 100644 --- a/guilib/src/ProgressDialog.cpp +++ b/guilib/src/ProgressDialog.cpp @@ -101,7 +101,7 @@ void ProgressDialog::setCancelButtonVisible(bool visible) void ProgressDialog::appendText(const QString & text, const QColor & color) { - UDEBUG(text.toStdString().c_str()); + //UDEBUG(text.toStdString().c_str()); _text->setText(text); QString html = tr("%1 %3").arg(QTime::currentTime().toString("HH:mm:ss")).arg(color.name()).arg(text); _detailedText->append(html); diff --git a/guilib/src/ui/preferencesDialog.ui b/guilib/src/ui/preferencesDialog.ui index 5e3f6235..1c3bddbe 100644 --- a/guilib/src/ui/preferencesDialog.ui +++ b/guilib/src/ui/preferencesDialog.ui @@ -63,7 +63,7 @@ 0 - 0 + -1145 713 4705 @@ -95,7 +95,7 @@ QFrame::Raised - 1 + 9 @@ -10092,10 +10092,10 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - - + + - Image pre decimation. Decimation of the RGB image before visual feature detection. If depth size is larger than decimated RGB size, depth is decimated to be always at most equal to RGB size. If Vocabulary->Depth As Mask is true and if depth is smaller than decimated RGB, depth may be interpolated to match RGB size for feature detection. + Save intermediate node data. true @@ -10105,27 +10105,7 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - - - - - - - true - - - - - - - - - - false - - - - + Image post decimation. Decimation of the RGB image before saving it to database. If depth size is larger than decimated RGB size, depth is decimated to be always at most equal to RGB size. Decimation is done from the original image. If set to same value than Pre Decimation, data already decimated is saved (no need to re-decimate the image). @@ -10138,91 +10118,6 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - - - - - - - false - - - - - - - 1 - - - 16 - - - - - - - - - - false - - - - - - - Raw descriptors kept in memory. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - Create map labels. The first node of a map will be labelled as "map#" where # is the map ID. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - - 50 - 0 - - - - 1.000000000000000 - - - 0.010000000000000 - - - 0.200000000000000 - - - - - - - - - - false - - - @@ -10236,8 +10131,8 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - - + + @@ -10246,30 +10141,7 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - - - - Reduce graph. Merge nodes when loop closures are added (ignoring those with user data set). - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - - - - false - - - - + Multi-threaded compression. @@ -10282,10 +10154,10 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - - + + - T_recent : Ratio of locations after the last loop closure in WM that cannot be transferred. + Raw descriptors kept in memory. true @@ -10295,44 +10167,8 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - - - - - - - true - - - - - - - Save intermediate node data. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - Keep raw sensor data. Only useful to save loop closure computation time when features re-extraction is enabled. Disable to save RAM memory. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - + + @@ -10341,13 +10177,26 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - - - - 1 + + + + Create map labels. The first node of a map will be labelled as "map#" where # is the map ID. - - 16 + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + false @@ -10364,32 +10213,6 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - - - - 1 - - - 9999 - - - 15 - - - - - - - STM size : Short-term memory size. - - - false - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - @@ -10413,6 +10236,127 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag + + + + + + + false + + + + + + + + + + false + + + + + + + + + + false + + + + + + + Image pre decimation. Decimation of the RGB image before visual feature detection. If depth size is larger than decimated RGB size, depth is decimated to be always at most equal to RGB size. If Vocabulary->Depth As Mask is true and if depth is smaller than decimated RGB, depth may be interpolated to match RGB size for feature detection. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + false + + + + + + + STM size : Short-term memory size. + + + false + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + 50 + 0 + + + + 1.000000000000000 + + + 0.010000000000000 + + + 0.200000000000000 + + + + + + + 1 + + + 9999 + + + 15 + + + + + + + + + + false + + + + + + + T_recent : Ratio of locations after the last loop closure in WM that cannot be transferred. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + @@ -10426,10 +10370,13 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - - + + + + This can be useful in case the robots don't have all same camera orientation but are using the same map, so that not rotation-invariant visual features can still be used across the fleet. + - Save depth image into 16 bits format to reduce memory used. Warning: values over ~65 meters are ignored (maximum 65535 millimeters). + Rotate images so that upside is up if they are not already. true @@ -10439,6 +10386,49 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag + + + + + + + true + + + + + + + Reduce graph. Merge nodes when loop closures are added (ignoring those with user data set). + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + 1 + + + 16 + + + + + + + + + + false + + + @@ -10465,23 +10455,20 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - - - - + + + + 1 - - false + + 16 - - - - This can be useful in case the robots don't have all same camera orientation but are using the same map, so that not rotation-invariant visual features can still be used across the fleet. - + + - Rotate images so that upside is up if they are not already. + Save depth image into 16 bits format to reduce memory used. Warning: values over ~65 meters are ignored (maximum 65535 millimeters). true @@ -10491,13 +10478,49 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - - + + + + Keep raw sensor data. Only useful to save loop closure computation time when features re-extraction is enabled. Disable to save RAM memory. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + - false + true + + + + + + + Load all local visual features (keypoints, descriptors and 3D points) in RAM when loading an existing database. This can add significant time to initialize the memory but the features will be already loaded before computing loop closure transforms. If false, the features are loaded on-demand from the database when a loop closure transformation should be estimated. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + true @@ -11384,6 +11407,16 @@ generate the number of words requested. Features Quantization + + + + + + + true + + + @@ -11391,6 +11424,19 @@ generate the number of words requested. + + + + Factor used when rebuilding the incremental FLANN index. Set 1 to disable. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + @@ -11406,32 +11452,10 @@ Lower the ratio -> higher the precision. - - - - 2 - - - 0.010000000000000 - - - 1.000000000000000 - - - 0.100000000000000 - - - 0.700000000000000 - - - - - - - - + + - Path to a pre-computed dictionary (when "Use an incremental vocabulary" is not set). + When using a FLANN-based nearest neighbor strategy, add/remove points to its index without always rebuilding the index (the index is built only when the dictionary increases of the factor below in size). true @@ -11451,16 +11475,6 @@ Lower the ratio -> higher the precision. - - - - Nearest neighbor strategy. - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - @@ -11493,33 +11507,6 @@ Lower the ratio -> higher the precision. - - - - Use an incremental vocabulary. -When set to false, no new words are added to dictionary, so no more updates are required after each detection, which greatly increases time performance at the cost of lower adaptation to new environments. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - If the dictionary update and signature creation were parallelized. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - @@ -11530,16 +11517,6 @@ When set to false, no new words are added to dictionary, so no more updates are - - - - - - - true - - - @@ -11553,10 +11530,11 @@ When set to false, no new words are added to dictionary, so no more updates are - - + + - When using a FLANN-based nearest neighbor strategy, add/remove points to its index without always rebuilding the index (the index is built only when the dictionary increases of the factor below in size). + Use an incremental vocabulary. +When set to false, no new words are added to dictionary, so no more updates are required after each detection, which greatly increases time performance at the cost of lower adaptation to new environments. true @@ -11566,26 +11544,22 @@ When set to false, no new words are added to dictionary, so no more updates are - - - - + + + + 2 - - true + + 0.010000000000000 - - - - - - Factor used when rebuilding the incremental FLANN index. Set 1 to disable. + + 1.000000000000000 - - true + + 0.100000000000000 - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + 0.700000000000000 @@ -11608,6 +11582,16 @@ When set to false, no new words are added to dictionary, so no more updates are + + + + + + + true + + + @@ -11621,8 +11605,93 @@ When set to false, no new words are added to dictionary, so no more updates are - - + + + + + + + Path to a pre-computed dictionary (when "Use an incremental vocabulary" is not set). + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + true + + + + + + + On serialization of the FLANN index, compute checksum of the data used by the FLANN index. This adds a slight overhead on serialization/deserialization to make sure that the dictionary data correspond to same data used when the index was built. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + If the dictionary update and signature creation were parallelized. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Nearest neighbor strategy. + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Save FLANN index during localization session (when Mem/IncrementalMemory=false). The FLANN index will be saved to database after the first time localization mode is used, then on next sessions, the index is reloaded from the database instead of being rebuilt again. This can save significant loading time when the visual word dictionary is big (>1M words). Note that if the dictionary is modified (parameters or data), the index will be rebuilt and saved again on the next session. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + false + + + + + diff --git a/tools/Matcher/main.cpp b/tools/Matcher/main.cpp index 8404db8a..f99a3f06 100644 --- a/tools/Matcher/main.cpp +++ b/tools/Matcher/main.cpp @@ -400,10 +400,12 @@ int main(int argc, char * argv[]) std::map wordsTo = uMultimapToMapUnique(dataTo.getWords()); std::map kptsFrom; std::map kptsTo; + UASSERT(dataFrom.getWords().size() == dataFrom.getWordsKpts().size()); for(std::map::iterator iter=wordsFrom.begin(); iter!=wordsFrom.end(); ++iter) { kptsFrom.insert(std::make_pair(iter->first, dataFrom.getWordsKpts()[iter->second])); } + UASSERT(dataTo.getWords().size() == dataTo.getWordsKpts().size()); for(std::map::iterator iter=wordsTo.begin(); iter!=wordsTo.end(); ++iter) { kptsTo.insert(std::make_pair(iter->first, dataTo.getWordsKpts()[iter->second])); From efcfb9be161a1406558e4dfc6ca9e9351a5a835c Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sat, 6 Sep 2025 16:30:01 -0700 Subject: [PATCH 014/126] Export: added options to export 2D oocupancy grid (--map) and octomap (--octomap) --- tools/Export/main.cpp | 180 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 166 insertions(+), 14 deletions(-) diff --git a/tools/Export/main.cpp b/tools/Export/main.cpp index 7b8e753f..8ac628c1 100644 --- a/tools/Export/main.cpp +++ b/tools/Export/main.cpp @@ -32,8 +32,13 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include #include #include +#include +#ifdef RTABMAP_OCTOMAP +#include +#endif #include #include #include @@ -125,6 +130,9 @@ void showUsage() " 1=KML (Google Earth)\n" " --images Export images with stamp as file name.\n" " --images_id Export images with node id as file name.\n" + " --map Export 2D occupancy grid. Note that with \"--opt 2\", the already optimized \n" + " map saved in database (if exists) is exported.\n" + " --octomap Export 3D OctoMap.\n" " --ba Do global bundle adjustment before assembling the clouds.\n" " --gain # Gain compensation value (default 1, set 0 to disable).\n" " --gain_gray Do gain estimation compensation on gray channel only (default RGB channels).\n" @@ -164,7 +172,7 @@ void showUsage() " --random_samples # Number of output samples using a random filter (default 0, 0=disabled).\n" " --color_radius # Radius used to colorize polygons (default 0.05 m, 0 m with --scan). Set 0 for nearest color.\n" " --scan Use laser scan for the point cloud.\n" - " --save_in_db Save resulting assembled point cloud or mesh in the database.\n" + " --save_in_db Save resulting optimized poses, assembled point cloud, mesh or 2D occupancy grid in the database.\n" " --xmin # Minimum range on X axis to keep nodes to export.\n" " --xmax # Maximum range on X axis to keep nodes to export.\n" " --ymin # Minimum range on Y axis to keep nodes to export.\n" @@ -191,6 +199,37 @@ class ConsoleProgessState : public ProgressState } }; +void saveMap( + const std::string & outputDirectory, + const std::string & baseName, + const cv::Mat & map, + float xMin, + float yMin, + float cellSize, + const ParametersMap & parameters) +{ + cv::Mat map8U = rtabmap::util3d::convertMap2Image8U(map, true); + std::string path=outputDirectory+"/"+baseName+".pgm"; + + cv::imwrite(path, map8U); + + std::string yaml = outputDirectory+"/"+baseName+".yaml"; + + float occupancyThr = Parameters::defaultGridGlobalOccupancyThr(); + Parameters::parse(parameters, Parameters::kGridGlobalOccupancyThr(), occupancyThr); + + std::ofstream file; + file.open (yaml); + file << "image: " << baseName << ".pgm" << std::endl; + file << "resolution: " << cellSize << std::endl; + file << "origin: [" << xMin << ", " << yMin << ", 0.0]" << std::endl; + file << "negate: 0" << std::endl; + file << "occupied_thresh: " << occupancyThr << std::endl; + file << "free_thresh: 0.196" << std::endl; + file << std::endl; + file.close(); +} + int main(int argc, char * argv[]) { ULogger::setType(ULogger::kTypeConsole); @@ -264,6 +303,8 @@ int main(int argc, char * argv[]) int exportGps = -1; bool exportImages = false; bool exportImagesId = false; + bool export2DMap = false; + bool exportOctomap = false; int optimizationApproach = 0; std::string outputName; std::string outputDir; @@ -571,6 +612,18 @@ int main(int argc, char * argv[]) exportImages = true; exportImagesId = true; } + else if(std::strcmp(argv[i], "--map") == 0) + { + export2DMap = true; + } + else if(std::strcmp(argv[i], "--octomap") == 0) + { +#ifdef RTABMAP_OCTOMAP + exportOctomap = true; +#else + printf("Option --octomap cannot be used, rtabmap is not built with OctoMap support.\n"); +#endif + } else if(std::strcmp(argv[i], "--ba") == 0) { ba = true; @@ -1117,7 +1170,9 @@ int main(int argc, char * argv[]) exportPosesGt || exportPosesGps || exportGps>=0 || - texture)) + texture || + export2DMap || + exportOctomap)) { printf("Launching the tool without any required option(s) is deprecated. We will add --cloud to keep compatibilty with old behavior.\n"); exportCloud = true; @@ -1172,12 +1227,19 @@ int main(int argc, char * argv[]) } printf("Opening database \"%s\"... done (%fs).\n", dbPath.c_str(), timer.ticks()); + std::string outputDirectory = outputDir.empty()?UDirectory::getDir(dbPath):outputDir; + if(!UDirectory::exists(outputDirectory)) + { + UDirectory::makeDir(outputDirectory); + } + std::string baseName = outputName.empty()?uSplit(UFile::getName(dbPath), '.').front():outputName; + std::map optimizedPoses; std::map odomPoses; std::multimap links; dbDriver->getAllOdomPoses(odomPoses, true); dbDriver->getAllLinks(links, true, true); - if(optimizationApproach == 3 || !(exportCloud || exportMesh || exportPoses || exportPosesCamera || exportPosesScan || exportPosesLandmarks)) + if(optimizationApproach == 3 || !(exportCloud || exportMesh || exportPoses || exportPosesCamera || exportPosesScan || exportPosesLandmarks || export2DMap || exportOctomap)) { // Just use odometry poses when exporting only images optimizedPoses = odomPoses; @@ -1200,6 +1262,22 @@ int main(int argc, char * argv[]) else { printf("Loading optimized poses from database... done (%d optimized poses loaded).\n", (int)optimizedPoses.size()); + if(export2DMap) + { + printf("Loading optimized 2D occupancy grid from database...\n"); + float xMin, yMin, cellSize; + cv::Mat map = dbDriver->load2DMap(xMin, yMin, cellSize); + if(map.empty()) { + printf("Optimized 2D occupancy grid in the database is empty, it will be regenerated.\n"); + } + else{ + saveMap(outputDirectory, baseName, map, xMin, yMin, cellSize, parameters); + printf("Loading optimized 2D occupancy grid from database... done! Saved to \"%s\" and \"%s\"\n", + (baseName+".pgm").c_str(), (baseName+".yaml").c_str()); + + export2DMap = false; + } + } } } if(optimizationApproach <= 1) @@ -1361,13 +1439,6 @@ int main(int argc, char * argv[]) } } - std::string outputDirectory = outputDir.empty()?UDirectory::getDir(dbPath):outputDir; - if(!UDirectory::exists(outputDirectory)) - { - UDirectory::makeDir(outputDirectory); - } - std::string baseName = outputName.empty()?uSplit(UFile::getName(dbPath), '.').front():outputName; - // Construct the cloud if(exportCloud || exportMesh) { @@ -1377,6 +1448,11 @@ int main(int argc, char * argv[]) { printf("Export images...\n"); } + else if(export2DMap || exportOctomap) + { + printf("Assemble global occupancy grid...\n"); + } + pcl::PointCloud::Ptr assembledCloud(new pcl::PointCloud); pcl::PointCloud::Ptr assembledCloudI(new pcl::PointCloud); std::map robotPoses; @@ -1399,6 +1475,12 @@ int main(int argc, char * argv[]) std::vector rawViewpointIndices; std::map rawViewpoints; std::map densityPoses; + LocalGridCache localGridCache; + OccupancyGrid grid(&localGridCache, parameters); +#ifdef RTABMAP_OCTOMAP + OctoMap octomap(&localGridCache, parameters); +#endif + std::map addedPosesToMap; if(densityRadius && (exportCloud || exportMesh)) { densityPoses = graph::radiusPosesFiltering(optimizedPoses, densityRadius, densityAngle*CV_PI/180.0f); @@ -1436,7 +1518,7 @@ int main(int argc, char * argv[]) SensorData data; bool loadImages = ((exportCloud || exportMesh) && (!cloudFromScan || texture || camProjection)) || exportImages; bool loadScan = ((exportCloud || exportMesh) && cloudFromScan) || exportPosesScan; - if(loadImages || loadScan) + if(loadImages || loadScan || export2DMap || exportOctomap) { dbDriver->getNodeData( iter->first, @@ -1444,7 +1526,7 @@ int main(int argc, char * argv[]) loadImages, loadScan, false, - false); + export2DMap || exportOctomap); } // uncompress data @@ -1756,6 +1838,29 @@ int main(int argc, char * argv[]) gtStamps.insert(std::make_pair(iter->first, stamp)); } + if(weight != -1 && (export2DMap || exportOctomap)) { + cv::Mat ground; + cv::Mat obstacles; + cv::Mat empty; + data.uncompressDataConst(0, 0, 0, 0, &ground, &obstacles, &empty); + if(ground.empty() && obstacles.empty() && empty.empty()) { + printf("Node %d doesn't have local occupancy grid, ignored!\n", iter->first); + } + else { + addedPosesToMap.insert(*iter); + localGridCache.add(iter->first, ground, obstacles, empty, data.gridCellSize(), data.gridViewPoint()); + if(export2DMap && !grid.update(addedPosesToMap)) { + printf("Failed to assemble local grid %d to global occupancy grid!\n", iter->first); + } +#ifdef RTABMAP_OCTOMAP + if(exportOctomap && !octomap.update(addedPosesToMap)) { + printf("Failed to assemble local grid %d to OctoMap!\n", iter->first); + } +#endif + localGridCache.clear(); + } + } + if(optimizedPoses.size() >= 500) { ++processedNodes; @@ -1774,16 +1879,55 @@ int main(int argc, char * argv[]) { printf("Create and assemble the clouds... done (%fs, %d points).\n", timer.ticks(), !assembledCloud->empty()?(int)assembledCloud->size():(int)assembledCloudI->size()); } + else if(export2DMap || exportOctomap) + { + printf("Assemble global occupancy grid... done (%fs).\n", timer.ticks()); + } if(exportImages || exportImagesId) { printf("%d images exported!\n", imagesExported); - if(!(exportCloud || exportMesh || exportPoses || exportPosesCamera || exportPosesScan)) { + if(!(exportCloud || exportMesh || exportPoses || exportPosesCamera || exportPosesScan || export2DMap || exportOctomap)) { //images exported, early exit. return 0; } } + if(export2DMap) + { + if(grid.addedNodes().empty()) + { + printf( "Option --map and/or --prob_map is enabled, but no local occupancy grids have been " + "assembled to the 2D occupancy grid. Use rtabmap-databaseViewer to regenerate them.\n"); + } + else { + printf("Saving 2D occupancy grid...\n"); + float xMin, yMin; + cv::Mat map = grid.getMap(xMin, yMin); + saveMap(outputDirectory, baseName, map, xMin, yMin, grid.getCellSize(), parameters); + printf("Saving 2D occupancy grid... done (%fs)! Saved to \"%s\" and \"%s\"\n", timer.ticks(), + (baseName+".pgm").c_str(), (baseName+".yaml").c_str()); + } + } +#ifdef RTABMAP_OCTOMAP + if(exportOctomap) + { + if(octomap.addedNodes().empty()) + { + printf( "Option --octomap is enabled, but no local occupancy grids have been " + "assembled in the Octomap. Use rtabmap-databaseViewer to regenerate them.\n"); + } + else { + pcl::PointCloud::Ptr cloud = octomap.createCloud(); + pcl::io::savePLYFile(outputDirectory+"/"+baseName+"_octomap.ply",*cloud); + printf("Saving 3D OctoMap...\n"); + octomap.writeBinary(outputDirectory+"/"+baseName+"_octomap.bt"); + printf("Saving 3D OctoMap... done (%fs)! Saved to \"%s\".\n", timer.ticks(), + (baseName+"_octomap.bt").c_str()); + } + } +#endif + ConsoleProgessState progressState; if(saveInDb) @@ -1793,7 +1937,15 @@ int main(int argc, char * argv[]) Transform lastlocalizationPose; driver->loadOptimizedPoses(&lastlocalizationPose); //optimized poses have changed, reset 2d map - driver->save2DMap(cv::Mat(), 0, 0, 0); + if(export2DMap && grid.addedNodes().size() != 0) { + printf("Saved optimized 2D occupancy grid back to database!\n"); + float xMin, yMin; + cv::Mat map = grid.getMap(xMin, yMin); + driver->save2DMap(map, xMin, yMin, grid.getCellSize()); + } + else { + driver->save2DMap(cv::Mat(), 0, 0, 0); + } driver->saveOptimizedPoses(robotPoses, lastlocalizationPose); cv::Vec3f vmin, vmax; graph::computeMinMax(robotPoses, vmin, vmax); From 668494948c9bdad9be0f54fa080621de352b0d09 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sat, 6 Sep 2025 16:52:29 -0700 Subject: [PATCH 015/126] fixed build: missing fstream --- tools/Export/main.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/Export/main.cpp b/tools/Export/main.cpp index 8ac628c1..d89c7733 100644 --- a/tools/Export/main.cpp +++ b/tools/Export/main.cpp @@ -49,6 +49,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include #ifdef RTABMAP_PDAL #include From a96d574517abb064a62ab188ef47a522a571a77b Mon Sep 17 00:00:00 2001 From: matlabbe Date: Fri, 12 Sep 2025 10:49:08 -0700 Subject: [PATCH 016/126] ImageView: fixed assert when depth image is smaller than rgb image if depth colormap is projected in base frame --- guilib/src/ImageView.cpp | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/guilib/src/ImageView.cpp b/guilib/src/ImageView.cpp index 73bc9413..ccf4b14d 100644 --- a/guilib/src/ImageView.cpp +++ b/guilib/src/ImageView.cpp @@ -1382,6 +1382,16 @@ void ImageView::setImageDepth(const cv::Mat & imageDepth, const cv::Mat & imageD // convert the depth values in height values cv::Mat depthInBaseFrame = _imageDepthCv.clone(); int subImageWidth = _imageDepthCv.cols / _models.size(); + std::vector models; // scale model to size of depth image if needed + for(const auto & model: _models) { + UASSERT(subImageWidth <= model.imageWidth()); + if(subImageWidth < model.imageWidth()) { + models.push_back(model.scaled(float(subImageWidth)/float(model.imageWidth()))); + } + else { + models.push_back(model); + } + } if(depthInBaseFrame.type() == CV_16UC1) { for(int v=0; v(v); @@ -1390,9 +1400,9 @@ void ImageView::setImageDepth(const cv::Mat & imageDepth, const cv::Mat & imageD if(val > 0) { cv::Point3f pt; int cameraIndex = u/subImageWidth; - UASSERT(cameraIndex>=0 && cameraIndex < (int)_models.size() && subImageWidth == _models[cameraIndex].imageWidth()); - _models[cameraIndex].project(u,v,float(val)/1000.0f, pt.x, pt.y, pt.z); - pt = util3d::transformPoint(pt, _models[cameraIndex].localTransform()); + UASSERT(cameraIndex>=0 && cameraIndex < (int)models.size() && subImageWidth == models[cameraIndex].imageWidth()); + models[cameraIndex].project(u,v,float(val)/1000.0f, pt.x, pt.y, pt.z); + pt = util3d::transformPoint(pt, models[cameraIndex].localTransform()); val = (unsigned short)(pt.z*1000.0f); } } @@ -1406,9 +1416,9 @@ void ImageView::setImageDepth(const cv::Mat & imageDepth, const cv::Mat & imageD if(val > 0) { cv::Point3f pt; int cameraIndex = u/subImageWidth; - UASSERT(cameraIndex>=0 && cameraIndex < (int)_models.size() && subImageWidth == _models[cameraIndex].imageWidth()); - _models[cameraIndex].project(u,v,val, pt.x, pt.y, pt.z); - pt = util3d::transformPoint(pt, _models[cameraIndex].localTransform()); + UASSERT(cameraIndex>=0 && cameraIndex < (int)models.size() && subImageWidth == models[cameraIndex].imageWidth()); + models[cameraIndex].project(u,v,val, pt.x, pt.y, pt.z); + pt = util3d::transformPoint(pt, models[cameraIndex].localTransform()); val = pt.z; } } From 6a435c968f45d3fb6485f19291ff0703a3f42bc7 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sat, 13 Sep 2025 15:58:40 -0700 Subject: [PATCH 017/126] reorganized includes (added SYSTEM keyword) (#1578) --- corelib/src/CMakeLists.txt | 12 ++++++------ guilib/src/CMakeLists.txt | 17 +++++++---------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/corelib/src/CMakeLists.txt b/corelib/src/CMakeLists.txt index 39288a03..d0205f7c 100644 --- a/corelib/src/CMakeLists.txt +++ b/corelib/src/CMakeLists.txt @@ -163,10 +163,6 @@ IF(MSVC) ENDIF(MSVC) SET(INCLUDE_DIRS - ${CMAKE_CURRENT_SOURCE_DIR} - ${CMAKE_CURRENT_SOURCE_DIR}/../include - ${CMAKE_CURRENT_BINARY_DIR} - ${CMAKE_CURRENT_BINARY_DIR}/include ${ZLIB_INCLUDE_DIRS} ) @@ -858,8 +854,12 @@ generate_export_header(rtabmap_core DEPRECATED_MACRO_NAME RTABMAP_DEPRECATED) target_include_directories(rtabmap_core PUBLIC - "$" - "$") + "$" + "$") + +target_include_directories(rtabmap_core SYSTEM PUBLIC + "$" + "$") TARGET_LINK_LIBRARIES(rtabmap_core PUBLIC diff --git a/guilib/src/CMakeLists.txt b/guilib/src/CMakeLists.txt index 8eb53433..c590077a 100644 --- a/guilib/src/CMakeLists.txt +++ b/guilib/src/CMakeLists.txt @@ -97,11 +97,7 @@ IF(MSVC) SET(SRC_FILES ${SRC_FILES} ${HEADERS}) ENDIF(MSVC) -SET(INCLUDE_DIRS - ${CMAKE_CURRENT_SOURCE_DIR}/../include - ${CMAKE_CURRENT_SOURCE_DIR} - ${CMAKE_CURRENT_BINARY_DIR} # for qt ui generated in binary dir -) +SET(INCLUDE_DIRS "") IF(QT4_FOUND) INCLUDE(${QT_USE_FILE}) @@ -172,9 +168,6 @@ IF(VTK_USE_QVTK) SET(LIBRARIES ${LIBRARIES} ${QVTK_LIBRARY}) ENDIF(VTK_USE_QVTK) -#include files -INCLUDE_DIRECTORIES(${INCLUDE_DIRS}) - add_definitions(${PCL_DEFINITIONS}) # Include presets @@ -205,8 +198,12 @@ generate_export_header(rtabmap_gui DEPRECATED_MACRO_NAME RTABMAP_DEPRECATED) target_include_directories(rtabmap_gui PUBLIC - "$" - "$") + "$" + "$") + +target_include_directories(rtabmap_gui SYSTEM PUBLIC + "$" + "$") TARGET_LINK_LIBRARIES(rtabmap_gui PUBLIC From cb0cc9ed1893d43111031449367b26075b0ee04a Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 14 Sep 2025 19:29:28 -0700 Subject: [PATCH 018/126] VINS Fusion overhaul (#1580) * Updated correct focal length used by VINS, updated VINS visualization. * Renamed OdometryVINS to OdometryVINSFusion --- CMakeLists.txt | 21 +- Version.h.in | 2 +- corelib/include/rtabmap/core/Odometry.h | 2 +- corelib/include/rtabmap/core/Parameters.h | 4 +- .../rtabmap/core/odometry/OdometryVINS.h | 38 +-- .../core/odometry/OdometryVINSFusion.h | 64 ++++ corelib/src/CMakeLists.txt | 2 +- corelib/src/Odometry.cpp | 6 +- corelib/src/Parameters.cpp | 5 +- ...dometryVINS.cpp => OdometryVINSFusion.cpp} | 282 +++++++++--------- .../include/rtabmap/gui/PreferencesDialog.h | 2 +- guilib/src/AboutDialog.cpp | 2 +- guilib/src/MainWindow.cpp | 4 +- guilib/src/PreferencesDialog.cpp | 16 +- guilib/src/ui/preferencesDialog.ui | 6 +- 15 files changed, 245 insertions(+), 211 deletions(-) create mode 100644 corelib/include/rtabmap/core/odometry/OdometryVINSFusion.h rename corelib/src/odometry/{OdometryVINS.cpp => OdometryVINSFusion.cpp} (65%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 936a7a6f..71ee6c4b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,7 +20,7 @@ SET(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake_modules") ####################### SET(RTABMAP_MAJOR_VERSION 0) SET(RTABMAP_MINOR_VERSION 23) -SET(RTABMAP_PATCH_VERSION 0) +SET(RTABMAP_PATCH_VERSION 1) SET(RTABMAP_VERSION ${RTABMAP_MAJOR_VERSION}.${RTABMAP_MINOR_VERSION}.${RTABMAP_PATCH_VERSION}) @@ -218,7 +218,7 @@ option(WITH_DVO "Include DVO support" OFF) option(WITH_ORB_SLAM "Include ORB_SLAM2 or ORB_SLAM3 support" OFF) option(WITH_OKVIS "Include OKVIS support" OFF) option(WITH_MSCKF_VIO "Include MSCKF_VIO support" OFF) -option(WITH_VINS "Include VINS-Fusion support" OFF) +option(WITH_VINS_FUSION "Include VINS-Fusion support" OFF) option(WITH_OPENVINS "Include OpenVINS support" OFF) option(WITH_MADGWICK "Include Madgwick IMU filtering support" ON) option(WITH_FASTCV "Include FastCV support" ON) @@ -794,15 +794,20 @@ IF(WITH_MSCKF_VIO) ENDIF(msckf_vio_FOUND) ENDIF(WITH_MSCKF_VIO) -IF(WITH_VINS) +IF(WITH_VINS AND NOT WITH_VINS_FUSION) +message(DEPRECATION "The option WITH_VINS is deprecated and will be removed in a future version. Please use WITH_VINS_FUSION instead.") +set(WITH_VINS_FUSION ON) +ENDIF(WITH_VINS AND NOT WITH_VINS_FUSION) + +IF(WITH_VINS_FUSION) FIND_PACKAGE(vins QUIET) IF(vins_FOUND) - MESSAGE(STATUS "Found vins: ${vins_INCLUDE_DIRS}") + MESSAGE(STATUS "Found vins-fusion: ${vins_INCLUDE_DIRS}") IF(okvis_FOUND) - MESSAGE(WARNING "VINS and OKVIS will be both linked to project, make sure VINS has been built with against same Ceres version than OKVIS to avoid some crashes.") + MESSAGE(WARNING "VINS-Fusion and OKVIS will be both linked to project, make sure VINS-Fusion has been built with against same Ceres version than OKVIS to avoid some crashes.") ENDIF(okvis_FOUND) ENDIF(vins_FOUND) -ENDIF(WITH_VINS) +ENDIF(WITH_VINS_FUSION) IF(WITH_OPENVINS) FIND_PACKAGE(ov_msckf QUIET) @@ -1101,7 +1106,7 @@ IF(NOT msckf_vio_FOUND) SET(MSCKF_VIO "//") ENDIF() IF(NOT vins_FOUND) - SET(VINS "//") + SET(VINSFUSION "//") ENDIF() IF(NOT ov_msckf_FOUND) SET(OPENVINS "//") @@ -1800,7 +1805,7 @@ ENDIF() IF(vins_FOUND) MESSAGE(STATUS " With VINS-Fusion = YES (License: GPLv3)") ELSEIF(NOT WITH_VINS) -MESSAGE(STATUS " With VINS-Fusion = NO (WITH_VINS=OFF)") +MESSAGE(STATUS " With VINS-Fusion = NO (WITH_VINS_FUSION=OFF)") ELSE() MESSAGE(STATUS " With VINS-Fusion = NO (VINS-Fusion not found)") ENDIF() diff --git a/Version.h.in b/Version.h.in index 29feac8b..5af7a75e 100644 --- a/Version.h.in +++ b/Version.h.in @@ -82,7 +82,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. @DVO@#define RTABMAP_DVO @OKVIS@#define RTABMAP_OKVIS @MSCKF_VIO@#define RTABMAP_MSCKF_VIO -@VINS@#define RTABMAP_VINS +@VINSFUSION@#define RTABMAP_VINS_FUSION @OPENVINS@#define RTABMAP_OPENVINS @ORB_SLAM@#define RTABMAP_ORB_SLAM @ORB_SLAM_VERSION@ @ORB_OCTREE@#define RTABMAP_ORB_OCTREE diff --git a/corelib/include/rtabmap/core/Odometry.h b/corelib/include/rtabmap/core/Odometry.h index 8b897673..75864b50 100644 --- a/corelib/include/rtabmap/core/Odometry.h +++ b/corelib/include/rtabmap/core/Odometry.h @@ -53,7 +53,7 @@ public: kTypeOkvis = 6, kTypeLOAM = 7, kTypeMSCKF = 8, - kTypeVINS = 9, + kTypeVINSFusion = 9, kTypeOpenVINS = 10, kTypeFLOAM = 11, kTypeOpen3D = 12 diff --git a/corelib/include/rtabmap/core/Parameters.h b/corelib/include/rtabmap/core/Parameters.h index 69a941b8..ad4b866c 100644 --- a/corelib/include/rtabmap/core/Parameters.h +++ b/corelib/include/rtabmap/core/Parameters.h @@ -606,8 +606,8 @@ class RTABMAP_CORE_EXPORT Parameters RTABMAP_PARAM(OdomMSCKF, InitCovExTrans, double, 0.000025, ""); RTABMAP_PARAM(OdomMSCKF, MaxCamStateSize, int, 20, ""); - // Odometry VINS - RTABMAP_PARAM_STR(OdomVINS, ConfigPath, "", "Path of VINS config file."); + // Odometry VINS-Fusion + RTABMAP_PARAM_STR(OdomVINSFusion, ConfigPath, "", "Path of VINS-Fusion config file."); // Odometry OpenVINS RTABMAP_PARAM(OdomOpenVINS, UseStereo, bool, true, "If we have more than 1 camera, if we should try to track stereo constraints between pairs"); diff --git a/corelib/include/rtabmap/core/odometry/OdometryVINS.h b/corelib/include/rtabmap/core/odometry/OdometryVINS.h index 030de4a3..bd1a3e64 100644 --- a/corelib/include/rtabmap/core/odometry/OdometryVINS.h +++ b/corelib/include/rtabmap/core/odometry/OdometryVINS.h @@ -25,39 +25,7 @@ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#ifndef ODOMETRYVINS_H_ -#define ODOMETRYVINS_H_ +#pragma once +#pragma message("Warning: OdometryVINS.h is deprecated. Please use OdometryVINSFusion.h instead.") -#include - -namespace rtabmap { - -class VinsEstimator; - -class RTABMAP_CORE_EXPORT OdometryVINS : public Odometry -{ -public: - OdometryVINS(const rtabmap::ParametersMap & parameters = rtabmap::ParametersMap()); - virtual ~OdometryVINS(); - - virtual void reset(const Transform & initialPose = Transform::getIdentity()); - virtual Odometry::Type getType() {return Odometry::kTypeVINS;} - virtual bool canProcessRawImages() const {return true;} - virtual bool canProcessAsyncIMU() const {return true;} - -private: - virtual Transform computeTransform(SensorData & image, const Transform & guess = Transform(), OdometryInfo * info = 0); - -private: -#ifdef RTABMAP_VINS - VinsEstimator * vinsEstimator_; - bool initGravity_; - Transform previousPose_; - Transform previousLocalTransform_; - IMU lastImu_; -#endif -}; - -} - -#endif /* ODOMETRYVINS_H_ */ +#include "rtabmap/core/odometry/OdometryVINSFusion.h" diff --git a/corelib/include/rtabmap/core/odometry/OdometryVINSFusion.h b/corelib/include/rtabmap/core/odometry/OdometryVINSFusion.h new file mode 100644 index 00000000..e5059d22 --- /dev/null +++ b/corelib/include/rtabmap/core/odometry/OdometryVINSFusion.h @@ -0,0 +1,64 @@ +/* +Copyright (c) 2010-2016, Mathieu Labbe - IntRoLab - Universite de Sherbrooke +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Universite de Sherbrooke nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef ODOMETRYVINSFUSION_H_ +#define ODOMETRYVINSFUSION_H_ + +#include + +namespace rtabmap { + +class VinsFusionEstimator; + +class RTABMAP_CORE_EXPORT OdometryVINSFusion : public Odometry +{ +public: + OdometryVINSFusion(const rtabmap::ParametersMap & parameters = rtabmap::ParametersMap()); + virtual ~OdometryVINSFusion(); + + virtual void reset(const Transform & initialPose = Transform::getIdentity()); + virtual Odometry::Type getType() {return Odometry::kTypeVINSFusion;} + virtual bool canProcessRawImages() const {return true;} + virtual bool canProcessAsyncIMU() const {return true;} + +private: + virtual Transform computeTransform(SensorData & image, const Transform & guess = Transform(), OdometryInfo * info = 0); + +private: +#ifdef RTABMAP_VINS_FUSION + VinsFusionEstimator * vinsEstimator_; + bool initGravity_; + Transform previousPose_; + Transform previousLocalTransform_; + IMU lastImu_; + double lastImuStamp_; +#endif +}; + +} + +#endif /* ODOMETRYVINSFUSION_H_ */ diff --git a/corelib/src/CMakeLists.txt b/corelib/src/CMakeLists.txt index d0205f7c..bc2cd392 100644 --- a/corelib/src/CMakeLists.txt +++ b/corelib/src/CMakeLists.txt @@ -98,7 +98,7 @@ SET(SRC_FILES odometry/OdometryLOAM.cpp odometry/OdometryFLOAM.cpp odometry/OdometryMSCKF.cpp - odometry/OdometryVINS.cpp + odometry/OdometryVINSFusion.cpp odometry/OdometryOpenVINS.cpp odometry/OdometryOpen3D.cpp diff --git a/corelib/src/Odometry.cpp b/corelib/src/Odometry.cpp index 2b25e15c..c5c35641 100644 --- a/corelib/src/Odometry.cpp +++ b/corelib/src/Odometry.cpp @@ -36,7 +36,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/core/odometry/OdometryLOAM.h" #include "rtabmap/core/odometry/OdometryFLOAM.h" #include "rtabmap/core/odometry/OdometryMSCKF.h" -#include "rtabmap/core/odometry/OdometryVINS.h" +#include "rtabmap/core/odometry/OdometryVINSFusion.h" #include "rtabmap/core/odometry/OdometryOpenVINS.h" #include "rtabmap/core/odometry/OdometryOpen3D.h" #include "rtabmap/core/OdometryInfo.h" @@ -103,8 +103,8 @@ Odometry * Odometry::create(Odometry::Type & type, const ParametersMap & paramet case Odometry::kTypeMSCKF: odometry = new OdometryMSCKF(parameters); break; - case Odometry::kTypeVINS: - odometry = new OdometryVINS(parameters); + case Odometry::kTypeVINSFusion: + odometry = new OdometryVINSFusion(parameters); break; case Odometry::kTypeOpenVINS: odometry = new OdometryOpenVINS(parameters); diff --git a/corelib/src/Parameters.cpp b/corelib/src/Parameters.cpp index 03d767ca..be0300b0 100644 --- a/corelib/src/Parameters.cpp +++ b/corelib/src/Parameters.cpp @@ -236,6 +236,9 @@ const std::map > & Parameters::getRemo { // removed parameters + // 0.23.1 + removedParameters_.insert(std::make_pair("OdomVINS/ConfigPath", std::make_pair(true, Parameters::kOdomVINSFusionConfigPath()))); + // 0.21.13 removedParameters_.insert(std::make_pair("Vis/ForwardEstOnly", std::make_pair(false, ""))); @@ -936,7 +939,7 @@ ParametersMap Parameters::parseArguments(int argc, char * argv[], bool onlyParam std::cout << str << std::setw(spacing - str.size()) << "false" << std::endl; #endif str = "With VINS-Fusion:"; -#ifdef RTABMAP_VINS +#ifdef RTABMAP_VINS_FUSION std::cout << str << std::setw(spacing - str.size()) << "true" << std::endl; #else std::cout << str << std::setw(spacing - str.size()) << "false" << std::endl; diff --git a/corelib/src/odometry/OdometryVINS.cpp b/corelib/src/odometry/OdometryVINSFusion.cpp similarity index 65% rename from corelib/src/odometry/OdometryVINS.cpp rename to corelib/src/odometry/OdometryVINSFusion.cpp index aa18393c..93df6353 100644 --- a/corelib/src/odometry/OdometryVINS.cpp +++ b/corelib/src/odometry/OdometryVINSFusion.cpp @@ -25,7 +25,7 @@ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#include "rtabmap/core/odometry/OdometryVINS.h" +#include "rtabmap/core/odometry/OdometryVINSFusion.h" #include "rtabmap/core/OdometryInfo.h" #include "rtabmap/core/util3d_transforms.h" #include "rtabmap/utilite/ULogger.h" @@ -35,7 +35,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/utilite/UDirectory.h" #include -#ifdef RTABMAP_VINS +#ifdef RTABMAP_VINS_FUSION #include #include #include @@ -45,11 +45,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. namespace rtabmap { -#ifdef RTABMAP_VINS -class VinsEstimator: public Estimator +#ifdef RTABMAP_VINS_FUSION +class VinsFusionEstimator: public Estimator { public: - VinsEstimator( + VinsFusionEstimator( const Transform & imuLocalTransform, const StereoCameraModel & model, bool rectified) : Estimator() @@ -57,6 +57,9 @@ public: MULTIPLE_THREAD = 0; setParameter(); + ROW=model.left().imageHeight(); + COL=model.left().imageWidth(); + //overwrite camera calibration only if received model is radtan, otherwise use config UASSERT(NUM_OF_CAM >= 1 && NUM_OF_CAM <=2); @@ -81,6 +84,15 @@ public: camera->setParameters(params); featureTracker.m_camera.push_back(camera); + double originalParalax = MIN_PARALLAX * FOCAL_LENGTH; + // If you have compiler error about FOCAL_LENGTH being const, make sure to use the following patch: + // https://gist.github.com/matlabbe/795ab37067367dca58bbadd8201d986c#file-vins-fusion_pull136-patch + FOCAL_LENGTH = params.fx(); + MIN_PARALLAX = originalParalax / FOCAL_LENGTH; + ProjectionTwoFrameOneCamFactor::sqrt_info = FOCAL_LENGTH / 1.5 * Matrix2d::Identity(); + ProjectionTwoFrameTwoCamFactor::sqrt_info = FOCAL_LENGTH / 1.5 * Matrix2d::Identity(); + ProjectionOneFrameTwoCamFactor::sqrt_info = FOCAL_LENGTH / 1.5 * Matrix2d::Identity(); + if(NUM_OF_CAM == 2) { camodocal::PinholeCameraPtr camera( new camodocal::PinholeCamera ); @@ -120,8 +132,8 @@ public: Transform imuCam0 = imuLocalTransform.inverse() * model.localTransform(); - tic[0] = Vector3d(imuCam0.x(), imuCam0.y(), imuCam0.z()); - ric[0] = imuCam0.toEigen4d().block<3,3>(0,0); + tic[0] = TIC[0] = Vector3d(imuCam0.x(), imuCam0.y(), imuCam0.z()); + ric[0] = RIC[0] = imuCam0.toEigen4d().block<3,3>(0,0); if(NUM_OF_CAM == 2) { @@ -140,54 +152,39 @@ public: UASSERT(!cam0cam1.isNull()); Transform imuCam1 = imuCam0 * cam0cam1; - tic[1] = Vector3d(imuCam1.x(), imuCam1.y(), imuCam1.z()); - ric[1] = imuCam1.toEigen4d().block<3,3>(0,0); + tic[1] = TIC[0] = Vector3d(imuCam1.x(), imuCam1.y(), imuCam1.z()); + ric[1] = RIC[0] = imuCam1.toEigen4d().block<3,3>(0,0); } for (int i = 0; i < NUM_OF_CAM; i++) { - cout << " exitrinsic cam " << i << endl << ric[i] << endl << tic[i].transpose() << endl; + cout << " new extrinsic cam " << i << endl << ric[i] << endl << tic[i].transpose() << endl; + } + for (int i = 0; i < NUM_OF_CAM; i++) + { + cout << " new intrinsic cam " << i << endl << featureTracker.m_camera[i]->parametersToString() << endl; } f_manager.setRic(ric); - ProjectionTwoFrameOneCamFactor::sqrt_info = FOCAL_LENGTH / 1.5 * Matrix2d::Identity(); - ProjectionTwoFrameTwoCamFactor::sqrt_info = FOCAL_LENGTH / 1.5 * Matrix2d::Identity(); - ProjectionOneFrameTwoCamFactor::sqrt_info = FOCAL_LENGTH / 1.5 * Matrix2d::Identity(); - td = TD; - g = G; - cout << "set g " << g.transpose() << endl; } // Copy of original inputImage() so that overridden processMeasurements() is used and threading is disabled. void inputImage(double t, const cv::Mat &_img, const cv::Mat &_img1) { + TicToc processTime; inputImageCnt++; map>>> featureFrame; - TicToc featureTrackerTime; - if(_img1.empty()) + if(_img1.empty()) { featureFrame = featureTracker.trackImage(t, _img); - else + } + else { featureFrame = featureTracker.trackImage(t, _img, _img1); - //printf("featureTracker time: %f\n", featureTrackerTime.toc()); - - //if(MULTIPLE_THREAD) - //{ - // if(inputImageCnt % 2 == 0) - // { - // mBuf.lock(); - // featureBuf.push(make_pair(t, featureFrame)); - // mBuf.unlock(); - // } - //} - //else - { - mBuf.lock(); - featureBuf.push(make_pair(t, featureFrame)); - mBuf.unlock(); - TicToc processTime; - processMeasurements(); - UDEBUG("VINS process time: %f", processTime.toc()); - } + } + mBuf.lock(); + featureBuf.push(make_pair(t, featureFrame)); + mBuf.unlock(); + processMeasurements(); + UDEBUG("VINS process time: %f", processTime.toc()); } // Copy of original inputIMU() but with publisher commented @@ -199,127 +196,98 @@ public: //printf("input imu with time %f \n", t); mBuf.unlock(); - fastPredictIMU(t, linearAcceleration, angularVelocity); - //if (solver_flag == NON_LINEAR) - // pubLatestOdometry(latest_P, latest_Q, latest_V, t); + if (solver_flag == NON_LINEAR) + { + mPropagate.lock(); + fastPredictIMU(t, linearAcceleration, angularVelocity); + mPropagate.unlock(); + } } // Copy of original processMeasurements() but with publishers commented and threading disabled void processMeasurements() { - //while (1) + pair > > > > feature; + vector> accVector, gyrVector; + if(!featureBuf.empty()) { - //printf("process measurments\n"); - pair > > > > feature; - vector> accVector, gyrVector; - if(!featureBuf.empty()) + feature = featureBuf.front(); + curTime = feature.first + td; + if (USE_IMU && !IMUAvailable(feature.first + td)) { - feature = featureBuf.front(); - curTime = feature.first + td; - //while(1) - //{ - if (!((!USE_IMU || IMUAvailable(feature.first + td)))) - //if ((!USE_IMU || IMUAvailable(feature.first + td))) - // break; - //else - { - printf("wait for imu ... \n"); - //if (! MULTIPLE_THREAD) - return; - //std::chrono::milliseconds dura(5); - //std::this_thread::sleep_for(dura); - } - //} - mBuf.lock(); - if(USE_IMU) - getIMUInterval(prevTime, curTime, accVector, gyrVector); - - featureBuf.pop(); - mBuf.unlock(); - - if(USE_IMU) - { - if(!initFirstPoseFlag) - initFirstIMUPose(accVector); - UDEBUG("accVector.size() = %d", accVector.size()); - for(size_t i = 0; i < accVector.size(); i++) - { - double dt; - if(i == 0) - dt = accVector[i].first - prevTime; - else if (i == accVector.size() - 1) - dt = curTime - accVector[i - 1].first; - else - dt = accVector[i].first - accVector[i - 1].first; - processIMU(accVector[i].first, dt, accVector[i].second, gyrVector[i].second); - } - } - - processImage(feature.second, feature.first); - - prevTime = curTime; - - printStatistics(*this, 0); - - //std_msgs::Header header; - //header.frame_id = "world"; - //header.stamp = ros::Time(feature.first); - - //pubOdometry(*this, header); - //pubKeyPoses(*this, header); - //pubCameraPose(*this, header); - //pubPointCloud(*this, header); - //pubKeyframe(*this); - //pubTF(*this, header); + printf("wait for imu ... \n"); + return; } + mBuf.lock(); + if(USE_IMU) + getIMUInterval(prevTime, curTime, accVector, gyrVector); - //if (! MULTIPLE_THREAD) - // break; + featureBuf.pop(); + mBuf.unlock(); - //std::chrono::milliseconds dura(2); - //std::this_thread::sleep_for(dura); + if(USE_IMU) + { + if(!initFirstPoseFlag) + initFirstIMUPose(accVector); + + for(size_t i = 0; i < accVector.size(); i++) + { + double dt; + if(i == 0) + dt = accVector[i].first - prevTime; + else if (i == accVector.size() - 1) + dt = curTime - accVector[i - 1].first; + else + dt = accVector[i].first - accVector[i - 1].first; + processIMU(accVector[i].first, dt, accVector[i].second, gyrVector[i].second); + } + } + mProcess.lock(); + processImage(feature.second, feature.first); + prevTime = curTime; + mProcess.unlock(); } } }; #endif -OdometryVINS::OdometryVINS(const ParametersMap & parameters) : +OdometryVINSFusion::OdometryVINSFusion(const ParametersMap & parameters) : Odometry(parameters) -#ifdef RTABMAP_VINS +#ifdef RTABMAP_VINS_FUSION , vinsEstimator_(0), initGravity_(false), previousPose_(Transform::getIdentity()) #endif { -#ifdef RTABMAP_VINS +#ifdef RTABMAP_VINS_FUSION // intialize std::string configFilename; - Parameters::parse(parameters, Parameters::kOdomVINSConfigPath(), configFilename); + Parameters::parse(parameters, Parameters::kOdomVINSFusionConfigPath(), configFilename); if(configFilename.empty()) { - UERROR("VINS config file is empty (%s=%s)!", - Parameters::kOdomVINSConfigPath().c_str(), - Parameters::kOdomVINSConfigPath().c_str()); + UERROR("VINS config file is empty (%s)!", + Parameters::kOdomVINSFusionConfigPath().c_str()); } else { + UINFO("Using config file %s", configFilename.c_str()); readParameters(uReplaceChar(configFilename, '~', UDirectory::homeDir())); } #endif } -OdometryVINS::~OdometryVINS() +OdometryVINSFusion::~OdometryVINSFusion() { -#ifdef RTABMAP_VINS +#ifdef RTABMAP_VINS_FUSION delete vinsEstimator_; #endif } -void OdometryVINS::reset(const Transform & initialPose) +void OdometryVINSFusion::reset(const Transform & initialPose) { Odometry::reset(initialPose); -#ifdef RTABMAP_VINS +#ifdef RTABMAP_VINS_FUSION if(!initGravity_) { delete vinsEstimator_; @@ -333,18 +301,19 @@ void OdometryVINS::reset(const Transform & initialPose) } // return not null transform if odometry is correctly computed -Transform OdometryVINS::computeTransform( +Transform OdometryVINSFusion::computeTransform( SensorData & data, const Transform & guess, OdometryInfo * info) { Transform t; -#ifdef RTABMAP_VINS +#ifdef RTABMAP_VINS_FUSION UTimer timer; + bool hasImage = !data.imageRaw().empty() && !data.rightRaw().empty() && data.stereoCameraModels().size() == 1 && data.stereoCameraModels()[0].isValidForProjection(); + if(USE_IMU!=0 && !data.imu().empty()) { - double t = data.stamp(); double dx = data.imu().linearAcceleration().val[0]; double dy = data.imu().linearAcceleration().val[1]; double dz = data.imu().linearAcceleration().val[2]; @@ -358,16 +327,19 @@ Transform OdometryVINS::computeTransform( if(vinsEstimator_ != 0) { - vinsEstimator_->inputIMU(t, acc, gyr); + vinsEstimator_->inputIMU(data.stamp(), acc, gyr); } else { lastImu_ = data.imu(); - UWARN("Waiting an image for initialization..."); + lastImuStamp_ = data.stamp(); + if(!hasImage) { + UWARN("Waiting an image for initialization..."); + } } } - if(!data.imageRaw().empty() && !data.rightRaw().empty() && data.stereoCameraModels().size() == 1 && data.stereoCameraModels()[0].isValidForProjection()) + if(hasImage) { if(USE_IMU==1 && lastImu_.localTransform().isNull()) { @@ -377,10 +349,23 @@ Transform OdometryVINS::computeTransform( if(vinsEstimator_ == 0) { // intialize - vinsEstimator_ = new VinsEstimator( + UINFO("Initializing with image %f", data.stamp()); + vinsEstimator_ = new VinsFusionEstimator( lastImu_.localTransform().isNull()?Transform::getIdentity():lastImu_.localTransform(), data.stereoCameraModels()[0], this->imagesAlreadyRectified()); + + if(USE_IMU) { + double dx = lastImu_.linearAcceleration().val[0]; + double dy = lastImu_.linearAcceleration().val[1]; + double dz = lastImu_.linearAcceleration().val[2]; + double rx = lastImu_.angularVelocity().val[0]; + double ry = lastImu_.angularVelocity().val[1]; + double rz = lastImu_.angularVelocity().val[2]; + Vector3d acc(dx, dy, dz); + Vector3d gyr(rx, ry, rz); + vinsEstimator_->inputIMU(lastImuStamp_, acc, gyr); + } } UDEBUG("Image update stamp=%f", data.stamp()); @@ -455,36 +440,45 @@ Transform OdometryVINS::computeTransform( info->reg.covariance = cv::Mat::eye(6,6, CV_64FC1); info->reg.covariance *= this->framesProcessed() == 0?9999:0.0001; - // feature map - Transform fixT = this->getPose()*previousPoseInv; + // feature map: based on code from pubPointCloud() of vins's visualization.cpp for (auto &it_per_id : vinsEstimator_->f_manager.feature) { - int used_num; - used_num = it_per_id.feature_per_frame.size(); - if (!(used_num >= 2 && it_per_id.start_frame < WINDOW_SIZE - 2)) - continue; - if (it_per_id.start_frame > WINDOW_SIZE * 3.0 / 4.0 || it_per_id.solve_flag != 1) + if(it_per_id.feature_per_frame.size() < 2) { + // feature just added but not tracked, or old feature not tracked anymore continue; + } + int imu_i = it_per_id.start_frame; - Vector3d pts_i = it_per_id.feature_per_frame[it_per_id.feature_per_frame.size()-1].point * it_per_id.estimated_depth; + Vector3d pts_i = it_per_id.feature_per_frame[0].point * it_per_id.estimated_depth; Vector3d w_pts_i = vinsEstimator_->Rs[imu_i] * (vinsEstimator_->ric[0] * pts_i + vinsEstimator_->tic[0]) + vinsEstimator_->Ps[imu_i]; cv::Point3f p; p.x = w_pts_i(0); p.y = w_pts_i(1); p.z = w_pts_i(2); - p = util3d::transformPoint(p, fixT); - info->localMap.insert(std::make_pair(it_per_id.feature_id, p)); - if(this->imagesAlreadyRectified()) - { - cv::Point2f pt; - data.stereoCameraModels()[0].left().reproject(pts_i(0), pts_i(1), pts_i(2), pt.x, pt.y); - info->reg.inliersIDs.push_back(info->newCorners.size()); - info->newCorners.push_back(pt); + int featureIndex = info->localMap.size(); + info->localMap.insert(std::make_pair(featureIndex, p)); + + FeaturePerFrame & refFrame = it_per_id.feature_per_frame[0]; // First frame it was seen + FeaturePerFrame & newFrame = it_per_id.feature_per_frame[it_per_id.feature_per_frame.size()-1]; // Last frame it was seen (not necessary in last frame) + cv::Point2f refUV(refFrame.uv[0], refFrame.uv[1]); + cv::Point2f newUV(newFrame.uv[0], newFrame.uv[1]); + info->refCorners.push_back(refUV); + info->newCorners.push_back(newUV); + + info->reg.matchesIDs.push_back(featureIndex); + if(it_per_id.solve_flag > 0) { + // Feature correctly tracked + info->words.insert(std::make_pair(featureIndex, cv::KeyPoint(newUV, 3.0f))); + info->cornerInliers.push_back(featureIndex); + info->reg.inliersIDs.push_back(featureIndex); } + + ++featureIndex; } - info->features = info->newCorners.size(); + info->features = info->localMap.size(); + info->reg.inliers = info->reg.inliersIDs.size(); info->localMapSize = info->localMap.size(); } UINFO("Odom update time = %fs p=%s", timer.elapsed(), p.prettyPrint().c_str()); @@ -492,7 +486,7 @@ Transform OdometryVINS::computeTransform( } else { - UWARN("VINS not yet initialized... waiting to get enough IMU messages"); + UWARN("VINS-Fusion not yet initialized... needing more data."); } } else if(!data.imageRaw().empty() && !data.depthRaw().empty()) diff --git a/guilib/include/rtabmap/gui/PreferencesDialog.h b/guilib/include/rtabmap/gui/PreferencesDialog.h index df34b25a..441c41b8 100644 --- a/guilib/include/rtabmap/gui/PreferencesDialog.h +++ b/guilib/include/rtabmap/gui/PreferencesDialog.h @@ -367,7 +367,7 @@ private Q_SLOTS: void changeDictionaryPath(); void changeOdometryORBSLAMVocabulary(); void changeOdometryOKVISConfigPath(); - void changeOdometryVINSConfigPath(); + void changeOdometryVINSFusionConfigPath(); void changeOdometryOpenVINSLeftMask(); void changeOdometryOpenVINSRightMask(); void changeIcpPMConfigPath(); diff --git a/guilib/src/AboutDialog.cpp b/guilib/src/AboutDialog.cpp index 69a34000..c12cb8fd 100644 --- a/guilib/src/AboutDialog.cpp +++ b/guilib/src/AboutDialog.cpp @@ -281,7 +281,7 @@ AboutDialog::AboutDialog(QWidget * parent) : _ui->label_msckf_license->setEnabled(false); #endif -#ifdef RTABMAP_VINS +#ifdef RTABMAP_VINS_FUSION _ui->label_vins_fusion->setText("Yes"); _ui->label_vins_fusion_license->setEnabled(true); #else diff --git a/guilib/src/MainWindow.cpp b/guilib/src/MainWindow.cpp index df757ba9..e4ae64d6 100644 --- a/guilib/src/MainWindow.cpp +++ b/guilib/src/MainWindow.cpp @@ -1675,7 +1675,7 @@ void MainWindow::processOdometry(const rtabmap::OdometryEvent & odom, bool dataI odom.info().type == (int)Odometry::kTypeViso2 || odom.info().type == (int)Odometry::kTypeFovis || odom.info().type == (int)Odometry::kTypeMSCKF || - odom.info().type == (int)Odometry::kTypeVINS || + odom.info().type == (int)Odometry::kTypeVINSFusion || odom.info().type == (int)Odometry::kTypeOpenVINS) { std::vector kpts; @@ -1725,7 +1725,6 @@ void MainWindow::processOdometry(const rtabmap::OdometryEvent & odom, bool dataI if( odom.info().type == (int)Odometry::kTypeF2M || odom.info().type == (int)Odometry::kTypeORBSLAM || odom.info().type == (int)Odometry::kTypeMSCKF || - odom.info().type == (int)Odometry::kTypeVINS || odom.info().type == (int)Odometry::kTypeOpenVINS) { if(_ui->imageView_odometry->isFeaturesShown() && !_preferencesDialog->isOdomOnlyInliersShown()) @@ -1742,6 +1741,7 @@ void MainWindow::processOdometry(const rtabmap::OdometryEvent & odom, bool dataI } if((odom.info().type == (int)Odometry::kTypeF2F || odom.info().type == (int)Odometry::kTypeViso2 || + odom.info().type == (int)Odometry::kTypeVINSFusion || odom.info().type == (int)Odometry::kTypeFovis) && odom.info().refCorners.size()) { if(_ui->imageView_odometry->isFeaturesShown() || _ui->imageView_odometry->isLinesShown()) diff --git a/guilib/src/PreferencesDialog.cpp b/guilib/src/PreferencesDialog.cpp index 92be1ca1..b02ab32c 100644 --- a/guilib/src/PreferencesDialog.cpp +++ b/guilib/src/PreferencesDialog.cpp @@ -226,7 +226,7 @@ PreferencesDialog::PreferencesDialog(QWidget * parent) : #ifndef RTABMAP_MSCKF_VIO _ui->odom_strategy->setItemData(8, 0, Qt::UserRole - 1); #endif -#ifndef RTABMAP_VINS +#ifndef RTABMAP_VINS_FUSION _ui->odom_strategy->setItemData(9, 0, Qt::UserRole - 1); #endif #ifndef RTABMAP_OPENVINS @@ -1556,8 +1556,8 @@ PreferencesDialog::PreferencesDialog(QWidget * parent) : _ui->OdomMSCKFInitCovExTrans->setObjectName(Parameters::kOdomMSCKFInitCovExTrans().c_str()); // Odometry VINS - _ui->lineEdit_OdomVinsPath->setObjectName(Parameters::kOdomVINSConfigPath().c_str()); - connect(_ui->toolButton_OdomVinsPath, SIGNAL(clicked()), this, SLOT(changeOdometryVINSConfigPath())); + _ui->lineEdit_OdomVinsFusionPath->setObjectName(Parameters::kOdomVINSFusionConfigPath().c_str()); + connect(_ui->toolButton_OdomVinsFusionPath, SIGNAL(clicked()), this, SLOT(changeOdometryVINSFusionConfigPath())); // Odometry OpenVINS _ui->checkBox_OdomOpenVINSUseStereo->setObjectName(Parameters::kOdomOpenVINSUseStereo().c_str()); @@ -5476,7 +5476,7 @@ void PreferencesDialog::updateOdometryStackedIndex(int index) _ui->groupBox_odomOKVIS->setVisible(index==6); _ui->groupBox_odomLOAM->setVisible(index==7); _ui->groupBox_odomMSCKF->setVisible(index==8); - _ui->groupBox_odomVINS->setVisible(index==9); + _ui->groupBox_odomVINSFusion->setVisible(index==9); _ui->groupBox_odomOpenVINS->setVisible(index==10); _ui->groupBox_odomOpen3D->setVisible(index==12); } @@ -5567,20 +5567,20 @@ void PreferencesDialog::changeOdometryOKVISConfigPath() } } -void PreferencesDialog::changeOdometryVINSConfigPath() +void PreferencesDialog::changeOdometryVINSFusionConfigPath() { QString path; - if(_ui->lineEdit_OdomVinsPath->text().isEmpty()) + if(_ui->lineEdit_OdomVinsFusionPath->text().isEmpty()) { path = QFileDialog::getOpenFileName(this, tr("VINS-Fusion Config"), this->getWorkingDirectory(), tr("VINS-Fusion config (*.yaml)")); } else { - path = QFileDialog::getOpenFileName(this, tr("VINS-Fusion Config"), _ui->lineEdit_OdomVinsPath->text(), tr("VINS-Fusion config (*.yaml)")); + path = QFileDialog::getOpenFileName(this, tr("VINS-Fusion Config"), _ui->lineEdit_OdomVinsFusionPath->text(), tr("VINS-Fusion config (*.yaml)")); } if(!path.isEmpty()) { - _ui->lineEdit_OdomVinsPath->setText(path); + _ui->lineEdit_OdomVinsFusionPath->setText(path); } } diff --git a/guilib/src/ui/preferencesDialog.ui b/guilib/src/ui/preferencesDialog.ui index 1c3bddbe..a1ccfd7f 100644 --- a/guilib/src/ui/preferencesDialog.ui +++ b/guilib/src/ui/preferencesDialog.ui @@ -19461,7 +19461,7 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - + VINS-Fusion @@ -19485,7 +19485,7 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - + @@ -19501,7 +19501,7 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - + ... From c774ef641ac9605b2de0fb5a70b3ec002e5d9201 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sat, 20 Sep 2025 18:08:38 -0700 Subject: [PATCH 019/126] rgbd_dataset: added support for rgbd_bonn ground truth format --- corelib/include/rtabmap/core/Graph.h | 2 +- .../rtabmap/core/camera/CameraImages.h | 4 +- corelib/src/Graph.cpp | 18 ++++++++- corelib/src/camera/CameraImages.cpp | 6 +-- guilib/src/ui/preferencesDialog.ui | 16 ++++++-- tools/RgbdDataset/main.cpp | 39 +++++++++++++++++-- 6 files changed, 70 insertions(+), 15 deletions(-) diff --git a/corelib/include/rtabmap/core/Graph.h b/corelib/include/rtabmap/core/Graph.h index 974830cd..010b74b8 100644 --- a/corelib/include/rtabmap/core/Graph.h +++ b/corelib/include/rtabmap/core/Graph.h @@ -56,7 +56,7 @@ bool RTABMAP_CORE_EXPORT exportPoses( bool RTABMAP_CORE_EXPORT importPoses( const std::string & filePath, - int format, // 0=Raw, 1=RGBD-SLAM motion capture (10=without change of coordinate frame, 11=10+ID), 2=KITTI, 3=TORO, 4=g2o, 5=NewCollege(t,x,y), 6=Malaga Urban GPS, 7=St Lucia INS, 8=Karlsruhe, 9=EuRoC MAV + int format, // 0=Raw, 1=RGBD-SLAM motion capture (10=without change of coordinate frame, 11=10+ID), 2=KITTI, 3=TORO, 4=g2o, 5=NewCollege(t,x,y), 6=Malaga Urban GPS, 7=St Lucia INS, 8=Karlsruhe, 9=EuRoC MAV, 12=rgbd_bonn std::map & poses, std::multimap * constraints = 0, // optional for formats 3 and 4 std::map * stamps = 0); // optional for format 1 and 9 diff --git a/corelib/include/rtabmap/core/camera/CameraImages.h b/corelib/include/rtabmap/core/camera/CameraImages.h index 9b7134dc..cbdbcd78 100644 --- a/corelib/include/rtabmap/core/camera/CameraImages.h +++ b/corelib/include/rtabmap/core/camera/CameraImages.h @@ -94,14 +94,14 @@ public: _depthFromScanFillHolesFromBorder = fillHolesFromBorder; } - // Format: 0=Raw, 1=RGBD-SLAM, 2=KITTI, 3=TORO, 4=g2o, 5=NewCollege(t,x,y), 6=Malaga Urban GPS, 7=St Lucia INS, 8=Karlsruhe + // 0=Raw, 1=RGBD-SLAM motion capture (10=without change of coordinate frame, 11=10+ID), 2=KITTI, 3=TORO, 4=g2o, 5=NewCollege(t,x,y), 6=Malaga Urban GPS, 7=St Lucia INS, 8=Karlsruhe, 9=EuRoC MAV, 12=rgbd_bonn void setOdometryPath(const std::string & filePath, int format = 0) { _odometryPath = filePath; _odometryFormat = format; } - // Format: 0=Raw, 1=RGBD-SLAM, 2=KITTI, 3=TORO, 4=g2o, 5=NewCollege(t,x,y), 6=Malaga Urban GPS, 7=St Lucia INS, 8=Karlsruhe + // 0=Raw, 1=RGBD-SLAM motion capture (10=without change of coordinate frame, 11=10+ID), 2=KITTI, 3=TORO, 4=g2o, 5=NewCollege(t,x,y), 6=Malaga Urban GPS, 7=St Lucia INS, 8=Karlsruhe, 9=EuRoC MAV, 12=rgbd_bonn void setGroundTruthPath(const std::string & filePath, int format = 0) { _groundTruthPath = filePath; diff --git a/corelib/src/Graph.cpp b/corelib/src/Graph.cpp index bf605853..8bbcf660 100644 --- a/corelib/src/Graph.cpp +++ b/corelib/src/Graph.cpp @@ -196,7 +196,7 @@ bool exportPoses( bool importPoses( const std::string & filePath, - int format, // 0=Raw, 1=RGBD-SLAM motion capture (10=without change of coordinate frame, 11=10+ID), 2=KITTI, 3=TORO, 4=g2o, 5=NewCollege(t,x,y), 6=Malaga Urban GPS, 7=St Lucia INS, 8=Karlsruhe, 9=EuRoC MAV + int format, // 0=Raw, 1=RGBD-SLAM motion capture (10=without change of coordinate frame, 11=10+ID), 2=KITTI, 3=TORO, 4=g2o, 5=NewCollege(t,x,y), 6=Malaga Urban GPS, 7=St Lucia INS, 8=Karlsruhe, 9=EuRoC MAV, 12=rgbd_bonn std::map & poses, std::multimap * constraints, // optional for formats 3 and 4 std::map * stamps) // optional for format 1 and 9 @@ -440,7 +440,7 @@ bool importPoses( UERROR("Error parsing \"%s\" with NewCollege format (should have 3 values: stamp x y, found %d)", str.c_str(), (int)strList.size()); } } - else if(format == 1 || format==10 || format==11) // rgbd-slam format + else if(format == 1 || format==10 || format==11 || format==12) // rgbd-slam format { std::list strList = uSplit(str); if((strList.size() >= 8 && format!=11) || (strList.size() == 9 && format==11)) @@ -481,6 +481,20 @@ bool importPoses( 1, 0, 0, 0); pose = t*pose; } + else if(format == 12) + { + // See https://www.ipb.uni-bonn.de/data/rgbd-dynamic-dataset/index.html + Transform T_ros(-1, 0, 0, 0, + 0, 0, 1, 0, + 0, 1, 0, 0); + Transform T_m( + 1.0157, 0.1828, -0.2389, 0.0113, + 0.0009, -0.8431, -0.6413, -0.00980, + -0.3009, 0.6147, -0.8085, 0.0111); + + // we remove the optical rotation + pose = T_ros*pose*T_ros*T_m*CameraModel::opticalRotation().inverse(); + } poses.insert(std::make_pair(id, pose)); } } diff --git a/corelib/src/camera/CameraImages.cpp b/corelib/src/camera/CameraImages.cpp index 0591181a..3b192778 100644 --- a/corelib/src/camera/CameraImages.cpp +++ b/corelib/src/camera/CameraImages.cpp @@ -523,19 +523,19 @@ bool CameraImages::readPoses( UERROR("Cannot read pose file \"%s\".", filePath.c_str()); return false; } - else if((format != 1 && format != 10 && format != 5 && format != 6 && format != 7 && format != 9) && poses.size() != this->imagesCount()) + else if((format != 1 && format != 10 && format != 12 && format != 5 && format != 6 && format != 7 && format != 9) && poses.size() != this->imagesCount()) { UERROR("The pose count is not the same as the images (%d vs %d)! Please remove " "the pose file path if you don't want to use it (current file path=%s).", (int)poses.size(), this->imagesCount(), filePath.c_str()); return false; } - else if((format == 1 || format == 10 || format == 5 || format == 6 || format == 7 || format == 9) && (inOutStamps.empty() && stamps.size()!=poses.size())) + else if((format == 1 || format == 10 || format == 12 || format == 5 || format == 6 || format == 7 || format == 9) && (inOutStamps.empty() && stamps.size()!=poses.size())) { UERROR("When using RGBD-SLAM, GPS, MALAGA, ST LUCIA and EuRoC MAV formats, images must have timestamps!"); return false; } - else if(format == 1 || format == 10 || format == 5 || format == 6 || format == 7 || format == 9) + else if(format == 1 || format == 10 || format == 12 || format == 5 || format == 6 || format == 7 || format == 9) { UDEBUG(""); //Match ground truth values with images diff --git a/guilib/src/ui/preferencesDialog.ui b/guilib/src/ui/preferencesDialog.ui index a1ccfd7f..2653fcca 100644 --- a/guilib/src/ui/preferencesDialog.ui +++ b/guilib/src/ui/preferencesDialog.ui @@ -63,7 +63,7 @@ 0 - -1145 + 0 713 4705 @@ -95,7 +95,7 @@ QFrame::Raised - 9 + 5 @@ -3227,7 +3227,7 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - Local transform from /base_link to /camera_link. Mouse over the box to show formats. If odometry sensor is enabled, it is the transform to odometry sensor. + Local transform from /base_link to /camera_link (without optical transform). Mouse over the box to show formats. If odometry sensor is enabled, it is the transform to odometry sensor. true @@ -7437,6 +7437,11 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki RGBD-SLAM + ID + + + rgbd_bonn + + @@ -7582,6 +7587,11 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki RGBD-SLAM + ID + + + rgbd_bonn + + diff --git a/tools/RgbdDataset/main.cpp b/tools/RgbdDataset/main.cpp index 02f86fdb..74ed4b6d 100644 --- a/tools/RgbdDataset/main.cpp +++ b/tools/RgbdDataset/main.cpp @@ -58,6 +58,7 @@ void showUsage() " --output Output directory. By default, results are saved in \"path\".\n" " --output_name Output database name (default \"rtabmap\").\n" " --skip # Skip X frames.\n" + " --max_time_diff #.# Maximum time difference with frame to attribute a valid ground truth pose (default 0.02 s).\n" " --quiet Don't show log messages and iteration updates.\n" "%s\n" "Example:\n\n" @@ -92,6 +93,7 @@ int main(int argc, char * argv[]) std::string output; std::string outputName = "rtabmap"; int skipFrames = 0; + float maxTimeDiff = 0.02f; bool quiet = false; if(argc < 2) { @@ -114,6 +116,11 @@ int main(int argc, char * argv[]) skipFrames = atoi(argv[++i]); UASSERT(skipFrames > 0); } + else if(std::strcmp(argv[i], "--max_time_diff") == 0) + { + maxTimeDiff = atof(argv[++i]); + UASSERT(maxTimeDiff > 0.0f); + } else if(std::strcmp(argv[i], "--quiet") == 0) { quiet = true; @@ -158,14 +165,16 @@ int main(int argc, char * argv[]) " Depth path: %s\n" " Output: %s\n" " Output name: %s\n" - " Skip frames: %d\n", + " Skip frames: %d\n" + " Max time diff: %f\n", seq.c_str(), path.c_str(), pathRgbImages.c_str(), pathDepthImages.c_str(), output.c_str(), outputName.c_str(), - skipFrames); + skipFrames, + maxTimeDiff); if(!pathGt.empty()) { printf(" groundtruth.txt: %s\n", pathGt.c_str()); @@ -185,6 +194,7 @@ int main(int argc, char * argv[]) std::string sequenceName = UFile(path).getName(); Transform opticalRotation(0,0,1,0, -1,0,0,0, 0,-1,0,0); float depthFactor = 5.0f; + int gtPoseFormat = 1; if(sequenceName.find("freiburg1") != std::string::npos) { model = CameraModel(outputName+"_calib", 517.3, 516.5, 318.6, 255.3, opticalRotation, 0, cv::Size(640,480)); @@ -193,10 +203,30 @@ int main(int argc, char * argv[]) { model = CameraModel(outputName+"_calib", 520.9, 521.0, 325.1, 249.7, opticalRotation, 0, cv::Size(640,480)); } - else //if(sequenceName.find("freiburg3") != std::string::npos) + else if(sequenceName.find("freiburg3") != std::string::npos) { model = CameraModel(outputName+"_calib", 535.4, 539.2, 320.1, 247.6, opticalRotation, 0, cv::Size(640,480)); } + else if(sequenceName.find("rgbd_bonn") != std::string::npos) + { + cv::Mat K = cv::Mat::eye(3,3,CV_64FC1); + K.at(0,0) = 542.822841; // fx + K.at(1,1) = 542.576870; // fy + K.at(0,2) = 315.593520; // cx + K.at(1,2) = 237.756098; // cy + cv::Mat D = cv::Mat::eye(1,5,CV_64FC1); + D.at(0,0) = 0.039903; + D.at(0,1) = -0.099343; + D.at(0,2) = -0.000730; + D.at(0,3) = -0.000144; + D.at(0,4) = 0.000000; + + model = CameraModel(outputName+"_calib", cv::Size(640,480), K, D, cv::Mat(), cv::Mat(), opticalRotation); + gtPoseFormat = 12; + } + else { + printf("ERROR: Dataset %s is not supported. Update rgbd_dataset tool to include the right calibration parameter for this dataset!\n", sequenceName.c_str()); + } //parameters.insert(ParametersPair(Parameters::kg2oBaseline(), uNumber2Str(40.0f/model.fx()))); model.save(path); @@ -209,7 +239,8 @@ int main(int argc, char * argv[]) ((CameraRGBDImages*)cameraThread.camera())->setTimestamps(true, "", false); if(!pathGt.empty()) { - ((CameraRGBDImages*)cameraThread.camera())->setGroundTruthPath(pathGt, 1); + ((CameraRGBDImages*)cameraThread.camera())->setGroundTruthPath(pathGt, gtPoseFormat); + ((CameraRGBDImages*)cameraThread.camera())->setMaxPoseTimeDiff(maxTimeDiff); } bool intermediateNodes = Parameters::defaultRtabmapCreateIntermediateNodes(); From a552bdfb2fb35ddf2a6afdafcb6713818f43e3b2 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Wed, 24 Sep 2025 23:24:32 -0700 Subject: [PATCH 020/126] GUI: Support Optimizer/Robust with RGBD/OptimizeMaxError at the same time. DBReader: fixed repeated odom covariance when Mem/ReduceGraph is used. Memory: avoid error msg when receiving frames without features while we expect odometry features. See also #1573 --- corelib/include/rtabmap/core/Memory.h | 1 + corelib/include/rtabmap/core/Parameters.h | 8 ++-- corelib/src/DBReader.cpp | 47 ++++++++++++++++------- corelib/src/Memory.cpp | 29 +++++++++----- corelib/src/VWDictionary.cpp | 11 +++++- guilib/src/PreferencesDialog.cpp | 9 ----- guilib/src/ui/preferencesDialog.ui | 8 ++-- 7 files changed, 71 insertions(+), 42 deletions(-) diff --git a/corelib/include/rtabmap/core/Memory.h b/corelib/include/rtabmap/core/Memory.h index dc1dd43f..e3b24677 100644 --- a/corelib/include/rtabmap/core/Memory.h +++ b/corelib/include/rtabmap/core/Memory.h @@ -356,6 +356,7 @@ private: bool _linksChanged; // False by default, become true when links are modified. int _signaturesAdded; bool _allNodesInWM; + bool _receivingOdometryFeatures; GPS _gpsOrigin; std::vector _rectCameraModels; std::vector _rectStereoCameraModels; diff --git a/corelib/include/rtabmap/core/Parameters.h b/corelib/include/rtabmap/core/Parameters.h index ad4b866c..93f4d87c 100644 --- a/corelib/include/rtabmap/core/Parameters.h +++ b/corelib/include/rtabmap/core/Parameters.h @@ -213,8 +213,8 @@ class RTABMAP_CORE_EXPORT Parameters RTABMAP_PARAM_STR(Mem, DepthCompressionFormat, ".rvl", "Depth image compression format for 16UC1 depth type. It should be \".png\" or \".rvl\". If depth type is 32FC1, \".png\" is used."); RTABMAP_PARAM(Mem, STMSize, unsigned int, 10, "Short-term memory size."); RTABMAP_PARAM(Mem, IncrementalMemory, bool, true, "SLAM mode, otherwise it is Localization mode."); - RTABMAP_PARAM(Mem, LocalizationDataSaved, bool, false, uFormat("Save localization data during localization session (when %s=false). When enabled, the database will then also grow in localization mode. This mode would be used only for debugging purpose.", kMemIncrementalMemory().c_str()).c_str()); - RTABMAP_PARAM(Mem, ReduceGraph, bool, false, "Reduce graph. Merge nodes when loop closures are added (ignoring those with user data set)."); + RTABMAP_PARAM(Mem, LocalizationDataSaved, bool, false, uFormat("Save localization data during localization session (when %s=false). When enabled, the database will then also grow in localization mode. This mode would be used only for debugging purpose.", kMemIncrementalMemory().c_str()).c_str()); + RTABMAP_PARAM(Mem, ReduceGraph, bool, false, uFormat("Reduce graph. Merge nodes when loop closures are added (ignoring those with user data). Note that this approach assumes that 100%% of the loop closures accepted are good, so it is highly recommended to enable \"%s\" at the same time.", kRGBDOptimizeMaxError().c_str())); RTABMAP_PARAM(Mem, RecentWmRatio, float, 0.2, "Ratio of locations after the last loop closure in WM that cannot be transferred."); RTABMAP_PARAM(Mem, TransferSortingByWeightId, bool, false, "On transfer, signatures are sorted by weight->ID only (i.e. the oldest of the lowest weighted signatures are transferred first). If false, the signatures are sorted by weight->Age->ID (i.e. the oldest inserted in WM of the lowest weighted signatures are transferred first). Note that retrieval updates the age, not the ID."); RTABMAP_PARAM(Mem, RehearsalIdUpdatedToNewOne, bool, false, "On merge, update to new id. When false, no copy."); @@ -369,7 +369,7 @@ class RTABMAP_CORE_EXPORT Parameters RTABMAP_PARAM(RGBD, AggressiveLoopThr, float, 0.05, uFormat("Loop closure threshold used (overriding %s) when a new mapping session is not yet linked to a map of the highest loop closure hypothesis. In localization mode, this threshold is used when there are no loop closure constraints with any map in the cache (%s). In all cases, the goal is to aggressively loop on a previous map in the database. Only used when %s is enabled. Set 1 to disable.", kRtabmapLoopThr().c_str(), kRGBDMaxOdomCacheSize().c_str(), kRGBDEnabled().c_str())); RTABMAP_PARAM(RGBD, NewMapOdomChangeDistance, float, 0, "A new map is created if a change of odometry translation greater than X m is detected (0 m = disabled)."); RTABMAP_PARAM(RGBD, OptimizeFromGraphEnd, bool, false, "Optimize graph from the newest node. If false, the graph is optimized from the oldest node of the current graph (this adds an overhead computation to detect to oldest node of the current graph, but it can be useful to preserve the map referential from the oldest node). Warning when set to false: when some nodes are transferred, the first referential of the local map may change, resulting in momentary changes in robot/map position (which are annoying in teleoperation)."); - RTABMAP_PARAM(RGBD, OptimizeMaxError, float, 3.0, uFormat("Reject loop closures if optimization error ratio is greater than this value (0=disabled). Ratio is computed as absolute error over standard deviation of each link. This will help to detect when a wrong loop closure is added to the graph. Not compatible with \"%s\" if enabled.", kOptimizerRobust().c_str())); + RTABMAP_PARAM(RGBD, OptimizeMaxError, float, 3.0, uFormat("Reject loop closures if optimization error ratio is greater than this value (0=disabled). Ratio is computed as absolute error over standard deviation of each link. This will help to detect when a wrong loop closure is added to the graph. If used with \"%s\", the disabled loop closure links will be removed.", kOptimizerRobust().c_str())); RTABMAP_PARAM(RGBD, MaxLoopClosureDistance, float, 0.0, "Reject loop closures/localizations if the distance from the map is over this distance (0=disabled)."); RTABMAP_PARAM(RGBD, ForceOdom3DoF, bool, true, uFormat("Force odometry pose to be 3DoF if %s=true.", kRegForce3DoF().c_str())); RTABMAP_PARAM(RGBD, StartAtOrigin, bool, false, uFormat("If true, rtabmap will assume the robot is starting from origin of the map. If false, rtabmap will assume the robot is restarting from the last saved localization pose from previous session (the place where it shut down previously). Used only in localization mode (%s=false).", kMemIncrementalMemory().c_str())); @@ -431,7 +431,7 @@ class RTABMAP_CORE_EXPORT Parameters #endif #endif RTABMAP_PARAM(Optimizer, VarianceIgnored, bool, false, "Ignore constraints' variance. If checked, identity information matrix is used for each constraint. Otherwise, an information matrix is generated from the variance saved in the links."); - RTABMAP_PARAM(Optimizer, Robust, bool, false, uFormat("Robust graph optimization using Vertigo (only work for g2o and GTSAM optimization strategies). Not compatible with \"%s\" if enabled.", kRGBDOptimizeMaxError().c_str())); + RTABMAP_PARAM(Optimizer, Robust, bool, false, "Robust graph optimization using Vertigo (only work for g2o and GTSAM optimization strategies)."); RTABMAP_PARAM(Optimizer, PriorsIgnored, bool, true, "Ignore prior constraints (global pose or GPS) while optimizing. Currently only g2o and gtsam optimization supports this."); RTABMAP_PARAM(Optimizer, LandmarksIgnored, bool, false, "Ignore landmark constraints while optimizing. Currently only g2o and gtsam optimization supports this."); #if defined(RTABMAP_G2O) || defined(RTABMAP_GTSAM) diff --git a/corelib/src/DBReader.cpp b/corelib/src/DBReader.cpp index bc5272c7..7eb69c3e 100644 --- a/corelib/src/DBReader.cpp +++ b/corelib/src/DBReader.cpp @@ -510,22 +510,41 @@ SensorData DBReader::getNextData(SensorCaptureInfo * info) } else { - // if localization data saved in database, covariance will be set in a prior link - _dbDriver->loadLinks(*_currentId, links, Link::kPosePrior); - if(links.size()) - { - // assume the first is the backward neighbor, take its variance - infMatrix = links.begin()->second.infMatrix(); - _previousInfMatrix = infMatrix; - } - else - { - if(_previousInfMatrix.empty()) + // In case the graph was reduced, look for forward neighbor link from previous id + bool covAdded = false; + if(_currentId != _ids.begin()) { + std::set::iterator previousId = _currentId; + --previousId; + std::multimap previousLinks; + _dbDriver->loadLinks(*previousId, previousLinks, Link::kNeighbor); + if(previousLinks.size() && previousLinks.rbegin()->first == *_currentId) { - _previousInfMatrix = cv::Mat::eye(6,6,CV_64FC1); + // assume the last is the forward neighbor pointing to current ID, take its covariance + infMatrix = previousLinks.rbegin()->second.infMatrix(); + _previousInfMatrix = infMatrix; + covAdded = true; + } + } + + if(!covAdded) { + // if localization data saved in database, covariance will be set in a prior link + _dbDriver->loadLinks(*_currentId, links, Link::kPosePrior); + if(links.size()) + { + // assume the first is the backward neighbor, take its variance + infMatrix = links.begin()->second.infMatrix(); + _previousInfMatrix = infMatrix; + } + else + { + if(_previousInfMatrix.empty()) + { + _previousInfMatrix = cv::Mat::eye(6,6,CV_64FC1); + } + // we have a node not linked to map, use last variance + UWARN("The node loaded (%d) doesn't have neighbor, re-using the covariance of the previous link for odometry.", s->id()); + infMatrix = _previousInfMatrix; } - // we have a node not linked to map, use last variance - infMatrix = _previousInfMatrix; } } } diff --git a/corelib/src/Memory.cpp b/corelib/src/Memory.cpp index 27721c28..124ec2a4 100644 --- a/corelib/src/Memory.cpp +++ b/corelib/src/Memory.cpp @@ -131,6 +131,7 @@ Memory::Memory(const ParametersMap & parameters) : _linksChanged(false), _signaturesAdded(0), _allNodesInWM(true), + _receivingOdometryFeatures(false), _badSignRatio(Parameters::defaultKpBadSignRatio()), _tfIdfLikelihoodUsed(Parameters::defaultKpTfIdfLikelihoodUsed()), _parallelized(Parameters::defaultKpParallelized()), @@ -1262,16 +1263,17 @@ void Memory::moveSignatureToWMFromSTM(int id, int * reducedTo) std::multimap linksCopy = links; for(std::multimap::iterator iter=linksCopy.begin(); iter!=linksCopy.end(); ++iter) { - if(iter->second.type() == Link::kNeighbor || - iter->second.type() == Link::kNeighborMerged) + if(iter->second.type() == Link::kNeighborMerged) { + // Removing only merged neighbor links, we keep original neighbor + // links to be able to reprocess databases with correct odometry covariance. s->removeLink(iter->first); - if(iter->second.type() == Link::kNeighbor) + } + if(iter->second.type() == Link::kNeighbor) + { + if(_lastGlobalLoopClosureId == s->id()) { - if(_lastGlobalLoopClosureId == s->id()) - { - _lastGlobalLoopClosureId = iter->first; - } + _lastGlobalLoopClosureId = iter->first; } } } @@ -1929,6 +1931,7 @@ void Memory::clear() _landmarksIndex.clear(); _landmarksSize.clear(); _allNodesInWM = true; + _receivingOdometryFeatures = false; if(_dbDriver) { @@ -4849,6 +4852,11 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor { meanWordsPerLocation = _vwd->getTotalActiveReferences() / (treeSize-1); // ignore virtual signature } + else if(_useOdometryFeatures) { + // To not detect first image as bad signature if odometry + // is using less features than feature2D->getMaxFeatures() + meanWordsPerLocation = 0; + } if(_parallelized && !isIntermediateNode) { @@ -4952,10 +4960,12 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor SensorData decimatedData; UDEBUG("Received kpts=%d kpts3D=%d, descriptors=%d _useOdometryFeatures=%s", (int)data.keypoints().size(), (int)data.keypoints3D().size(), data.descriptors().rows, _useOdometryFeatures?"true":"false"); + // TODO: do we still need the third and fouth comparisons? + // TODO: there is significant repetitive code between the if and the else, could we combine them?! if(!_useOdometryFeatures || - data.keypoints().empty() || + (!_receivingOdometryFeatures && data.keypoints().empty()) || (int)data.keypoints().size() != data.descriptors().rows || - (_feature2D->getType() == Feature2D::kFeatureOrbOctree && data.descriptors().empty())) + (!_receivingOdometryFeatures && _feature2D->getType() == Feature2D::kFeatureOrbOctree && data.descriptors().empty())) { if(_feature2D->getMaxFeatures() >= 0 && !data.imageRaw().empty() && !isIntermediateNode) { @@ -5292,6 +5302,7 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor } else if(_feature2D->getMaxFeatures() >= 0 && !isIntermediateNode) { + _receivingOdometryFeatures = true; UINFO("Use odometry features: kpts=%d 3d=%d desc=%d (dim=%d, type=%d)", (int)data.keypoints().size(), (int)data.keypoints3D().size(), diff --git a/corelib/src/VWDictionary.cpp b/corelib/src/VWDictionary.cpp index 5193f4c5..2b5f0d28 100644 --- a/corelib/src/VWDictionary.cpp +++ b/corelib/src/VWDictionary.cpp @@ -919,14 +919,21 @@ std::list VWDictionary::addNewWords( type = _visualWords.begin()->second->getDescriptor().type(); UASSERT(type == CV_32F || type == CV_8U); } + static std::string moreInfo = uFormat( + "This could happen if the computer doesn't have access to same " + "feature detectors than when the database was created. This could " + "also happen if we enabled \"%s\" but the first frame received " + "was empty, thus features were re-extracted with a different detector " + "than the one used by the odometry.", + Parameters::kMemUseOdomFeatures().c_str()); if(dim && dim != descriptorsIn.cols) { - UERROR("Descriptors (size=%d) are not the same size as already added words in dictionary(size=%d)", descriptorsIn.cols, dim); + UERROR("Descriptors (size=%d) are not the same size as already added words in dictionary (size=%d). %s", descriptorsIn.cols, dim, moreInfo.c_str()); return wordIds; } if(type>=0 && type != descriptorsIn.type()) { - UERROR("Descriptors (type=%d) are not the same type as already added words in dictionary(type=%d)", descriptorsIn.type(), type); + UERROR("Descriptors (type=%d) are not the same type as already added words in dictionary (type=%d). %s", descriptorsIn.type(), type, moreInfo.c_str()); return wordIds; } diff --git a/guilib/src/PreferencesDialog.cpp b/guilib/src/PreferencesDialog.cpp index b02ab32c..d31421f3 100644 --- a/guilib/src/PreferencesDialog.cpp +++ b/guilib/src/PreferencesDialog.cpp @@ -3733,15 +3733,6 @@ bool PreferencesDialog::validateForm() _ui->odom_f2m_bundleStrategy->setCurrentIndex(0); } - // verify that Robust and Reject threshold are not set at the same time - if(_ui->graphOptimization_robust->isChecked() && _ui->graphOptimization_maxError->value()>0.0) - { - QMessageBox::warning(this, tr("Parameter warning"), - tr("Robust graph optimization and maximum optimization error threshold cannot be " - "both used at the same time. Disabling robust optimization.")); - _ui->graphOptimization_robust->setChecked(false); - } - //verify binary features and nearest neighbor // BOW dictionary type if(_ui->comboBox_dictionary_strategy->currentIndex() == VWDictionary::kNNFlannLSH && _ui->comboBox_detector_strategy->currentIndex() <= 1) diff --git a/guilib/src/ui/preferencesDialog.ui b/guilib/src/ui/preferencesDialog.ui index 2653fcca..4ff1fa03 100644 --- a/guilib/src/ui/preferencesDialog.ui +++ b/guilib/src/ui/preferencesDialog.ui @@ -95,7 +95,7 @@ QFrame::Raised - 5 + 8 @@ -10409,7 +10409,7 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - Reduce graph. Merge nodes when loop closures are added (ignoring those with user data set). + Reduce graph. Merge nodes when loop closures are added (ignoring those with user data). Note that this approach assumes that 100% of the loop closures accepted are good, so it is highly recommended to enable optimization error ratio at the same time. true @@ -13433,7 +13433,7 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - Robust graph optimization using Vertigo (only for g2o and GTSAM optimization strategies). This approach can filter wrong loop closure detections. + Robust graph optimization using Vertigo (only work for g2o and GTSAM optimization strategies). true @@ -13468,7 +13468,7 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - Reject loop closures if optimization error ratio is greater than this value (0=disabled). Ratio is computed as absolute error over standard deviation of each link. This will help to detect when a wrong loop closure is added to the graph. It should not be used at the same time than Vertigo above. + Reject loop closures if optimization error ratio is greater than this value (0=disabled). Ratio is computed as absolute error over standard deviation of each link. This will help to detect when a wrong loop closure is added to the graph. If used with robust graph optimization, the disabled loop closure links will be removed. true From 0f37bafd66e871542f58d5ba42b2cd4adb826ede Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 28 Sep 2025 16:31:42 -0700 Subject: [PATCH 021/126] Added source odometry guess option in standalone app --- corelib/include/rtabmap/core/OdometryThread.h | 8 +- corelib/src/Odometry.cpp | 15 +- corelib/src/OdometryThread.cpp | 61 +- .../include/rtabmap/gui/PreferencesDialog.h | 1 + guilib/src/MainWindow.cpp | 4 +- guilib/src/PreferencesDialog.cpp | 10 +- guilib/src/ui/preferencesDialog.ui | 626 +++++++++--------- 7 files changed, 389 insertions(+), 336 deletions(-) diff --git a/corelib/include/rtabmap/core/OdometryThread.h b/corelib/include/rtabmap/core/OdometryThread.h index 6d581b73..f9d7082b 100644 --- a/corelib/include/rtabmap/core/OdometryThread.h +++ b/corelib/include/rtabmap/core/OdometryThread.h @@ -29,6 +29,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #define ODOMETRYTHREAD_H_ #include +#include #include #include #include @@ -55,18 +56,19 @@ private: // MAIN LOOP //============================================================ virtual void mainLoop(); - void addData(const SensorData & data); - bool getData(SensorData & data); + void addData(const SensorEvent & data); + bool getData(SensorEvent & data); private: USemaphore _dataAdded; UMutex _dataMutex; - std::list _dataBuffer; + std::list _dataBuffer; std::list _imuBuffer; Odometry * _odometry; unsigned int _dataBufferMaxSize; bool _resetOdometry; Transform _resetPose; + Transform _previousGuessPose; double _oldestAsyncImuStamp; double _newestAsyncImuStamp; }; diff --git a/corelib/src/Odometry.cpp b/corelib/src/Odometry.cpp index c5c35641..14579ef9 100644 --- a/corelib/src/Odometry.cpp +++ b/corelib/src/Odometry.cpp @@ -625,6 +625,7 @@ Transform Odometry::process(SensorData & data, const Transform & guessIn, Odomet if(!guessIn.isNull()) { guess = guessIn; + UDEBUG("Using provided guess %s", guessIn.prettyPrint().c_str()); } else if(!imus_.empty()) { @@ -641,12 +642,16 @@ Transform Odometry::process(SensorData & data, const Transform & guessIn, Odomet { guess = guess.to3DoF(); } + UDEBUG("Adjusting guess from motion with IMU %s", guess.prettyPrint().c_str()); } else if(!imuLastTransform_.isNull()) { UWARN("Could not find imu transform at %f", data.stamp()); } } + else if(!guess.isNull()) { + UDEBUG("Using guess from motion %s", guess.prettyPrint().c_str()); + } UTimer time; @@ -1011,8 +1016,14 @@ Transform Odometry::process(SensorData & data, const Transform & guessIn, Odomet --_resetCurrentCount; if(_resetCurrentCount == 0) { - UWARN("Odometry automatically reset to latest pose!"); - this->reset(_pose); + if(!guess.isNull() && !guessFromMotion_) { + UWARN("Odometry automatically reset to latest pose (%s) + guess (%s)!", _pose.prettyPrint().c_str(), guess.prettyPrint().c_str()); + this->reset(_pose * guess); + } + else { + UWARN("Odometry automatically reset to latest pose (%s)!", _pose.prettyPrint().c_str()); + this->reset(_pose); + } _resetCurrentCount = _resetCountdown; if(info) { diff --git a/corelib/src/OdometryThread.cpp b/corelib/src/OdometryThread.cpp index 490a3753..0f103a94 100644 --- a/corelib/src/OdometryThread.cpp +++ b/corelib/src/OdometryThread.cpp @@ -63,7 +63,7 @@ bool OdometryThread::handleEvent(UEvent * event) SensorEvent * sensorEvent = (SensorEvent*)event; if(sensorEvent->getCode() == SensorEvent::kCodeData) { - this->addData(sensorEvent->data()); + this->addData(*sensorEvent); } } else if(event->getClassName().compare("IMUEvent") == 0) @@ -112,31 +112,42 @@ void OdometryThread::mainLoop() _imuBuffer.clear(); _oldestAsyncImuStamp = 0.0; _newestAsyncImuStamp = 0.0; + _previousGuessPose.setNull(); } - SensorData data; - if(getData(data)) + SensorEvent event; + if(getData(event)) { OdometryInfo info; UDEBUG("Processing data..."); - Transform pose = _odometry->process(data, &info); + Transform guess; + UDEBUG("event.info().odomPose=%s", event.info().odomPose.prettyPrint().c_str()); + if(!_previousGuessPose.isNull() && !event.info().odomPose.isNull()) { + guess = _previousGuessPose.inverse() * event.info().odomPose; + } + + SensorData data = event.data(); + Transform pose = _odometry->process(data, guess , &info); if(!data.imageRaw().empty() || !data.laserScanRaw().empty() || (pose.isNull() && data.imu().empty())) { UDEBUG("Odom pose = %s", pose.prettyPrint().c_str()); // a null pose notify that odometry could not be computed this->post(new OdometryEvent(data, pose, info)); + if(!pose.isNull()) { + _previousGuessPose = event.info().odomPose; + } } } } -void OdometryThread::addData(const SensorData & data) +void OdometryThread::addData(const SensorEvent & event) { - if(data.imu().empty()) + if(event.data().imu().empty()) { if(dynamic_cast(_odometry) == 0) { - if((data.imageRaw().empty() || data.depthOrRightRaw().empty() || (data.cameraModels().empty() && data.stereoCameraModels().empty())) && - data.laserScanRaw().empty()) + if((event.data().imageRaw().empty() || event.data().depthOrRightRaw().empty() || (event.data().cameraModels().empty() && event.data().stereoCameraModels().empty())) && + event.data().laserScanRaw().empty()) { ULOGGER_ERROR("Missing some information (images/scans empty or missing calibration)!?"); return; @@ -145,7 +156,7 @@ void OdometryThread::addData(const SensorData & data) else { // Mono can accept RGB only - if(data.imageRaw().empty() || (data.cameraModels().empty() && data.stereoCameraModels().empty())) + if(event.data().imageRaw().empty() || (event.data().cameraModels().empty() && event.data().stereoCameraModels().empty())) { ULOGGER_ERROR("Missing some information (image empty or missing calibration)!?"); return; @@ -156,30 +167,30 @@ void OdometryThread::addData(const SensorData & data) bool notify = true; _dataMutex.lock(); { - if( !data.imageRaw().empty() || - !data.imageCompressed().empty() || - !data.laserScanRaw().isEmpty() || - !data.laserScanCompressed().empty() || - data.imu().empty()) + if( !event.data().imageRaw().empty() || + !event.data().imageCompressed().empty() || + !event.data().laserScanRaw().isEmpty() || + !event.data().laserScanCompressed().empty() || + event.data().imu().empty()) { - if(_oldestAsyncImuStamp > 0.0 && data.stamp() < _oldestAsyncImuStamp) { + if(_oldestAsyncImuStamp > 0.0 && event.data().stamp() < _oldestAsyncImuStamp) { UWARN("Received image/lidar with stamp (%f) older than oldest received imu " "(%f), skipping that frame (imu buffer size=%ld). " "When using async IMU, make sure IMU is published faster " "than camera/lidar (assuming IMU latency is very small compared to camera/lidar).", - data.stamp(), _oldestAsyncImuStamp, _imuBuffer.size()); + event.data().stamp(), _oldestAsyncImuStamp, _imuBuffer.size()); notify = false; } - else if(_newestAsyncImuStamp > 0.0 && data.stamp()>=_newestAsyncImuStamp) { + else if(_newestAsyncImuStamp > 0.0 && event.data().stamp()>=_newestAsyncImuStamp) { UWARN("Received image/lidar with stamp (%f) newer than latest received imu " "(%f), skipping that frame (imu buffer size=%ld). " "When using async IMU, make sure IMU is published faster " "than camera/lidar (assuming IMU latency is very small compared to camera/lidar).", - data.stamp(), _newestAsyncImuStamp, _imuBuffer.size()); + event.data().stamp(), _newestAsyncImuStamp, _imuBuffer.size()); notify = false; } else { - _dataBuffer.push_back(data); + _dataBuffer.push_back(event); while(_dataBufferMaxSize > 0 && _dataBuffer.size() > _dataBufferMaxSize) { UDEBUG("Data buffer is full, the oldest data is removed to add the new one."); @@ -190,11 +201,11 @@ void OdometryThread::addData(const SensorData & data) } else { - _imuBuffer.push_back(data); + _imuBuffer.push_back(event.data()); if(_oldestAsyncImuStamp == 0) { - _oldestAsyncImuStamp = data.stamp(); + _oldestAsyncImuStamp = event.data().stamp(); } - _newestAsyncImuStamp = data.stamp(); + _newestAsyncImuStamp = event.data().stamp(); } } _dataMutex.unlock(); @@ -205,7 +216,7 @@ void OdometryThread::addData(const SensorData & data) } } -bool OdometryThread::getData(SensorData & data) +bool OdometryThread::getData(SensorEvent & event) { bool dataFilled = false; _dataAdded.acquire(); @@ -219,12 +230,12 @@ bool OdometryThread::getData(SensorData & data) _odometry->process(_imuBuffer.front()); double stamp =_imuBuffer.front().stamp(); _imuBuffer.pop_front(); - if(stamp > _dataBuffer.front().stamp()) { + if(stamp > _dataBuffer.front().data().stamp()) { break; } } - data = _dataBuffer.front(); + event = _dataBuffer.front(); _dataBuffer.pop_front(); dataFilled = true; } diff --git a/guilib/include/rtabmap/gui/PreferencesDialog.h b/guilib/include/rtabmap/gui/PreferencesDialog.h index 441c41b8..72f019e3 100644 --- a/guilib/include/rtabmap/gui/PreferencesDialog.h +++ b/guilib/include/rtabmap/gui/PreferencesDialog.h @@ -173,6 +173,7 @@ public: int getOdomRegistrationApproach() const; double getOdomF2MGravitySigma() const; bool isOdomDisabled() const; + bool isOdomAsGuessEnabled() const; bool isOdomSensorAsGt() const; bool isGroundTruthAligned() const; diff --git a/guilib/src/MainWindow.cpp b/guilib/src/MainWindow.cpp index e4ae64d6..9ddcf68e 100644 --- a/guilib/src/MainWindow.cpp +++ b/guilib/src/MainWindow.cpp @@ -5995,7 +5995,7 @@ void MainWindow::startDetection() _imuThread = 0; } - if(!_sensorCapture->odomProvided() && !_preferencesDialog->isOdomDisabled()) + if((!_sensorCapture->odomProvided() || _preferencesDialog->isOdomAsGuessEnabled()) && !_preferencesDialog->isOdomDisabled()) { ParametersMap odomParameters = parameters; if(_preferencesDialog->getOdomRegistrationApproach() < 3) @@ -6053,7 +6053,7 @@ void MainWindow::startDetection() } } - if(_dataRecorder && _sensorCapture && _odomThread) + if(_dataRecorder && _sensorCapture) { UEventsManager::createPipe(_sensorCapture, _dataRecorder, "SensorEvent"); } diff --git a/guilib/src/PreferencesDialog.cpp b/guilib/src/PreferencesDialog.cpp index d31421f3..898a3904 100644 --- a/guilib/src/PreferencesDialog.cpp +++ b/guilib/src/PreferencesDialog.cpp @@ -918,6 +918,7 @@ PreferencesDialog::PreferencesDialog(QWidget * parent) : connect(_ui->doubleSpinBox_odom_sensor_scale_factor, SIGNAL(valueChanged(double)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->doubleSpinBox_odom_sensor_wait_time, SIGNAL(valueChanged(double)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->checkBox_odom_sensor_use_as_gt, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); + connect(_ui->checkbox_passthrough_source_odom, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->comboBox_imuFilter_strategy, SIGNAL(currentIndexChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->comboBox_imuFilter_strategy, SIGNAL(currentIndexChanged(int)), _ui->stackedWidget_imuFilter, SLOT(setCurrentIndex(int))); @@ -2309,6 +2310,7 @@ void PreferencesDialog::resetSettings(QGroupBox * groupBox) _ui->doubleSpinBox_odom_sensor_scale_factor->setValue(1); _ui->doubleSpinBox_odom_sensor_wait_time->setValue(100); _ui->checkBox_odom_sensor_use_as_gt->setChecked(false); + _ui->checkbox_passthrough_source_odom->setChecked(false); _ui->comboBox_imuFilter_strategy->setCurrentIndex(2); _ui->doubleSpinBox_imuFilterMadgwickGain->setValue(Parameters::defaultImuFilterMadgwickGain()); @@ -2835,6 +2837,7 @@ void PreferencesDialog::readCameraSettings(const QString & filePath) _ui->doubleSpinBox_odom_sensor_scale_factor->setValue(settings.value("odom_sensor_scale_factor", _ui->doubleSpinBox_odom_sensor_scale_factor->value()).toDouble()); _ui->doubleSpinBox_odom_sensor_wait_time->setValue(settings.value("odom_sensor_wait_time", _ui->doubleSpinBox_odom_sensor_wait_time->value()).toDouble()); _ui->checkBox_odom_sensor_use_as_gt->setChecked(settings.value("odom_sensor_odom_as_gt", _ui->checkBox_odom_sensor_use_as_gt->isChecked()).toBool()); + _ui->checkbox_passthrough_source_odom->setChecked(settings.value("odom_sensor_as_guess", _ui->checkbox_passthrough_source_odom->isChecked()).toBool()); settings.endGroup(); // OdomSensor settings.beginGroup("UsbCam"); @@ -3439,6 +3442,7 @@ void PreferencesDialog::writeCameraSettings(const QString & filePath) const settings.setValue("odom_sensor_scale_factor", _ui->doubleSpinBox_odom_sensor_scale_factor->value()); settings.setValue("odom_sensor_wait_time", _ui->doubleSpinBox_odom_sensor_wait_time->value()); settings.setValue("odom_sensor_odom_as_gt", _ui->checkBox_odom_sensor_use_as_gt->isChecked()); + settings.setValue("odom_sensor_as_guess", _ui->checkbox_passthrough_source_odom->isChecked()); settings.endGroup(); // OdomSensor settings.beginGroup("UsbCam"); @@ -5777,7 +5781,7 @@ void PreferencesDialog::updateSourceGrpVisibility() // Odom Sensor Group _ui->frame_visual_odometry_sensor->setVisible(getOdomSourceDriver() != kSrcUndef); // Not Lidar None - _ui->groupBox_odom_sensor->setVisible(_ui->comboBox_sourceType->currentIndex() != 3); // Don't show when database is selected + _ui->comboBox_odom_sensor->setEnabled(_ui->comboBox_sourceType->currentIndex() != 3); // Don't enable when database is selected // Lidar Sensor Group _ui->comboBox_lidar_src->setEnabled(_ui->comboBox_sourceType->currentIndex() != 3); // Disable if database input @@ -5911,6 +5915,10 @@ bool PreferencesDialog::isOdomDisabled() const { return _ui->checkbox_odomDisabled->isChecked(); } +bool PreferencesDialog::isOdomAsGuessEnabled() const +{ + return _ui->checkbox_passthrough_source_odom->isChecked(); +} bool PreferencesDialog::isOdomSensorAsGt() const { return _ui->checkBox_odom_sensor_use_as_gt->isChecked(); diff --git a/guilib/src/ui/preferencesDialog.ui b/guilib/src/ui/preferencesDialog.ui index 4ff1fa03..76cde95d 100644 --- a/guilib/src/ui/preferencesDialog.ui +++ b/guilib/src/ui/preferencesDialog.ui @@ -63,9 +63,9 @@ 0 - 0 + -3050 713 - 4705 + 4779 @@ -95,7 +95,7 @@ QFrame::Raised - 8 + 5 @@ -3165,22 +3165,16 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki 0 - - - - Hz + + + + Create a calibration file with known intrinsics (fx, fy, cx, cy). Useful if you already know the intrinsics of the source of images. - - 1 + + true - - 100.000000000000000 - - - 0.100000000000000 - - - 0.000000000000000 + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse @@ -3194,13 +3188,6 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - - - - - - @@ -3214,40 +3201,34 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - - - <html><head/><body><p>Format (3 values): x y z<br/>Format (6 values): x y z roll pitch yaw<br/>Format (7 values): x y z qx qy qz qw<br/>Format (9 values, 3x3 rotation): r11 r12 r13 r21 r22 r23 r31 r32 r33<br/>Format (12 values, 3x4 transform): r11 r12 r13 tx r21 r22 r23 ty r31 r32 r33 tz</p><p>KITTI: /base_link to /gray_camera = 0 0 0 0 0 0<br/>KITTI: /base_link to /color_camera = 0 -0.06 0 0 0 0<br/>KITTI: /base_footprint to /gray_camera = 0 0 1.67 0 0 0<br/>KITTI: /base_footprint to /color_camera = 0 -0.06 1.67 0 0 0</p><p>EuRoC MAV: /base_link to /cam0 = T_BS*T_SC0 = 0.999661 0.0257743 -0.00375625 0.00981073 -0.0257154 0.999557 0.0149672 0.064677 0.00414035 -0.0148655 0.999881 -0.0216401</p></body></html> - - - 0 0 0 0 0 0 - - + + + + + + ... + + + + + + + + 100 + 0 + + + + + + + + - - + + - Local transform from /base_link to /camera_link (without optical transform). Mouse over the box to show formats. If odometry sensor is enabled, it is the transform to odometry sensor. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - - - - - - - - Mirroring mode (flip image horizontally). It has no effect on database source. + Equalizes the histogram of grayscale images. true @@ -3264,10 +3245,33 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - + + - Image decimation. RGB/Mono and depth images will be resized according to this value (size*1/decimation). Note that if depth images are captured, decimation should be a multiple of the depth image size. If depth images are smaller than RGB images, the decimation is first applied on RGB, if the resulting RGB image is still bigger than depth image, the depth is not decimated. + + + + false + + + + + + + Calibration file path (*.yaml). If empty, the GUID of the camera is used (for those having one). OpenNI and Freenect drivers use factory calibration by default (so they ignore this parameter). A calibrated camera is required for RGB-D SLAM mode. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Detect features inside camera thread using parameters set in Visual Registration panel. true @@ -3302,102 +3306,6 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - - - Equalizes the histogram of grayscale images. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - - - - false - - - - - - - Only RGB images (for RGB-D cameras) or left images (for stereo cameras) are published. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - - - - false - - - - - - - Detect features inside camera thread using parameters set in Visual Registration panel. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - - - ... - - - - - - - - 100 - 0 - - - - - - - - - - - - - Calibration file path (*.yaml). If empty, the GUID of the camera is used (for those having one). OpenNI and Freenect drivers use factory calibration by default (so they ignore this parameter). A calibrated camera is required for RGB-D SLAM mode. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - @@ -3424,23 +3332,17 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - - - - 0 - 0 - - + + - Create Calibration + - - + + - Create a calibration file with known intrinsics (fx, fy, cx, cy). Useful if you already know the intrinsics of the source of images. + Mirroring mode (flip image horizontally). It has no effect on database source. true @@ -3463,6 +3365,104 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki + + + + + + + false + + + + + + + Only RGB images (for RGB-D cameras) or left images (for stereo cameras) are published. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Hz + + + 1 + + + 100.000000000000000 + + + 0.100000000000000 + + + 0.000000000000000 + + + + + + + + + + + + + + <html><head/><body><p>Format (3 values): x y z<br/>Format (6 values): x y z roll pitch yaw<br/>Format (7 values): x y z qx qy qz qw<br/>Format (9 values, 3x3 rotation): r11 r12 r13 r21 r22 r23 r31 r32 r33<br/>Format (12 values, 3x4 transform): r11 r12 r13 tx r21 r22 r23 ty r31 r32 r33 tz</p><p>KITTI: /base_link to /gray_camera = 0 0 0 0 0 0<br/>KITTI: /base_link to /color_camera = 0 -0.06 0 0 0 0<br/>KITTI: /base_footprint to /gray_camera = 0 0 1.67 0 0 0<br/>KITTI: /base_footprint to /color_camera = 0 -0.06 1.67 0 0 0</p><p>EuRoC MAV: /base_link to /cam0 = T_BS*T_SC0 = 0.999661 0.0257743 -0.00375625 0.00981073 -0.0257154 0.999557 0.0149672 0.064677 0.00414035 -0.0148655 0.999881 -0.0216401</p></body></html> + + + 0 0 0 0 0 0 + + + + + + + Image decimation. RGB/Mono and depth images will be resized according to this value (size*1/decimation). Note that if depth images are captured, decimation should be a multiple of the depth image size. If depth images are smaller than RGB images, the decimation is first applied on RGB, if the resulting RGB image is still bigger than depth image, the depth is not decimated. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + 0 + 0 + + + + Create Calibration + + + + + + + Local transform from /base_link to /camera_link (without optical transform). Mouse over the box to show formats. If odometry sensor is enabled, it is the transform to odometry sensor. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + @@ -8292,7 +8292,7 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - Visual Odometry Sensor + Odometry Sensor @@ -8337,6 +8337,26 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki + + + + Pass input odometry as guess to internal odometry. Otherwise, internal odometry is skipped and input odometry is sent directly to SLAM's back-end. This applies to all sources that can provide odometry (e.g., odometry file, database, ...). + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + @@ -8354,10 +8374,111 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki 0 - - + + - Calibration file path (*.yaml) for the visual odometry sensor. If empty, the GUID of the camera is used (for those having one). Only required to calibrate extrinsics. + ID of the device, which might be a serial number, bus@address or the index of the device. If empty, the first device found is taken. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Use as ground truth. The odometry poses will be saved in ground truth field of sensor data instead of being used as odometry. The actual odometry will be computed by rtabmap using the camera sensor. This can be useful to compare rtabmap odometry versus odometry sensor, and to estimate a scale factor between the sensors (using "rtabmap-report -- scale") that can be used above. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Time offset between camera and visual odometry sensors. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + ms + + + 1 + + + -999.000000000000000 + + + 999.000000000000000 + + + + + + + + 0 + 0 + + + + Calibrate Extrinsics + + + + + + + + + + + + + + <html><head/><body><p>Extrinsics between pose frame and camera's left lens (without optical rotation). Default extrinsics match the 3D printed bracket <a href=" https://www.intelrealsense.com/depth-and-tracking-combined-get-started/"><span style=" text-decoration: underline; color:#0000ff;">here</span></a> for T265+D400 setup. (<a href="https://github.com/IntelRealSense/realsense-ros/blob/occupancy-mapping/realsense2_camera/meshes/mount_t265_d435.stl"><span style=" text-decoration: underline; color:#0000ff;">stl</span></a>). Not used if camera and visual odometry sensors are the same sensor.</p></body></html> + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Calibrate extrinsics between visual odometry sensor and camera. Both sensors should be already calibrated. See Calibrate button above to calibrate them individually. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Maximum wait time to get the pose for latest data captured. true @@ -8391,16 +8512,32 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - + + + + ms + + + 0 + + + 0.000000000000000 + + + 1000.000000000000000 + + + 100.000000000000000 + + + + + + + <html><head/><body><p>Format (3 values): x y z<br/>Format (6 values): x y z roll pitch yaw<br/>Format (7 values): x y z qx qy qz qw<br/>Format (9 values, 3x3 rotation): r11 r12 r13 r21 r22 r23 r31 r32 r33<br/>Format (12 values, 3x4 transform): r11 r12 r13 tx r21 r22 r23 ty r31 r32 r33 tz</p><p><br/></p></body></html> + - Calibrate extrinsics between visual odometry sensor and camera. Both sensors should be already calibrated. See Calibrate button above to calibrate them individually. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + 0.009 0.021 0.027 0.000 -0.018 0.005 @@ -8411,26 +8548,10 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - - - ms - - - 1 - - - -999.000000000000000 - - - 999.000000000000000 - - - - - + + - ID of the device, which might be a serial number, bus@address or the index of the device. If empty, the first device found is taken. + Scale factor between camera and visual odometry sensor. This factor is multiplied to translation components of each pose in the odometry trajectory. true @@ -8440,10 +8561,10 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - + + - Time offset between camera and visual odometry sensors. + Calibration file path (*.yaml) for the visual odometry sensor. If empty, the GUID of the camera is used (for those having one). Only required to calibrate extrinsics. true @@ -8475,107 +8596,6 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - - - Scale factor between camera and visual odometry sensor. This factor is multiplied to translation components of each pose in the odometry trajectory. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - Use as ground truth. The odometry poses will be saved in ground truth field of sensor data instead of being used as odometry. The actual odometry will be computed by rtabmap using the camera sensor. This can be useful to compare rtabmap odometry versus odometry sensor, and to estimate a scale factor between the sensors (using "rtabmap-report -- scale") that can be used above. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - <html><head/><body><p>Extrinsics between pose frame and camera's left lens (without optical rotation). Default extrinsics match the 3D printed bracket <a href=" https://www.intelrealsense.com/depth-and-tracking-combined-get-started/"><span style=" text-decoration: underline; color:#0000ff;">here</span></a> for T265+D400 setup. (<a href="https://github.com/IntelRealSense/realsense-ros/blob/occupancy-mapping/realsense2_camera/meshes/mount_t265_d435.stl"><span style=" text-decoration: underline; color:#0000ff;">stl</span></a>). Not used if camera and visual odometry sensors are the same sensor.</p></body></html> - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - - 0 - 0 - - - - Calibrate Extrinsics - - - - - - - - - - - - - - <html><head/><body><p>Format (3 values): x y z<br/>Format (6 values): x y z roll pitch yaw<br/>Format (7 values): x y z qx qy qz qw<br/>Format (9 values, 3x3 rotation): r11 r12 r13 r21 r22 r23 r31 r32 r33<br/>Format (12 values, 3x4 transform): r11 r12 r13 tx r21 r22 r23 ty r31 r32 r33 tz</p><p><br/></p></body></html> - - - 0.009 0.021 0.027 0.000 -0.018 0.005 - - - - - - - Maximum wait time to get the pose for latest data captured. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - ms - - - 0 - - - 0.000000000000000 - - - 1000.000000000000000 - - - 100.000000000000000 - - - From 05f077b95039c489fde5c433ccbb984f4d2529c3 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Tue, 30 Sep 2025 16:22:54 -0700 Subject: [PATCH 022/126] Fixed auto reset identity jump. If external guess, do not report lost on recovery after auto reset. (#1587) --- corelib/src/Odometry.cpp | 8 +++++--- corelib/src/OdometryThread.cpp | 11 +++++++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/corelib/src/Odometry.cpp b/corelib/src/Odometry.cpp index 14579ef9..971fea04 100644 --- a/corelib/src/Odometry.cpp +++ b/corelib/src/Odometry.cpp @@ -1016,7 +1016,7 @@ Transform Odometry::process(SensorData & data, const Transform & guessIn, Odomet --_resetCurrentCount; if(_resetCurrentCount == 0) { - if(!guess.isNull() && !guessFromMotion_) { + if(!guess.isNull() && !guessIn.isNull()) { UWARN("Odometry automatically reset to latest pose (%s) + guess (%s)!", _pose.prettyPrint().c_str(), guess.prettyPrint().c_str()); this->reset(_pose * guess); } @@ -1029,14 +1029,15 @@ Transform Odometry::process(SensorData & data, const Transform & guessIn, Odomet { *info = OdometryInfo(); } - return this->computeTransform(data, Transform(), info); + this->computeTransform(data, Transform(), info); + return _pose; } - } previousVelocities_.clear(); velocityGuess_.setNull(); previousStamp_ = 0; +} return Transform(); } @@ -1066,6 +1067,7 @@ void Odometry::initKalmanFilter(const Transform & initialPose, float vx, float v 0, 0, 0, 0, 0, 0.17 } }; static const boost::array STANDARD_TWIST_COVARIANCE = { { 0.05, 0, 0, 0, 0, 0, + } 0, 0.05, 0, 0, 0, 0, 0, 0, 0.05, 0, 0, 0, 0, 0, 0, 0.09, 0, 0, diff --git a/corelib/src/OdometryThread.cpp b/corelib/src/OdometryThread.cpp index 0f103a94..35c456f7 100644 --- a/corelib/src/OdometryThread.cpp +++ b/corelib/src/OdometryThread.cpp @@ -131,11 +131,18 @@ void OdometryThread::mainLoop() if(!data.imageRaw().empty() || !data.laserScanRaw().empty() || (pose.isNull() && data.imu().empty())) { UDEBUG("Odom pose = %s", pose.prettyPrint().c_str()); - // a null pose notify that odometry could not be computed - this->post(new OdometryEvent(data, pose, info)); if(!pose.isNull()) { _previousGuessPose = event.info().odomPose; + if(!event.info().odomPose.isNull() && info.reg.covariance.at(0,0) >= 9999) + { + // In case of external guess, keep reporting lost till we + // process the second frame with valid covariance. This way it + // won't trigger a new map. + pose = Transform(); + } } + // a null pose notify that odometry could not be computed + this->post(new OdometryEvent(data, pose, info)); } } } From 11577fb5532924a2b238352d5bdc6af04a11f470 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Wed, 1 Oct 2025 10:27:24 -0700 Subject: [PATCH 023/126] When external guess is provided, don't report lost for the first ever frame (only on reset). Updated Odom/ResetCountdown description. (#1588) --- corelib/include/rtabmap/core/Parameters.h | 2 +- corelib/src/OdometryThread.cpp | 5 +++-- guilib/src/ui/preferencesDialog.ui | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/corelib/include/rtabmap/core/Parameters.h b/corelib/include/rtabmap/core/Parameters.h index 93f4d87c..63fca366 100644 --- a/corelib/include/rtabmap/core/Parameters.h +++ b/corelib/include/rtabmap/core/Parameters.h @@ -457,7 +457,7 @@ class RTABMAP_CORE_EXPORT Parameters // Odometry RTABMAP_PARAM(Odom, Strategy, int, 0, "0=Frame-to-Map (F2M) 1=Frame-to-Frame (F2F) 2=Fovis 3=viso2 4=DVO-SLAM 5=ORB_SLAM2 6=OKVIS 7=LOAM 8=MSCKF_VIO 9=VINS-Fusion 10=OpenVINS 11=FLOAM 12=Open3D"); - RTABMAP_PARAM(Odom, ResetCountdown, int, 0, "Automatically reset odometry after X consecutive images on which odometry cannot be computed (value=0 disables auto-reset)."); + RTABMAP_PARAM(Odom, ResetCountdown, int, 0, "Automatically reset odometry after X consecutive images where odometry cannot be computed (a value of 0 disables auto-reset). When a reset occurs, odometry resumes from the last successfully computed pose with large covariance to trigger a new map. If external odometry is used, it will also be reset based on the motion estimated relative to the last computed pose but no large covariance will be received, so that a new map won't be triggered."); RTABMAP_PARAM(Odom, Holonomic, bool, true, "If the robot is holonomic (strafing commands can be issued). If not, y value will be estimated from x and yaw values (y=x*tan(yaw))."); RTABMAP_PARAM(Odom, FillInfoData, bool, true, "Fill info with data (inliers/outliers features)."); RTABMAP_PARAM(Odom, ImageBufferSize, unsigned int, 1, "Data buffer size (0 min inf)."); diff --git a/corelib/src/OdometryThread.cpp b/corelib/src/OdometryThread.cpp index 35c456f7..b7986cc6 100644 --- a/corelib/src/OdometryThread.cpp +++ b/corelib/src/OdometryThread.cpp @@ -133,9 +133,10 @@ void OdometryThread::mainLoop() UDEBUG("Odom pose = %s", pose.prettyPrint().c_str()); if(!pose.isNull()) { _previousGuessPose = event.info().odomPose; - if(!event.info().odomPose.isNull() && info.reg.covariance.at(0,0) >= 9999) + if(!event.info().odomPose.isNull() && info.reg.covariance.at(0,0) >= 9999 && + (pose.x() != 0.0f || pose.y() != 0.0f || pose.z() != 0.0f)) // not the first frame { - // In case of external guess, keep reporting lost till we + // In case of external guess and auto reset, keep reporting lost till we // process the second frame with valid covariance. This way it // won't trigger a new map. pose = Transform(); diff --git a/guilib/src/ui/preferencesDialog.ui b/guilib/src/ui/preferencesDialog.ui index 76cde95d..69c52f09 100644 --- a/guilib/src/ui/preferencesDialog.ui +++ b/guilib/src/ui/preferencesDialog.ui @@ -63,7 +63,7 @@ 0 - -3050 + 0 713 4779 @@ -95,7 +95,7 @@ QFrame::Raised - 5 + 19 @@ -15733,7 +15733,7 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - Automatically reset odometry after X consecutive images on which odometry cannot be computed (value=0 disables auto-reset). When reset, the odometry starts from the last pose computed. + Automatically reset odometry after X consecutive images where odometry cannot be computed (a value of 0 disables auto-reset). When a reset occurs, odometry resumes from the last successfully computed pose with large covariance to trigger a new map. If external odometry is used, it will also be reset based on the motion estimated relative to the last computed pose but no large covariance will be received, so that a new map won't be triggered. true From 5f1eccb2cd7257e553a9a491fad57f7b865c5b88 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Thu, 2 Oct 2025 09:07:58 -0700 Subject: [PATCH 024/126] Odom F2M always report 9999 when lost (#1589) --- corelib/src/odometry/OdometryF2M.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/corelib/src/odometry/OdometryF2M.cpp b/corelib/src/odometry/OdometryF2M.cpp index f1bbb58b..58d0e3aa 100644 --- a/corelib/src/odometry/OdometryF2M.cpp +++ b/corelib/src/odometry/OdometryF2M.cpp @@ -1560,6 +1560,10 @@ Transform OdometryF2M::computeTransform( { info->reg = regInfo.copyWithoutData(); } + if(output.isNull()) + { + info->reg.covariance = cv::Mat::eye(6,6,CV_64FC1)*9999.0; // Lost + } } UINFO("Odom update time = %fs lost=%s features=%d inliers=%d/%d variance:lin=%f, ang=%f local_map=%d local_scan_map=%d", From 5aec9bacec0e1bf26866f1814f068da52e8ef94a Mon Sep 17 00:00:00 2001 From: Felix Toft Date: Fri, 3 Oct 2025 11:49:21 -0700 Subject: [PATCH 025/126] CuVSLAM VO Strategy (#1583) * integrating cuvslam, stereo cam initialization * fixing transformations * use isaac ros TocuVSLAMPose * organizing * handling covariance conversion * cleaning up config * moving cmake instructions to FindCuVSLAM.cmake file * fixing ui integration * updating software license for cuvslam * improving memory management * cleaning up header more * reverting mistake in GUI * using opaque pointers for header definitions, compiles * back to typed definitions using forward-referencing * fixing dangling pointer * cleaning up * fixing variable scope issues * Using cuda to allocate gpu memory * fixing transfrom from cuvslam back to ros coordinate frame * reducing excessive logs * removing redundant if true in cuvslam cmake * moving find cuda into cuvslam cmake * cleaning cmake to use modern target * adding copyright line * Finding tf2 and eigen3 in cmake * simplifying clean up logic * better logs * moving cuvslam implementation methods into cpp * fixing build with implmentation function declarations moved to the cpp * cleaning transformation logic and adding grayscale support * typo * removing unecessary class member variable for processed images * remove trailing underscore from initialize cuvslam function signature * locally scoping config params since they are copied by cuvslam * removing unecessary member variables and using locally scoped values since cuvslam seems to copy them * more cleaning and refactoring * fixing cmake inconsistancies and surpress warning * fixing up baseline transform * trying to remove tf2 for transformations * fixing incorect baseline transform swap * comment, begin reset logic investigation * better handling of previous pose and transform during error cases * hunting for reset bug, not found * observation export implementatin, currently getting strange errors * multi cam support * Memory fixes * comments * make cuda stream a member variable so it isn't initialized and destroyed on every frame * preparing to add ground constraints * adding ground constraints, working * adding wheel odom fusion into cuvslam tracker * parsing parameter to apply planar constraints * removing planar constraint in config, doesn't do anything * using proper cuda stream type for member variable * cleaning up for merge * only parse params when cuvslam is compiled * updating error message if using rgb-d instead of stereo * removing wheel odom testing logs * reverting bgr to rgb conversion * fixing initial pose logic * return null transform on invalid covariance * debugging covariance * guard against low velocity situations where cuvslam covariance spikes, but we aren't lost * cleaning things up --------- Co-authored-by: Felix Toft Co-authored-by: matlabbe --- CMakeLists.txt | 19 + Version.h.in | 1 + cmake_modules/FindCuVSLAM.cmake | 79 ++ corelib/include/rtabmap/core/Odometry.h | 3 +- corelib/include/rtabmap/core/Parameters.h | 2 +- .../rtabmap/core/odometry/OdometryCuVSLAM.h | 86 ++ corelib/src/CMakeLists.txt | 8 + corelib/src/Odometry.cpp | 4 + corelib/src/odometry/OdometryCuVSLAM.cpp | 1025 +++++++++++++++++ guilib/src/AboutDialog.cpp | 8 + guilib/src/ui/aboutDialog.ui | 33 + guilib/src/ui/preferencesDialog.ui | 5 + 12 files changed, 1271 insertions(+), 2 deletions(-) create mode 100644 cmake_modules/FindCuVSLAM.cmake create mode 100644 corelib/include/rtabmap/core/odometry/OdometryCuVSLAM.h create mode 100644 corelib/src/odometry/OdometryCuVSLAM.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 71ee6c4b..76d9cd5d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -220,6 +220,7 @@ option(WITH_OKVIS "Include OKVIS support" OFF) option(WITH_MSCKF_VIO "Include MSCKF_VIO support" OFF) option(WITH_VINS_FUSION "Include VINS-Fusion support" OFF) option(WITH_OPENVINS "Include OpenVINS support" OFF) +option(WITH_CUVSLAM "Include cuVSLAM support" OFF) option(WITH_MADGWICK "Include Madgwick IMU filtering support" ON) option(WITH_FASTCV "Include FastCV support" ON) option(WITH_OPENMP "Include OpenMP support" ON) @@ -849,6 +850,13 @@ IF(WITH_ORB_SLAM AND NOT G2O_FOUND) ENDIF(ORB_SLAM_FOUND) ENDIF(WITH_ORB_SLAM AND NOT G2O_FOUND) +IF(WITH_CUVSLAM) + FIND_PACKAGE(CuVSLAM QUIET) + IF(CUVSLAM_FOUND) + MESSAGE(STATUS "Found cuVSLAM: ${CUVSLAM_INCLUDE_DIRS}") + ENDIF() +ENDIF(WITH_CUVSLAM) + SET(DISABLE_NEW_DTAGS_FLAG "--disable-new-dtags") IF(NOT (APPLE OR WIN32) AND BUILD_WITH_RPATH_NOT_RUNPATH) ADD_LINK_OPTIONS(LINKER:${DISABLE_NEW_DTAGS_FLAG}) @@ -1111,6 +1119,9 @@ ENDIF() IF(NOT ov_msckf_FOUND) SET(OPENVINS "//") ENDIF() +IF(NOT CUVSLAM_FOUND) + SET(CUVSLAM "//") +ENDIF() IF(NOT ORB_SLAM_FOUND) SET(ORB_SLAM "//") ENDIF() @@ -1828,6 +1839,14 @@ ELSE() MESSAGE(STATUS " With ORB_SLAM = NO (ORB_SLAM2 and ORB_SLAM3 not found, make sure environment variable ORB_SLAM_ROOT_DIR is set)") ENDIF() +IF(CUVSLAM_FOUND) +MESSAGE(STATUS " With cuVSLAM = YES (License: NVIDIA ISAAC ROS SOFTWARE LICENSE)") +ELSEIF(NOT WITH_CUVSLAM) +MESSAGE(STATUS " With cuVSLAM = NO (WITH_CUVSLAM=OFF)") +ELSE() +MESSAGE(STATUS " With cuVSLAM = NO (cuVSLAM not found, make sure cuVSLAM is installed)") +ENDIF() + MESSAGE(STATUS "Show all options with: cmake -LA | grep WITH_") MESSAGE(STATUS "--------------------------------------------") diff --git a/Version.h.in b/Version.h.in index 5af7a75e..9863ef84 100644 --- a/Version.h.in +++ b/Version.h.in @@ -84,6 +84,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. @MSCKF_VIO@#define RTABMAP_MSCKF_VIO @VINSFUSION@#define RTABMAP_VINS_FUSION @OPENVINS@#define RTABMAP_OPENVINS +@CUVSLAM@#define RTABMAP_CUVSLAM @ORB_SLAM@#define RTABMAP_ORB_SLAM @ORB_SLAM_VERSION@ @ORB_OCTREE@#define RTABMAP_ORB_OCTREE @TORCH@#define RTABMAP_TORCH diff --git a/cmake_modules/FindCuVSLAM.cmake b/cmake_modules/FindCuVSLAM.cmake new file mode 100644 index 00000000..8a17530b --- /dev/null +++ b/cmake_modules/FindCuVSLAM.cmake @@ -0,0 +1,79 @@ +# - Find cuVSLAM library (https://github.com/NVIDIA-ISAAC-ROS/isaac_ros_visual_slam) +# +# CUVSLAM_ROOT_DIR environment variable can be set to find the library. +# +# It sets the following variables: +# CUVSLAM_FOUND - Set to false, or undefined, if cuVSLAM isn't found. +# CUVSLAM_INCLUDE_DIRS - The cuVSLAM include directory. +# CUVSLAM_LIBRARIES - The cuVSLAM library to link against. + +find_package(CUDA REQUIRED) +find_package(Eigen3 REQUIRED) + +find_path(CUVSLAM_INCLUDE_DIRS + NAMES cuvslam.h + PATHS + /usr/include + /usr/local/include + /opt/cuvslam/include + /opt/ros/humble/share/isaac_ros_nitros/cuvslam/include + $ENV{CUVSLAM_ROOT}/include + $ENV{CUVSLAM_ROOT_DIR}/include +) + +find_library(CUVSLAM_LIBRARY + NAMES cuvslam + PATHS + /usr/lib + /usr/local/lib + /opt/cuvslam/lib + /opt/ros/humble/share/isaac_ros_nitros/cuvslam/lib + $ENV{CUVSLAM_ROOT}/lib + $ENV{CUVSLAM_ROOT_DIR}/lib +) + +if(CUVSLAM_INCLUDE_DIRS AND CUVSLAM_LIBRARY) + set(CUVSLAM_FOUND TRUE) + set(CUVSLAM_LIBRARIES + ${CUVSLAM_LIBRARY} + ${CUDA_LIBRARIES} + # Eigen3 is header-only, so we don't need to link to it + ) + set(CUVSLAM_INCLUDE_DIRS + ${CUVSLAM_INCLUDE_DIRS} + ${CUDA_INCLUDE_DIRS} + ${EIGEN3_INCLUDE_DIR} + ) +endif() + +# Handle the QUIET and REQUIRED arguments +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(CuVSLAM + FOUND_VAR CUVSLAM_FOUND + REQUIRED_VARS CUVSLAM_LIBRARIES CUVSLAM_INCLUDE_DIRS + HANDLE_COMPONENTS +) + +if(CUVSLAM_FOUND) + # Create imported target for modern CMake usage + if(NOT TARGET cuvslam::cuvslam) + add_library(cuvslam::cuvslam UNKNOWN IMPORTED) + set_target_properties(cuvslam::cuvslam PROPERTIES + IMPORTED_LOCATION "${CUVSLAM_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${CUVSLAM_INCLUDE_DIRS}" + INTERFACE_LINK_LIBRARIES "${CUVSLAM_LIBRARIES};Eigen3::Eigen" + ) + endif() + + # Show which cuVSLAM was found only if not quiet + if(NOT CUVSLAM_FIND_QUIETLY) + message(STATUS "Found cuVSLAM: ${CUVSLAM_LIBRARIES}") + endif() +else() + # Fatal error if cuVSLAM is required but not found + if(CUVSLAM_FIND_REQUIRED) + message(FATAL_ERROR "Could not find cuVSLAM library") + endif() +endif() + +mark_as_advanced(CUVSLAM_INCLUDE_DIRS CUVSLAM_LIBRARY) diff --git a/corelib/include/rtabmap/core/Odometry.h b/corelib/include/rtabmap/core/Odometry.h index 75864b50..f682afca 100644 --- a/corelib/include/rtabmap/core/Odometry.h +++ b/corelib/include/rtabmap/core/Odometry.h @@ -56,7 +56,8 @@ public: kTypeVINSFusion = 9, kTypeOpenVINS = 10, kTypeFLOAM = 11, - kTypeOpen3D = 12 + kTypeOpen3D = 12, + kTypeCuVSLAM = 13 }; public: diff --git a/corelib/include/rtabmap/core/Parameters.h b/corelib/include/rtabmap/core/Parameters.h index 63fca366..6284d2cb 100644 --- a/corelib/include/rtabmap/core/Parameters.h +++ b/corelib/include/rtabmap/core/Parameters.h @@ -456,7 +456,7 @@ class RTABMAP_CORE_EXPORT Parameters RTABMAP_PARAM(GTSAM, IncRelinearizeSkip, int, 1, "Only relinearize any variables every X calls to ISAM2::update(). See GTSAM::ISAM2 doc for more info."); // Odometry - RTABMAP_PARAM(Odom, Strategy, int, 0, "0=Frame-to-Map (F2M) 1=Frame-to-Frame (F2F) 2=Fovis 3=viso2 4=DVO-SLAM 5=ORB_SLAM2 6=OKVIS 7=LOAM 8=MSCKF_VIO 9=VINS-Fusion 10=OpenVINS 11=FLOAM 12=Open3D"); + RTABMAP_PARAM(Odom, Strategy, int, 0, "0=Frame-to-Map (F2M) 1=Frame-to-Frame (F2F) 2=Fovis 3=viso2 4=DVO-SLAM 5=ORB_SLAM2 6=OKVIS 7=LOAM 8=MSCKF_VIO 9=VINS-Fusion 10=OpenVINS 11=FLOAM 12=Open3D 13=cuVSLAM"); RTABMAP_PARAM(Odom, ResetCountdown, int, 0, "Automatically reset odometry after X consecutive images where odometry cannot be computed (a value of 0 disables auto-reset). When a reset occurs, odometry resumes from the last successfully computed pose with large covariance to trigger a new map. If external odometry is used, it will also be reset based on the motion estimated relative to the last computed pose but no large covariance will be received, so that a new map won't be triggered."); RTABMAP_PARAM(Odom, Holonomic, bool, true, "If the robot is holonomic (strafing commands can be issued). If not, y value will be estimated from x and yaw values (y=x*tan(yaw))."); RTABMAP_PARAM(Odom, FillInfoData, bool, true, "Fill info with data (inliers/outliers features)."); diff --git a/corelib/include/rtabmap/core/odometry/OdometryCuVSLAM.h b/corelib/include/rtabmap/core/odometry/OdometryCuVSLAM.h new file mode 100644 index 00000000..92b76849 --- /dev/null +++ b/corelib/include/rtabmap/core/odometry/OdometryCuVSLAM.h @@ -0,0 +1,86 @@ +/* +Copyright (c) 2025 Felix Toft +Copyright (c) 2010-2016, Mathieu Labbe - IntRoLab - Universite de Sherbrooke +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Universite de Sherbrooke nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef ODOMETRYCUVSLAM_H_ +#define ODOMETRYCUVSLAM_H_ + +#include +#include + +#ifdef RTABMAP_CUVSLAM +#include +#include +#include +#endif + +namespace rtabmap { + +class RTABMAP_CORE_EXPORT OdometryCuVSLAM : public Odometry +{ +public: + OdometryCuVSLAM(const rtabmap::ParametersMap & parameters = rtabmap::ParametersMap()); + virtual ~OdometryCuVSLAM(); + + virtual void reset(const Transform & initialPose = Transform::getIdentity()); + virtual Odometry::Type getType() {return Odometry::kTypeCuVSLAM;} + +private: + virtual Transform computeTransform(SensorData & image, const Transform & guess = Transform(), OdometryInfo * info = 0); + +private: +#ifdef RTABMAP_CUVSLAM + CUVSLAM_TrackerHandle cuvslam_handle_; + CUVSLAM_GroundConstraintHandle ground_constraint_handle_; + + std::vector cuvslam_cameras_; + std::vector> intrinsics_; + + // State tracking + bool initialized_; + bool lost_; + bool tracking_; + bool planar_constraints_; + Transform previous_pose_; + double last_timestamp_; + + //visualization + std::vector observations_; + std::vector landmarks_; + + // GPU memory management + std::vector gpu_left_image_data_; // pointers to all gpu images + std::vector gpu_right_image_data_; + std::vector gpu_left_image_sizes_; // size of one image + std::vector gpu_right_image_sizes_; + cudaStream_t cuda_stream_; +#endif +}; + +} + +#endif /* ODOMETRYCUVSLAM_H_ */ \ No newline at end of file diff --git a/corelib/src/CMakeLists.txt b/corelib/src/CMakeLists.txt index bc2cd392..c1d5c083 100644 --- a/corelib/src/CMakeLists.txt +++ b/corelib/src/CMakeLists.txt @@ -101,6 +101,7 @@ SET(SRC_FILES odometry/OdometryVINSFusion.cpp odometry/OdometryOpenVINS.cpp odometry/OdometryOpen3D.cpp + odometry/OdometryCuVSLAM.cpp IMU.cpp IMUThread.cpp @@ -764,6 +765,13 @@ IF(ORB_SLAM_FOUND) ) ENDIF(ORB_SLAM_FOUND) +IF(CUVSLAM_FOUND) + SET(LIBRARIES + ${LIBRARIES} + cuvslam::cuvslam + ) +ENDIF(CUVSLAM_FOUND) + IF(GTSAM_FOUND) # Make sure GTSAM is built with system Eigen, not the included one in its package IF(GTSAM_INCLUDE_DIR) diff --git a/corelib/src/Odometry.cpp b/corelib/src/Odometry.cpp index 971fea04..79f4292a 100644 --- a/corelib/src/Odometry.cpp +++ b/corelib/src/Odometry.cpp @@ -39,6 +39,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/core/odometry/OdometryVINSFusion.h" #include "rtabmap/core/odometry/OdometryOpenVINS.h" #include "rtabmap/core/odometry/OdometryOpen3D.h" +#include "rtabmap/core/odometry/OdometryCuVSLAM.h" #include "rtabmap/core/OdometryInfo.h" #include "rtabmap/core/util3d.h" #include "rtabmap/core/util3d_mapping.h" @@ -112,6 +113,9 @@ Odometry * Odometry::create(Odometry::Type & type, const ParametersMap & paramet case Odometry::kTypeOpen3D: odometry = new OdometryOpen3D(parameters); break; + case Odometry::kTypeCuVSLAM: + odometry = new OdometryCuVSLAM(parameters); + break; default: UERROR("Unknown odometry type %d, using F2M instead...", (int)type); odometry = new OdometryF2M(parameters); diff --git a/corelib/src/odometry/OdometryCuVSLAM.cpp b/corelib/src/odometry/OdometryCuVSLAM.cpp new file mode 100644 index 00000000..c5ba56da --- /dev/null +++ b/corelib/src/odometry/OdometryCuVSLAM.cpp @@ -0,0 +1,1025 @@ +/* +Copyright (c) 2025 Felix Toft +Copyright (c) 2010-2016, Mathieu Labbe - IntRoLab - Universite de Sherbrooke +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Universite de Sherbrooke nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "rtabmap/core/odometry/OdometryCuVSLAM.h" +#include "rtabmap/core/OdometryInfo.h" +#include "rtabmap/utilite/ULogger.h" +#include "rtabmap/utilite/UTimer.h" + +#ifdef RTABMAP_CUVSLAM +#include "rtabmap/core/CameraModel.h" +#include "rtabmap/core/StereoCameraModel.h" +#include "rtabmap/core/SensorData.h" +#include "rtabmap/core/Transform.h" +#include "rtabmap/core/util3d_transforms.h" +#include +#include +#include +#include +#include + +// ============================================================================ +// Coordinate System Transformation Constants +// Based on Isaac ROS implementation: +// Source: isaac_ros_visual_slam/include/isaac_ros_visual_slam/impl/cuvslam_ros_conversion.hpp +// ============================================================================ + +// Transformation converting from +// Canonical ROS Frame (x-forward, y-left, z-up) to +// cuVSLAM Frame (x-right, y-up, z-backward) +// x -> -z +// y -> -x +// z -> y +const rtabmap::Transform cuvslam_pose_canonical( + 0, -1, 0, 0, + 0, 0, 1, 0, + -1, 0, 0, 0 +); + +// Transformation converting from +// cuVSLAM Frame (x-right, y-up, z-backward) to +// Canonical ROS Frame (x-forward, y-left, z-up) +const rtabmap::Transform canonical_pose_cuvslam = cuvslam_pose_canonical.inverse(); + +// Transformation converting from +// Optical Frame (x-right, y-down, z-forward) to +// cuVSLAM Frame (x-right, y-up, z-backward) +// Optical -> cuVSLAM +// x -> x +// y -> -y +// z -> -z +const rtabmap::Transform cuvslam_pose_optical( + 1, 0, 0, 0, + 0, -1, 0, 0, + 0, 0, -1, 0 +); + +// Transformation converting from +// cuVSLAM Frame (x-right, y-up, z-backward) to +// Optical Frame (x-right, y-down, z-forward) +const rtabmap::Transform optical_pose_cuvslam = cuvslam_pose_optical.inverse(); + + +// ============================================================================ +// Forward Declarations +// ============================================================================ + +namespace rtabmap { + +bool initializeCuVSLAM(const SensorData & data, + CUVSLAM_TrackerHandle & cuvslam_handle, + CUVSLAM_GroundConstraintHandle & ground_constraint_handle, + bool planar_constraints, + std::vector & gpu_left_image_data, + std::vector & gpu_right_image_data, + std::vector & gpu_left_image_sizes, + std::vector & gpu_right_image_sizes, + std::vector & cuvslam_cameras, + std::vector> & intrinsics, + cudaStream_t & cuda_stream); + +CUVSLAM_Configuration CreateConfiguration(const SensorData & data); + +bool prepareImages(const SensorData & data, + std::vector & cuvslam_images, + std::vector & gpu_left_image_data, + std::vector & gpu_right_image_data, + std::vector & gpu_left_image_sizes, + std::vector & gpu_right_image_sizes, + cudaStream_t & cuda_stream); + +cv::Mat convertCuVSLAMCovariance(const float * cuvslam_covariance); + +void printCovarianceMatrix(const cv::Mat & cov, const std::string & label); +void printRawCuvslamCovariance(const float * cuvslam_covariance, const std::string & label); + + +// ============================================================================ +// Transform Conversion Functions and Misc Helpers +// ============================================================================ + +// Helper function that converts RTAB-Map Transform into CUVSLAM_Pose +CUVSLAM_Pose TocuVSLAMPose(const Transform & rtabmap_transform) +{ + CUVSLAM_Pose cuvslamPose; + // RTAB-Map Transform is row major, but cuVSLAM is column major + // We need to transpose the rotation matrix when converting + const int32_t kRotationMatCol = 3; + const int32_t kRotationMatRow = 3; + int cuvslam_idx = 0; + for (int col_idx = 0; col_idx < kRotationMatCol; ++col_idx) { + for (int row_idx = 0; row_idx < kRotationMatRow; ++row_idx) { + // Access RTAB-Map Transform as (row, col) but store in column-major order for cuVSLAM + cuvslamPose.r[cuvslam_idx] = rtabmap_transform(row_idx, col_idx); + cuvslam_idx++; + } + } + + cuvslamPose.t[0] = rtabmap_transform.x(); + cuvslamPose.t[1] = rtabmap_transform.y(); + cuvslamPose.t[2] = rtabmap_transform.z(); + return cuvslamPose; +} + +// Helper function to convert cuVSLAM pose to RTAB-Map Transform +Transform FromcuVSLAMPose(const CUVSLAM_Pose & cuvslam_pose) +{ + const auto & r = cuvslam_pose.r; + const auto & t = cuvslam_pose.t; + // RTAB-Map Transform is row major and cuVSLAM rotation mat is column major. + Transform rtabmap_transform( + r[0], r[3], r[6], t[0], // r11, r12, r13, tx + r[1], r[4], r[7], t[1], // r21, r22, r23, ty + r[2], r[5], r[8], t[2] // r31, r32, r33, tz + ); + + return rtabmap_transform; +} + +void PrintConfiguration(const CUVSLAM_Configuration & cfg) +{ + UINFO("Use use_gpu: %s", cfg.use_gpu ? "true" : "false"); + UINFO("Enable IMU Fusion: %s", cfg.enable_imu_fusion ? "true" : "false"); + if (cfg.enable_imu_fusion) { + UINFO("gyroscope_noise_density: %f", + cfg.imu_calibration.gyroscope_noise_density); + UINFO("gyroscope_random_walk: %f", + cfg.imu_calibration.gyroscope_random_walk); + UINFO("accelerometer_noise_density: %f", + cfg.imu_calibration.accelerometer_noise_density); + UINFO("accelerometer_random_walk: %f", + cfg.imu_calibration.accelerometer_random_walk); + UINFO("frequency: %f", + cfg.imu_calibration.frequency); + } +} + +} // namespace rtabmap + +#endif + +// ============================================================================ +// OdometryCuVSLAM Class Implementation +// ============================================================================ + +namespace rtabmap { + +OdometryCuVSLAM::OdometryCuVSLAM(const ParametersMap & parameters) : + Odometry(parameters) +#ifdef RTABMAP_CUVSLAM + , + cuvslam_handle_(nullptr), + ground_constraint_handle_(nullptr), + initialized_(false), + lost_(false), + tracking_(false), + planar_constraints_(false), + previous_pose_(Transform::getIdentity()), + last_timestamp_(-1.0), + observations_(5000), + landmarks_(5000), + gpu_left_image_data_(), + gpu_right_image_data_(), + gpu_left_image_sizes_(), + gpu_right_image_sizes_(), + cuda_stream_(nullptr) +#endif +{ +#ifdef RTABMAP_CUVSLAM + Parameters::parse(parameters, Parameters::kRegForce3DoF(), planar_constraints_); +#endif +} + +OdometryCuVSLAM::~OdometryCuVSLAM() +{ +#ifdef RTABMAP_CUVSLAM + // Clean up cuVSLAM handles + if(cuvslam_handle_) + { + CUVSLAM_DestroyTracker(cuvslam_handle_); + } + if(ground_constraint_handle_){ + CUVSLAM_GroundConstraintDestroy(ground_constraint_handle_); + } + + // Clean up GPU memory + for(uint8_t * gpu_ptr : gpu_left_image_data_) { + if(gpu_ptr) { + cudaFree(gpu_ptr); + } + } + for(uint8_t * gpu_ptr : gpu_right_image_data_) { + if(gpu_ptr) { + cudaFree(gpu_ptr); + } + } + if(cuda_stream_) { + cudaStreamDestroy(cuda_stream_); + cuda_stream_ = nullptr; + } +#endif +} + +void OdometryCuVSLAM::reset(const Transform & initialPose) +{ + Odometry::reset(initialPose); + +#ifdef RTABMAP_CUVSLAM + // Clean up cuVSLAM handles + if(cuvslam_handle_) + { + CUVSLAM_DestroyTracker(cuvslam_handle_); + cuvslam_handle_ = nullptr; + } + if(ground_constraint_handle_){ + CUVSLAM_GroundConstraintDestroy(ground_constraint_handle_); + ground_constraint_handle_ = nullptr; + } + + // Clean up GPU memory + for(uint8_t * gpu_ptr : gpu_left_image_data_) { + if(gpu_ptr) { + cudaFree(gpu_ptr); + } + } + gpu_left_image_data_.clear(); + for(uint8_t * gpu_ptr : gpu_right_image_data_) { + if(gpu_ptr) { + cudaFree(gpu_ptr); + } + } + gpu_right_image_data_.clear(); + if(cuda_stream_) { + cudaStreamDestroy(cuda_stream_); + cuda_stream_ = nullptr; + } + + // Reset our internal state variables + gpu_left_image_sizes_.clear(); + gpu_right_image_sizes_.clear(); + cuvslam_cameras_.clear(); + intrinsics_.clear(); + initialized_ = false; + lost_ = false; + tracking_ = false; + previous_pose_ = Transform::getIdentity(); + last_timestamp_ = -1.0; +#endif +} + +Transform OdometryCuVSLAM::computeTransform( + SensorData & data, + const Transform & guess, + OdometryInfo * info) +{ +#ifdef RTABMAP_CUVSLAM + UTimer timer; + + // If we are lost after tracking has begun, return null transform + // We wait until a reset is triggered. + if(lost_ && tracking_) { + return Transform(); + } + + // Check if we have valid image data + if(data.imageRaw().empty() || data.rightRaw().empty()) + { + UERROR("cuVSLAM odometry only works with stereo cameras! It requires both left and right images! Left: %s, Right: %s", + data.imageRaw().empty() ? "empty" : "ok", + data.rightRaw().empty() ? "empty" : "ok"); + return Transform(); + } + + // Check if we have valid stereo camera models + if(data.stereoCameraModels().size() == 0) + { + UERROR("cuVSLAM odometry requires stereo camera models!"); + return Transform(); + } + + // Initialize cuVSLAM tracker on first frame + if(!initialized_) + { + if(!initializeCuVSLAM( + data, + cuvslam_handle_, + ground_constraint_handle_, + planar_constraints_, + gpu_left_image_data_, + gpu_right_image_data_, + gpu_left_image_sizes_, + gpu_right_image_sizes_, + cuvslam_cameras_, + intrinsics_, + cuda_stream_)) + { + UERROR("Failed to initialize cuVSLAM tracker"); + return Transform(); + } + + initialized_ = true; + if(info) + { + info->type = 0; + info->reg.covariance = cv::Mat::eye(6, 6, CV_64FC1) * 9999.0; + info->timeEstimation = timer.ticks(); + } + last_timestamp_ = data.stamp(); + return Transform(); + } + + // Prepare images for cuVSLAM + std::vector cuvslam_image_objects; + if(!prepareImages( + data, + cuvslam_image_objects, + gpu_left_image_data_, + gpu_right_image_data_, + gpu_left_image_sizes_, + gpu_right_image_sizes_, + cuda_stream_)) + { + UERROR("Failed to prepare images for cuVSLAM"); + return Transform(); + } + + // Process IMU data if available + if(!data.imu().empty()) + { + // TODO: Implement IMU processing + UWARN("IMU data available but processing not implemented yet"); + } + + // Validate images and tracker status + if(cuvslam_image_objects.empty()) { + UERROR("No images prepared for cuVSLAM tracking"); + return Transform(); + } + if(!cuvslam_handle_) { + UERROR("cuVSLAM tracker is null! initialized_: %s", initialized_ ? "true" : "false"); + return Transform(); + } + + // Process wheel odom pose if available + CUVSLAM_Pose * predicted_pose_ptr = nullptr; + CUVSLAM_Pose predicted_pose; + if(!guess.isNull()) { + Transform absolute_guess = previous_pose_ * guess; + absolute_guess = cuvslam_pose_canonical * absolute_guess * canonical_pose_cuvslam; + predicted_pose = TocuVSLAMPose(absolute_guess); + predicted_pose_ptr = &predicted_pose; + } + + CUVSLAM_PoseEstimate vo_pose_estimate; + const CUVSLAM_Status vo_status = CUVSLAM_TrackGpuMem( + cuvslam_handle_, + cuvslam_image_objects.data(), + cuvslam_image_objects.size(), + predicted_pose_ptr, // can safely handle nullptr if no guess is provided + &vo_pose_estimate + ); + + if(vo_status != CUVSLAM_SUCCESS) + { + // Provide specific error message + const char * error_msg = "Unknown error"; + switch(vo_status) { + case 1: error_msg = "CUVSLAM_TRACKING_LOST"; break; + case 2: error_msg = "CUVSLAM_INVALID_PARAMETER"; break; + case 3: error_msg = "CUVSLAM_INVALID_IMAGE_FORMAT or CUVSLAM_INVALID_CAMERA_CONFIG"; break; + case 4: error_msg = "CUVSLAM_GPU_MEMORY_ERROR"; break; + case 5: error_msg = "CUVSLAM_INITIALIZATION_ERROR"; break; + default: error_msg = "Unknown cuVSLAM error"; break; + } + + UERROR("cuVSLAM tracking error: %d (%s)", vo_status, error_msg); + + if(info) + { + info->reg.covariance = cv::Mat::eye(6, 6, CV_64FC1) * 9999.0; + info->timeEstimation = timer.ticks(); + } + + last_timestamp_ = data.stamp(); + return Transform(); + } + + // Check if we have invalid covariance values + bool valid_covariance = true; + for(int i = 0; i < 6; i++) + { + float & diag_val = vo_pose_estimate.covariance[i*6+i]; + // We allow 1.0 as a valid value, since cuVSLAM sends identity covariance for the first few frames. + if(!std::isfinite(diag_val) || diag_val <= 0.0 || (diag_val > 0.1 && diag_val != 1.0)) + { + diag_val = 9999.0; // Set to high uncertainty + valid_covariance = false; + } + } + + // Convert to RTABMAP covariance format and scale to meet RTABMAP expectations + cv::Mat covMat = convertCuVSLAMCovariance(vo_pose_estimate.covariance); + + // Handle invalid covariance. Protect against low velocity cases. + if(!valid_covariance) { + double velocity_ms = 9999.0; + double angular_velocity_rad_s = 9999.0; + if(!guess.isNull()) { + double time_s = data.stamp() - last_timestamp_; + velocity_ms = guess.getNorm() / time_s; + angular_velocity_rad_s = guess.getAngle(Transform::getIdentity()) / time_s; + } + if(velocity_ms < 0.1 && angular_velocity_rad_s < 0.1 && last_timestamp_ != -1.0) { + covMat = cv::Mat::eye(6, 6, CV_64FC1) * 0.0001; + } else { + // If we have already begun tracking, now we are lost. + if(tracking_) { + UWARN("LOST: Velocity is high and covariance is invalid, setting lost to true"); + lost_ = true; + } + // Still send covariance for debugging + if(info) { + info->reg.covariance = covMat; + } + return Transform(); + } + } + + // Tracking was successful and the covariance is valid, set tracking to true + tracking_ = true; + + if(info) + { + info->reg.covariance = covMat; + info->timeEstimation = timer.ticks(); + } + + // Apply ground constraint + if(planar_constraints_) { + if(CUVSLAM_GroundConstraintAddNextPose(ground_constraint_handle_, &vo_pose_estimate.pose) != CUVSLAM_SUCCESS) { + UERROR("Failed to add next pose to ground constraint"); + return Transform(); + } + if(CUVSLAM_GroundConstraintGetPoseOnGround(ground_constraint_handle_, &vo_pose_estimate.pose) != CUVSLAM_SUCCESS) { + UERROR("Failed to get pose on ground"); + return Transform(); + } + } + + // Convert cuVSLAM pose to RTAB-Map Transform + Transform current_pose = FromcuVSLAMPose(vo_pose_estimate.pose); + current_pose = canonical_pose_cuvslam * current_pose * cuvslam_pose_canonical; + + // Calculate incremental transform + UASSERT(!previous_pose_.isNull()); + Transform transform = previous_pose_.inverse() * current_pose; + + // Fill info with visualization data + if(info) { + if(data.stereoCameraModels().size()==1) { + info->type = kTypeF2F; + + // extract 2D VO observations for visualization + CUVSLAM_ObservationVector observation_vector; + observation_vector.max = observations_.size(); + observation_vector.observations = observations_.data(); + CUVSLAM_Status observation_status = CUVSLAM_GetLastLeftObservations(cuvslam_handle_, &observation_vector); + if(observation_status == CUVSLAM_SUCCESS && observation_vector.num > 0) { + info->newCorners.reserve(observation_vector.num); + for(uint32_t i = 0; i < observation_vector.num; ++i) + { + const CUVSLAM_Observation & observation = observation_vector.observations[i]; + info->newCorners.emplace_back(observation.u, observation.v); + } + } + } + else { + info->type = kTypeF2M; + } + + // extract 3D VO landmarks for visualization + CUVSLAM_LandmarkVector landmark_vector; + landmark_vector.max = landmarks_.size(); + landmark_vector.landmarks = landmarks_.data(); + CUVSLAM_Status landmark_status = CUVSLAM_GetLastLandmarks(cuvslam_handle_, &landmark_vector); + std::vector local_transform_inv(data.stereoCameraModels().size()); + for(size_t i=0; i 0) { + Transform absolute_pose = this->getPose() * transform; + for(uint32_t i = 0; i < landmark_vector.num; ++i) + { + const CUVSLAM_Landmark & landmark = landmark_vector.landmarks[i]; + cv::Point3f pt = util3d::transformPoint(cv::Point3f(landmark.x, landmark.y, landmark.z), canonical_pose_cuvslam); + info->localMap.insert(std::make_pair(landmark.id, util3d::transformPoint(pt, absolute_pose))); + if(data.stereoCameraModels().size() > 1) { + for(size_t i=0; i 0) + { + data.stereoCameraModels()[i].left().reproject(pt_in_cam.x, pt_in_cam.y, pt_in_cam.z, u, v); + if(data.stereoCameraModels()[i].left().inFrame(u,v)) + { + info->words.insert(std::make_pair(landmark.id, cv::KeyPoint(u + i*image_width, v, 3))); + info->reg.inliersIDs.push_back(landmark.id); + break; + } + } + } + } + } + } + } + + previous_pose_ = current_pose; + last_timestamp_ = data.stamp(); + return transform; +#else + UERROR("cuVSLAM support not compiled in RTAB-Map");\ + return Transform(); +#endif + +} + +#ifdef RTABMAP_CUVSLAM + +// ============================================================================ +// cuVSLAM Initialization and Configuration +// ============================================================================ + +bool initializeCuVSLAM(const SensorData & data, + CUVSLAM_TrackerHandle & cuvslam_handle, + CUVSLAM_GroundConstraintHandle & ground_constraint_handle, + bool planar_constraints, + std::vector & gpu_left_image_data, + std::vector & gpu_right_image_data, + std::vector & gpu_left_image_sizes, + std::vector & gpu_right_image_sizes, + std::vector & cuvslam_cameras, + std::vector> & intrinsics, + cudaStream_t & cuda_stream) +{ + // cuVSLAM verbosity level (0=none, 1=errors, 2=warnings, 3=info) + CUVSLAM_SetVerbosity(0); + + // Initialize cuVSLAM cameras and intrinsic vectors + cuvslam_cameras.resize(data.stereoCameraModels().size()*2); + intrinsics.resize(data.stereoCameraModels().size()*2); + + // Handle stereo cameras + for(size_t i = 0; i < data.stereoCameraModels().size(); ++i) + { + const StereoCameraModel & stereoModel = data.stereoCameraModels()[i]; + if(!stereoModel.isValidForProjection()) + { + UERROR("Invalid stereo camera model %d for cuVSLAM initialization!", static_cast(i)); + return false; + } + const CameraModel & leftModel = stereoModel.left(); + const CameraModel & rightModel = stereoModel.right(); + + auto & cam_left = cuvslam_cameras[i*2]; + auto & cam_right = cuvslam_cameras[i*2+1]; + auto & intrinsics_left = intrinsics[i*2]; + auto & intrinsics_right = intrinsics[i*2+1]; + + // Left camera + cam_left.parameters = intrinsics_left.data(); + cam_left.width = leftModel.imageWidth(); + cam_left.height = leftModel.imageHeight(); + cam_left.distortion_model = "pinhole"; + cam_left.num_parameters = 4; + intrinsics_left[0] = leftModel.cx(); + intrinsics_left[1] = leftModel.cy(); + intrinsics_left[2] = leftModel.fx(); + intrinsics_left[3] = leftModel.fy(); + + // Transform sequence: + // cuvslam -> optical -> camera extrinsics in optical -> cuvslam + rtabmap::Transform extrinsics = cuvslam_pose_canonical * stereoModel.localTransform() * optical_pose_cuvslam; + cam_left.pose = TocuVSLAMPose(extrinsics); + cam_left.border_top = 0; + cam_left.border_bottom = leftModel.imageHeight(); + cam_left.border_left = 0; + cam_left.border_right = leftModel.imageWidth(); + + // Right camera + cam_right.parameters = intrinsics_right.data(); + cam_right.width = rightModel.imageWidth(); + cam_right.height = rightModel.imageHeight(); + cam_right.distortion_model = "pinhole"; + cam_right.num_parameters = 4; + intrinsics_right[0] = rightModel.cx(); + intrinsics_right[1] = rightModel.cy(); + intrinsics_right[2] = rightModel.fx(); + intrinsics_right[3] = rightModel.fy(); + Transform baseline_transform(1, 0, 0, stereoModel.baseline(), + 0, 1, 0, 0, + 0, 0, 1, 0); + // Transform sequence: + // cuvslam -> optical -> baseline offset in optical -> camera extrinsics in optical -> cuvslam + extrinsics = cuvslam_pose_canonical * stereoModel.localTransform() * baseline_transform * optical_pose_cuvslam; + cam_right.pose = TocuVSLAMPose(extrinsics); + cam_right.border_top = 0; + cam_right.border_bottom = rightModel.imageHeight(); + cam_right.border_left = 0; + cam_right.border_right = rightModel.imageWidth(); + } + + // Set up camera rig + CUVSLAM_CameraRig camera_rig; + camera_rig.cameras = cuvslam_cameras.data(); + camera_rig.num_cameras = cuvslam_cameras.size(); + + const CUVSLAM_Configuration configuration = CreateConfiguration(data); + PrintConfiguration(configuration); + + // Create tracker + CUVSLAM_TrackerHandle tracker_handle; + UTimer create_timer; create_timer.start(); + const CUVSLAM_Status status_tracker = CUVSLAM_CreateTracker(&tracker_handle, &camera_rig, &configuration); + + if (status_tracker != CUVSLAM_SUCCESS) { + UERROR("Failed to initialize CUVSLAM tracker: %d", status_tracker); + return false; + } + + cuvslam_handle = tracker_handle; + + // Initialize gpu image data vectors and sizes + size_t stereo_pairs_count = data.stereoCameraModels().size(); + gpu_left_image_data.resize(stereo_pairs_count, nullptr); + gpu_right_image_data.resize(stereo_pairs_count, nullptr); + gpu_left_image_sizes.resize(stereo_pairs_count, 0); + gpu_right_image_sizes.resize(stereo_pairs_count, 0); + + // initialize ground constraints + if (planar_constraints) + { + CUVSLAM_Pose identity_cuvslam = TocuVSLAMPose(Transform::getIdentity()); // same in both frames + + const CUVSLAM_Status status_ground = CUVSLAM_GroundConstraintCreate( + &ground_constraint_handle, + &identity_cuvslam, + &identity_cuvslam, + &identity_cuvslam + ); + if(status_ground != CUVSLAM_SUCCESS) { + UERROR("Failed to initialize CUVSLAM ground constraint: %d", status_ground); + return false; + } + } + + return true; +} + +/* +Implementation based on Isaac ROS VisualSlamNode::VisualSlamImpl::CreateConfiguration() +Source: isaac_ros_visual_slam/isaac_ros_visual_slam/src/impl/visual_slam_impl.cpp:379-422 +https://github.com/NVIDIA-ISAAC-ROS/isaac_ros_visual_slam/blob/19be8c781a55dee9cfbe9f097adca3986638feb1/isaac_ros_visual_slam/src/impl/visual_slam_impl.cpp#L379-L422 +*/ +CUVSLAM_Configuration CreateConfiguration(const SensorData & data) +{ + CUVSLAM_Configuration configuration; + CUVSLAM_InitDefaultConfiguration(&configuration); + + configuration.multicam_mode = data.stereoCameraModels().size()>1?1:0; + + // Core Visual Odometry Settings + configuration.use_motion_model = 1; // Enable motion model for better tracking + configuration.use_denoising = 0; // Disable denoising by default + configuration.use_gpu = 1; // Use GPU acceleration + configuration.horizontal_stereo_camera = 1; // Stereo camera configuration + + configuration.enable_observations_export = 1; // Export observations for external reading + + // SLAM Enabled (required for observation buffer allocation) + configuration.enable_localization_n_mapping = 0; // NO SLAM + configuration.enable_landmarks_export = 0; // SLAM feature (optional) + configuration.enable_reading_slam_internals = 0; // SLAM feature (optional) + + // IMU Configuration (If we later implement IMU support) + configuration.enable_imu_fusion = 0; //data.imu().empty()?0:1; + configuration.debug_imu_mode = 0; // Disable IMU debug mode + // imu_calibration.gyroscope_noise_density = 0.0002f; + // imu_calibration.gyroscope_random_walk = 0.00003f; + // imu_calibration.accelerometer_noise_density = 0.01f; + // imu_calibration.accelerometer_random_walk = 0.001f; + // imu_calibration.frequency = 200.0f; + // configuration.imu_calibration = imu_calibration; + + // configuration.max_frame_delta_ms = 100.0; // Maximum frame interval (100ms default) + + // SLAM-specific parameters (disabled) + configuration.planar_constraints = 0; + configuration.slam_throttling_time_ms = 0; + configuration.slam_max_map_size = 0; + configuration.slam_sync_mode = 0; + + // Use for getting debug images and logs + // configuration.debug_dump_directory = "/home/...your desired directory..."; + + return configuration; +} + +// ============================================================================ +// GPU Memory Management +// ============================================================================ + +bool allocateGpuMemory(size_t size, uint8_t ** gpu_ptr, size_t * current_size) +{ + if(*current_size != size) { + // Reallocate GPU memory if size changed + if(*gpu_ptr != nullptr) { + cudaFree(*gpu_ptr); + } + *gpu_ptr = nullptr; + cudaError_t cuda_err = cudaMalloc(gpu_ptr, size); + if(cuda_err != cudaSuccess) { + UERROR("Failed to allocate GPU memory: %s", cudaGetErrorString(cuda_err)); + return false; + } + *current_size = size; + } + return true; +} + +bool copyToGpuAsync(const cv::Mat & cpu_image, uint8_t * gpu_ptr, size_t size, cudaStream_t & cuda_stream) +{ + // Initialize CUDA stream if not already done + if(cuda_stream == nullptr) { + cudaError_t stream_err = cudaStreamCreate(&cuda_stream); + if(stream_err != cudaSuccess) { + UERROR("Failed to create CUDA stream: %s", cudaGetErrorString(stream_err)); + return false; + } + } + + // Copy CPU data to GPU memory with async operation for better performance + cudaError_t cuda_err = cudaMemcpyAsync(gpu_ptr, cpu_image.data, size, + cudaMemcpyHostToDevice, cuda_stream); + if(cuda_err != cudaSuccess) { + UERROR("Failed to copy image to GPU: %s", cudaGetErrorString(cuda_err)); + return false; + } + + return true; +} + +bool synchronizeGpuOperations(cudaStream_t & cuda_stream) +{ + if(cuda_stream) { + cudaError_t cuda_err = cudaStreamSynchronize(cuda_stream); + if(cuda_err != cudaSuccess) { + UERROR("Failed to synchronize GPU operations: %s", cudaGetErrorString(cuda_err)); + return false; + } + } + return true; +} + +// ============================================================================ +// Image Processing and Preparation +// ============================================================================ + +bool prepareImages(const SensorData & data, + std::vector & cuvslam_images, + std::vector & gpu_left_image_data, + std::vector & gpu_right_image_data, + std::vector & gpu_left_image_sizes, + std::vector & gpu_right_image_sizes, + cudaStream_t & cuda_stream) +{ + // Convert timestamp to nanoseconds (cuVSLAM expects nanoseconds) + int64_t timestamp_ns = static_cast(data.stamp() * 1000000000.0); + + // Horizontally stitched images received by RTAB-Map + cv::Mat left_image = data.imageRaw(); + cv::Mat right_image = data.rightRaw(); + + // Validate basic image properties + if(left_image.empty() || right_image.empty()) { + UERROR("No left or right image available for stereo camera"); + return false; + } + if(left_image.channels() != 1 && left_image.channels() != 3) { + UERROR("Unsupported left image format: %d channels", left_image.channels()); + return false; + } + if(right_image.channels() != 1 && right_image.channels() != 3) { + UERROR("Unsupported right image format: %d channels", right_image.channels()); + return false; + } + + // Convert image format for cuVSLAM - mono8 or rgb8 + cv::Mat processed_left_image; + cv::Mat processed_right_image; + CUVSLAM_ImageEncoding left_encoding; + CUVSLAM_ImageEncoding right_encoding; + + // process left image - copies image if BGR to RGB conversion is needed + processed_left_image = left_image; + if(left_image.channels() == 1) { + left_encoding = CUVSLAM_ImageEncoding::MONO8; + } else if(left_image.channels() == 3) { + // convert from BGR to RGB + cv::cvtColor(left_image, processed_left_image, cv::COLOR_BGR2RGB); + left_encoding = CUVSLAM_ImageEncoding::RGB8; + } else { + UERROR("Unsupported left image format: %d channels", left_image.channels()); + return false; + } + + // process right image - copies image if BGR to RGB conversion is needed + processed_right_image = right_image; + if(right_image.channels() == 1) { + right_encoding = CUVSLAM_ImageEncoding::MONO8; + } else if(right_image.channels() == 3) { + // convert from BGR to RGB + cv::cvtColor(right_image, processed_right_image, cv::COLOR_BGR2RGB); + right_encoding = CUVSLAM_ImageEncoding::RGB8; + } else { + UERROR("Unsupported right image format: %d channels", right_image.channels()); + return false; + } + + int camera_index = 0; + int stereo_index = 0; + for(const StereoCameraModel & model : data.stereoCameraModels()) { + // slice out the image for the current stereo pair + // Assumes all images have the same width and height + int left_image_width = model.left().imageWidth(); + int right_image_width = model.right().imageWidth(); + int left_image_height = model.left().imageHeight(); + int right_image_height = model.right().imageHeight(); + + // makes a copy for the sliced images + cv::Mat left_image_slice = processed_left_image(cv::Rect(stereo_index * left_image_width, 0, left_image_width, left_image_height)).clone(); + cv::Mat right_image_slice = processed_right_image(cv::Rect(stereo_index * right_image_width, 0, right_image_width, right_image_height)).clone(); + + size_t left_image_size = left_image_slice.total() * left_image_slice.elemSize(); + size_t right_image_size = right_image_slice.total() * right_image_slice.elemSize(); + + // Allocate GPU memory for left camera + if(!allocateGpuMemory(left_image_size, &gpu_left_image_data[stereo_index], &gpu_left_image_sizes[stereo_index])) { + UERROR("PREPARE IMAGES: Failed to allocate GPU memory for left image"); + return false; + } + if(!copyToGpuAsync(left_image_slice, gpu_left_image_data[stereo_index], left_image_size, cuda_stream)) { + UERROR("PREPARE IMAGES: Failed to copy left image to GPU"); + return false; + } + + // Create CUVSLAM_Image for left camera with GPU memory + CUVSLAM_Image left_cuvslam_image; + left_cuvslam_image.width = left_image_width; + left_cuvslam_image.height = left_image_height; + left_cuvslam_image.pixels = gpu_left_image_data[stereo_index]; // GPU memory pointer + left_cuvslam_image.timestamp_ns = timestamp_ns; + left_cuvslam_image.camera_index = camera_index; + left_cuvslam_image.pitch = left_image_slice.step; + left_cuvslam_image.image_encoding = left_encoding; + + cuvslam_images.push_back(left_cuvslam_image); + + camera_index++; + + // Allocate GPU memory for right camera + if(!allocateGpuMemory(right_image_size, &gpu_right_image_data[stereo_index], &gpu_right_image_sizes[stereo_index])) { + UERROR("PREPARE IMAGES: Failed to allocate GPU memory for right image"); + return false; + } + + if(!copyToGpuAsync(right_image_slice, gpu_right_image_data[stereo_index], right_image_size, cuda_stream)) { + UERROR("PREPARE IMAGES: Failed to copy right image to GPU"); + return false; + } + + CUVSLAM_Image right_cuvslam_image; + right_cuvslam_image.width = right_image_width; + right_cuvslam_image.height = right_image_height; + right_cuvslam_image.pixels = gpu_right_image_data[stereo_index]; // GPU memory pointer + right_cuvslam_image.timestamp_ns = timestamp_ns; + right_cuvslam_image.camera_index = camera_index; + right_cuvslam_image.pitch = right_image_slice.step; + right_cuvslam_image.image_encoding = right_encoding; + + cuvslam_images.push_back(right_cuvslam_image); + + stereo_index++; + camera_index++; + } + + // Synchronize all async GPU operations before returning + if(!synchronizeGpuOperations(cuda_stream)) { + UERROR("PREPARE IMAGES: Failed to synchronize GPU operations"); + return false; + } + + return true; +} + + +/* +Convert cuVSLAM covariance to RTAB-Map format. +Based on Isaac ROS implementation: FromcuVSLAMCovariance() +Source: isaac_ros_visual_slam/src/impl/cuvslam_ros_conversion.cpp:275-299 +*/ +cv::Mat convertCuVSLAMCovariance(const float * cuvslam_covariance) +{ + // Scale cuvslam covariance to make it more realistic + const double scaling_factor = 10.0; + + // Handle null covariance pointer + if(cuvslam_covariance == nullptr) + { + UWARN("Covariance was recieved as a nullptr, proceeding with default infinite covariance"); + cv::Mat default_infinite_covariance = cv::Mat::eye(6, 6, CV_64FC1) * 9999.0; + return default_infinite_covariance; + } + + const float * covariance = cuvslam_covariance; + + // Create transformation matrix for coordinate system conversion + // cuVSLAM frame (x-right, y-up, z-backward) to RTAB-Map frame (x-forward, y-left, z-up) + // Get rotation matrix from canonical_pose_cuvslam transform + Eigen::Matrix canonical_pose_cuvslam_mat; + canonical_pose_cuvslam_mat << + canonical_pose_cuvslam.r11(), canonical_pose_cuvslam.r12(), canonical_pose_cuvslam.r13(), + canonical_pose_cuvslam.r21(), canonical_pose_cuvslam.r22(), canonical_pose_cuvslam.r23(), + canonical_pose_cuvslam.r31(), canonical_pose_cuvslam.r32(), canonical_pose_cuvslam.r33(); + + // Create 6x6 block diagonal transformation matrix + Eigen::Matrix block_canonical_pose_cuvslam = Eigen::Matrix::Zero(); + block_canonical_pose_cuvslam.block<3, 3>(0, 0) = canonical_pose_cuvslam_mat; + block_canonical_pose_cuvslam.block<3, 3>(3, 3) = canonical_pose_cuvslam_mat; + + // Map cuVSLAM covariance array to Eigen matrix + Eigen::Matrix covariance_mat = + Eigen::Map>(const_cast(covariance)); + + // Reorder covariance matrix elements + // The covariance matrix from cuVSLAM arranges elements as follows: + // (rotation about X axis, rotation about Y axis, rotation about Z axis, x, y, z) + // However, in RTAB-Map, the order is: + // (x, y, z, rotation about X axis, rotation about Y axis, rotation about Z axis) + Eigen::Matrix rtabmap_covariance_mat = Eigen::Matrix::Zero(); + rtabmap_covariance_mat.block<3, 3>(0, 0) = covariance_mat.block<3, 3>(3, 3); // translation-translation + rtabmap_covariance_mat.block<3, 3>(0, 3) = covariance_mat.block<3, 3>(3, 0); // translation-rotation + rtabmap_covariance_mat.block<3, 3>(3, 0) = covariance_mat.block<3, 3>(0, 3); // rotation-translation + rtabmap_covariance_mat.block<3, 3>(3, 3) = covariance_mat.block<3, 3>(0, 0); // rotation-rotation + + // Apply coordinate system transformation + Eigen::Matrix covariance_mat_change_basis = + block_canonical_pose_cuvslam * rtabmap_covariance_mat * block_canonical_pose_cuvslam.transpose(); + + // Convert Eigen matrix to OpenCV Mat + cv::Mat cv_covariance(6, 6, CV_64FC1); + for(int i = 0; i < 6; i++) + { + for(int j = 0; j < 6; j++) + { + cv_covariance.at(i, j) = static_cast(covariance_mat_change_basis(i, j)); + // for angular values, scale again to make it more realistic + if(i > 2 || j > 2) { + cv_covariance.at(i, j) *= scaling_factor; + } + } + } + + // Ensure diagonal elements are positive and finite (RTAB-Map requirement) + return cv_covariance; +} + +#endif // RTABMAP_CUVSLAM + +} // namespace rtabmap + diff --git a/guilib/src/AboutDialog.cpp b/guilib/src/AboutDialog.cpp index c12cb8fd..ab96c410 100644 --- a/guilib/src/AboutDialog.cpp +++ b/guilib/src/AboutDialog.cpp @@ -297,6 +297,14 @@ AboutDialog::AboutDialog(QWidget * parent) : _ui->label_openvins_license->setEnabled(false); #endif +#ifdef RTABMAP_CUVSLAM + _ui->label_cuvslam->setText("Yes"); + _ui->label_cuvslam_license->setEnabled(true); +#else + _ui->label_cuvslam->setText("No"); + _ui->label_cuvslam_license->setEnabled(false); +#endif + } AboutDialog::~AboutDialog() diff --git a/guilib/src/ui/aboutDialog.ui b/guilib/src/ui/aboutDialog.ui index d78e8c7b..8df68638 100644 --- a/guilib/src/ui/aboutDialog.ui +++ b/guilib/src/ui/aboutDialog.ui @@ -1633,6 +1633,39 @@ p, li { white-space: pre-wrap; } + + + + With cuVSLAM : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + NVIDIA ISAAC ROS SOFTWARE LICENSE + + + true + + + diff --git a/guilib/src/ui/preferencesDialog.ui b/guilib/src/ui/preferencesDialog.ui index 69c52f09..77ae0a9a 100644 --- a/guilib/src/ui/preferencesDialog.ui +++ b/guilib/src/ui/preferencesDialog.ui @@ -15728,6 +15728,11 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag Open3D + + + CuVSLAM + + From f0dab1800535703dc23bc0cf0add54299204ad8b Mon Sep 17 00:00:00 2001 From: matlabbe Date: Tue, 7 Oct 2025 10:22:16 -0700 Subject: [PATCH 026/126] Orbbec SDK v2 integration (new CameraOrbbecSDK class) (#1590) * Orbbec SDK 2 integration (new CameraOrbbecSDK class) * Added rectification option, fixed the build without orbbec sdk * Fixed timestamp issue on Windows * Removed WITH_ORBBEC_SDK=ON by default in case v1 is installed. Enabled inter imu when selecting orbbec sdk. * requiring orbbec version=2 to avoid confusion * Finalizing options --- CMakeLists.txt | 21 +- Version.h.in | 1 + app/src/CMakeLists.txt | 2 +- corelib/include/rtabmap/core/CameraRGBD.h | 1 + .../rtabmap/core/camera/CameraOrbbecSDK.h | 105 + corelib/src/CMakeLists.txt | 8 + corelib/src/OdometryThread.cpp | 11 +- corelib/src/camera/CameraOrbbecSDK.cpp | 728 +++++ examples/RGBDMapping/main.cpp | 147 +- guilib/include/rtabmap/gui/MainWindow.h | 1 + .../include/rtabmap/gui/PreferencesDialog.h | 1 + guilib/src/AboutDialog.cpp | 2 + guilib/src/GuiLib.qrc | 1 + guilib/src/ImageView.cpp | 8 +- guilib/src/MainWindow.cpp | 8 + guilib/src/PreferencesDialog.cpp | 73 + guilib/src/images/astra2.png | Bin 0 -> 3334 bytes guilib/src/ui/aboutDialog.ui | 2567 +++++++++-------- guilib/src/ui/consoleWidget.ui | 2 +- guilib/src/ui/mainWindow.ui | 19 + guilib/src/ui/preferencesDialog.ui | 972 ++++--- tools/CameraRGBD/main.cpp | 216 +- 22 files changed, 3078 insertions(+), 1816 deletions(-) create mode 100644 corelib/include/rtabmap/core/camera/CameraOrbbecSDK.h create mode 100644 corelib/src/camera/CameraOrbbecSDK.cpp create mode 100644 guilib/src/images/astra2.png diff --git a/CMakeLists.txt b/CMakeLists.txt index 76d9cd5d..f7e1551b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -207,6 +207,7 @@ option(WITH_REALSENSE2 "Include RealSense support" ON) option(WITH_MYNTEYE "Include mynteye-s support" ON) option(WITH_DEPTHAI "Include depthai-core support" OFF) option(WITH_XVSDK "Include XVisio SDK support" OFF) +option(WITH_ORBBEC_SDK "Include Orbbec SDK v2 support" OFF) option(WITH_OCTOMAP "Include OctoMap support" ON) option(WITH_GRIDMAP "Include GridMap support" OFF) option(WITH_CPUTSDF "Include CPUTSDF support" OFF) @@ -698,6 +699,13 @@ IF(WITH_XVSDK) ENDIF(xvsdk_FOUND) ENDIF(WITH_XVSDK) +IF(WITH_ORBBEC_SDK) + FIND_PACKAGE(OrbbecSDK 2 QUIET) + IF(OrbbecSDK_FOUND) + MESSAGE(STATUS "Found OrbbecSDK v2 (targets)") + ENDIF(OrbbecSDK_FOUND) +ENDIF(WITH_ORBBEC_SDK) + IF(WITH_OCTOMAP) FIND_PACKAGE(octomap QUIET) IF(octomap_FOUND) @@ -1080,6 +1088,9 @@ IF(NOT xvsdk_FOUND) ELSE() SET(CONF_WITH_XVSDK 1) ENDIF() +IF(NOT OrbbecSDK_FOUND) + SET(ORBBEC_SDK "//") +ENDIF(NOT OrbbecSDK_FOUND) IF(NOT octomap_FOUND) SET(OCTOMAP "//") SET(CONF_WITH_OCTOMAP 0) @@ -1592,7 +1603,7 @@ ENDIF() IF(grid_map_core_FOUND) MESSAGE(STATUS " With GridMap ${grid_map_core_VERSION} = YES (License: BSD)") -ELSEIF(NOT WITH_OCTOMAP) +ELSEIF(NOT WITH_GRIDMAP) MESSAGE(STATUS " With GridMap = NO (WITH_GRIDMAP=OFF)") ELSE() MESSAGE(STATUS " With GridMap = NO (grid_map_core not found)") @@ -1755,6 +1766,14 @@ ELSE() MESSAGE(STATUS " With XVisio SDK = NO (xvsdk not found)") ENDIF() +IF(OrbbecSDK_FOUND) +MESSAGE(STATUS " With Orbbec SDK ${OrbbecSDK_VERSION} = YES (License: MIT)") +ELSEIF(NOT WITH_ORBBEC_SDK) +MESSAGE(STATUS " With Orbbec SDK = NO (WITH_ORBBEC_SDK=OFF)") +ELSE() +MESSAGE(STATUS " With Orbbec SDK = NO (OrbbecSDK v2 not found)") +ENDIF() + MESSAGE(STATUS "") MESSAGE(STATUS " Odometry Approaches:") IF(loam_velodyne_FOUND) diff --git a/Version.h.in b/Version.h.in index 9863ef84..dc7ac90d 100644 --- a/Version.h.in +++ b/Version.h.in @@ -72,6 +72,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. @MYNTEYE@#define RTABMAP_MYNTEYE @DEPTHAI@#define RTABMAP_DEPTHAI @XVSDK@#define RTABMAP_XVSDK +@ORBBEC_SDK@#define RTABMAP_ORBBEC_SDK @OCTOMAP@#define RTABMAP_OCTOMAP @GRIDMAP@#define RTABMAP_GRIDMAP @CPUTSDF@#define RTABMAP_CPUTSDF diff --git a/app/src/CMakeLists.txt b/app/src/CMakeLists.txt index 2ed54b57..1a16cb60 100644 --- a/app/src/CMakeLists.txt +++ b/app/src/CMakeLists.txt @@ -34,7 +34,7 @@ ENDIF() IF(APPLE AND BUILD_AS_BUNDLE) ADD_EXECUTABLE(rtabmap_app MACOSX_BUNDLE ${SRC_FILES}) ELSEIF(WIN32 AND BUILD_AS_BUNDLE) - ADD_EXECUTABLE(rtabmap_app WIN32 ${SRC_FILES}) + ADD_EXECUTABLE(rtabmap_app ${SRC_FILES}) ELSE() ADD_EXECUTABLE(rtabmap_app ${SRC_FILES}) ENDIF() diff --git a/corelib/include/rtabmap/core/CameraRGBD.h b/corelib/include/rtabmap/core/CameraRGBD.h index afb2acff..11439ce8 100644 --- a/corelib/include/rtabmap/core/CameraRGBD.h +++ b/corelib/include/rtabmap/core/CameraRGBD.h @@ -38,3 +38,4 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include diff --git a/corelib/include/rtabmap/core/camera/CameraOrbbecSDK.h b/corelib/include/rtabmap/core/camera/CameraOrbbecSDK.h new file mode 100644 index 00000000..1d7bdf08 --- /dev/null +++ b/corelib/include/rtabmap/core/camera/CameraOrbbecSDK.h @@ -0,0 +1,105 @@ +/* +Copyright (c) 2010-2025, Mathieu Labbe - IntRoLab - Universite de Sherbrooke +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Universite de Sherbrooke nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include "rtabmap/core/CameraModel.h" +#include "rtabmap/core/Camera.h" +#include "rtabmap/core/Version.h" + +#ifdef RTABMAP_ORBBEC_SDK +namespace ob +{ + class Pipeline; + class Align; +} +#endif + +namespace rtabmap +{ + +class RTABMAP_CORE_EXPORT CameraOrbbecSDK : + public Camera +{ +public: + static bool available(); + +public: + // deviceId can be either an index (e.g., "0"), an UID (e.g, "2-1-2" or "gmsl-1") or a serial ("AAA6454S") + CameraOrbbecSDK( + std::string deviceId = "", + int colorWidth = 800, + int colorHeight = 600, + int depthWidth = 800, + int depthHeight = 600, + float imageRate = 0.0f, + const Transform & localTransform = Transform::getIdentity()); + virtual ~CameraOrbbecSDK(); + + virtual bool init(const std::string & calibrationFolder = ".", const std::string & cameraName = ""); + virtual bool isCalibrated() const; + virtual std::string getSerial() const; + + void close(); + + // Should be set before initializing + void enableColorRectification(bool enabled); + void enableImu(bool enabled); + + void enableDepthMM(bool enabled); + +protected: + virtual SensorData captureImage(SensorCaptureInfo * info = 0); + +private: + +#ifdef RTABMAP_ORBBEC_SDK + std::string deviceId_; + int colorWidth_; + int colorHeight_; + int depthWidth_; + int depthHeight_; + ob::Pipeline * pipeline_; + ob::Pipeline * imuPipeline_; + ob::Align * alignFilter_; + CameraModel model_; + Transform imuLocalTransform_; + bool imuLocalTransformInitialized_; + uint64_t lastAccStamp_; + uint64_t lastImageStamp_; + bool globalTimestampAvailable_; + bool rectifyColor_; + bool convertDepthToMM_; + bool imuPublished_; + std::map imuBuffer_; + UMutex imuMutex_; +#endif + +}; + + +} // namespace rtabmap diff --git a/corelib/src/CMakeLists.txt b/corelib/src/CMakeLists.txt index c1d5c083..efd8ece1 100644 --- a/corelib/src/CMakeLists.txt +++ b/corelib/src/CMakeLists.txt @@ -41,6 +41,7 @@ SET(SRC_FILES camera/CameraMyntEye.cpp camera/CameraDepthAI.cpp camera/CameraSeerSense.cpp + camera/CameraOrbbecSDK.cpp EpipolarGeometry.cpp VisualWord.cpp @@ -391,6 +392,13 @@ IF(xvsdk_FOUND) ) ENDIF(xvsdk_FOUND) +IF(OrbbecSDK_FOUND) + SET(LIBRARIES + ${LIBRARIES} + ob::OrbbecSDK + ) +ENDIF(OrbbecSDK_FOUND) + IF(TARGET OpenMP::OpenMP_CXX) SET(LIBRARIES ${LIBRARIES} diff --git a/corelib/src/OdometryThread.cpp b/corelib/src/OdometryThread.cpp index b7986cc6..fa216ccf 100644 --- a/corelib/src/OdometryThread.cpp +++ b/corelib/src/OdometryThread.cpp @@ -32,6 +32,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/core/OdometryInfo.h" #include "rtabmap/core/OdometryEvent.h" #include "rtabmap/utilite/ULogger.h" +#include "rtabmap/utilite/UTimer.h" namespace rtabmap { @@ -185,16 +186,18 @@ void OdometryThread::addData(const SensorEvent & event) UWARN("Received image/lidar with stamp (%f) older than oldest received imu " "(%f), skipping that frame (imu buffer size=%ld). " "When using async IMU, make sure IMU is published faster " - "than camera/lidar (assuming IMU latency is very small compared to camera/lidar).", - event.data().stamp(), _oldestAsyncImuStamp, _imuBuffer.size()); + "than camera/lidar (assuming IMU latency is very small compared to camera/lidar)." + "Current camera/lidar delay is %fs.", + event.data().stamp(), _oldestAsyncImuStamp, _imuBuffer.size(), UTimer::now() - event.data().stamp()); notify = false; } else if(_newestAsyncImuStamp > 0.0 && event.data().stamp()>=_newestAsyncImuStamp) { UWARN("Received image/lidar with stamp (%f) newer than latest received imu " "(%f), skipping that frame (imu buffer size=%ld). " "When using async IMU, make sure IMU is published faster " - "than camera/lidar (assuming IMU latency is very small compared to camera/lidar).", - event.data().stamp(), _newestAsyncImuStamp, _imuBuffer.size()); + "than camera/lidar (assuming IMU latency is very small compared to camera/lidar). " + "Current camera/lidar delay is %fs.", + event.data().stamp(), _newestAsyncImuStamp, _imuBuffer.size(), UTimer::now() - event.data().stamp()); notify = false; } else { diff --git a/corelib/src/camera/CameraOrbbecSDK.cpp b/corelib/src/camera/CameraOrbbecSDK.cpp new file mode 100644 index 00000000..d93d332c --- /dev/null +++ b/corelib/src/camera/CameraOrbbecSDK.cpp @@ -0,0 +1,728 @@ +/* +Copyright (c) 2010-2025, Mathieu Labbe - IntRoLab - Universite de Sherbrooke +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Universite de Sherbrooke nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include +#include + +#ifdef RTABMAP_ORBBEC_SDK +#include +#endif + +namespace rtabmap +{ +#ifdef RTABMAP_ORBBEC_SDK +Transform obToRtabmap(const OBExtrinsic & t) +{ + return Transform(t.rot[0], t.rot[1], t.rot[2], t.trans[0]/1000.0f, + t.rot[3], t.rot[4], t.rot[5], t.trans[1]/1000.0f, + t.rot[6], t.rot[7], t.rot[8], t.trans[2]/1000.0f); +} + + +cv::Mat obColorFrameToCv(const ob::VideoFrame & videoFrame) +{ + cv::Mat rgb; + switch(videoFrame.getFormat()) { + case OB_FORMAT_MJPG: { + cv::Mat rawMat(1, videoFrame.getDataSize(), CV_8UC1, videoFrame.getData()); + rgb = cv::imdecode(rawMat, 1); + } break; + case OB_FORMAT_NV21: { + cv::Mat rawMat(videoFrame.getHeight() * 3 / 2, videoFrame.getWidth(), CV_8UC1, videoFrame.getData()); + cv::cvtColor(rawMat, rgb, cv::COLOR_YUV2BGR_NV21); + } break; + case OB_FORMAT_YUYV: + case OB_FORMAT_YUY2: { + cv::Mat rawMat(videoFrame.getHeight(), videoFrame.getWidth(), CV_8UC2, videoFrame.getData()); + cv::cvtColor(rawMat, rgb, cv::COLOR_YUV2BGR_YUY2); + } break; + case OB_FORMAT_BGR: { + cv::Mat rawMat(videoFrame.getHeight(), videoFrame.getWidth(), CV_8UC3, videoFrame.getData()); + cv::cvtColor(rawMat, rgb, cv::COLOR_BGR2RGB); + } break; + case OB_FORMAT_RGB: { + cv::Mat rawMat(videoFrame.getHeight(), videoFrame.getWidth(), CV_8UC3, videoFrame.getData()); + cv::cvtColor(rawMat, rgb, cv::COLOR_RGB2BGR); + } break; + case OB_FORMAT_RGBA: { + cv::Mat rawMat(videoFrame.getHeight(), videoFrame.getWidth(), CV_8UC4, videoFrame.getData()); + cv::cvtColor(rawMat, rgb, cv::COLOR_RGBA2BGR); + } break; + case OB_FORMAT_BGRA: { + cv::Mat rawMat(videoFrame.getHeight(), videoFrame.getWidth(), CV_8UC4, videoFrame.getData()); + cv::cvtColor(rawMat, rgb, cv::COLOR_BGRA2RGB); + } break; + case OB_FORMAT_UYVY: { + cv::Mat rawMat(videoFrame.getHeight(), videoFrame.getWidth(), CV_8UC2, videoFrame.getData()); + cv::cvtColor(rawMat, rgb, cv::COLOR_YUV2BGR_UYVY); + } break; + case OB_FORMAT_I420: { + cv::Mat rawMat(videoFrame.getHeight() * 3 / 2, videoFrame.getWidth(), CV_8UC1, videoFrame.getData()); + cv::cvtColor(rawMat, rgb, cv::COLOR_YUV2BGR_I420); + } break; + case OB_FORMAT_Y8: { + rgb = cv::Mat(videoFrame.getHeight(), videoFrame.getWidth(), CV_8UC1, videoFrame.getData()).clone(); + } break; + case OB_FORMAT_Y16: { + cv::Mat rawMat(videoFrame.getHeight(), videoFrame.getWidth(), CV_16UC1, videoFrame.getData()); + rawMat.convertTo(rgb, CV_8UC1, 255.0 / 65535.0); + } break; + default: + break; + } + return rgb; +} + +cv::Mat obDepthFrameToCv(const ob::DepthFrame & depthFrame) +{ + cv::Mat depth; + if(depthFrame.getFormat() == OB_FORMAT_Y16 || depthFrame.getFormat() == OB_FORMAT_Z16 || depthFrame.getFormat() == OB_FORMAT_Y12C4) { + cv::Mat rawMat = cv::Mat(depthFrame.getHeight(), depthFrame.getWidth(), CV_16UC1, depthFrame.getData()); + float scale = depthFrame.getValueScale() / 1000.0f; + rawMat.convertTo(depth, CV_32F, scale); + } + return depth; +} + +cv::Mat obIntrinsicToK(const OBCameraIntrinsic & intrinsics) +{ + cv::Mat K = cv::Mat::eye(3,3,CV_64FC1); + K.at(0,0) = intrinsics.fx; + K.at(1,1) = intrinsics.fy; + K.at(0,2) = intrinsics.cx; + K.at(1,2) = intrinsics.cy; + return K; +} +cv::Mat obIntrinsicToP(const OBCameraIntrinsic & intrinsics) +{ + cv::Mat P = cv::Mat::eye(3,4,CV_64FC1); + obIntrinsicToK(intrinsics).copyTo(P.colRange(0,3)); + return P; +} +cv::Mat obDistortionToD(const OBCameraDistortion & distortion) +{ + cv::Mat D = cv::Mat(1,8,CV_64FC1); + D.at(0,0) = distortion.k1; + D.at(0,1) = distortion.k2; + D.at(0,2) = distortion.p1; + D.at(0,3) = distortion.p2; + D.at(0,4) = distortion.k3; + D.at(0,5) = distortion.k4; + D.at(0,6) = distortion.k5; + D.at(0,7) = distortion.k6; + if(distortion.k4 == 0 && distortion.k5 == 0 && distortion.k6 == 0) + { + D = D.colRange(0,5); + } + return D; +} +#endif + +bool CameraOrbbecSDK::available() +{ +#ifdef RTABMAP_ORBBEC_SDK + return true; +#else + return false; +#endif +} + +CameraOrbbecSDK::CameraOrbbecSDK( + std::string deviceId, + int colorWidth, + int colorHeight, + int depthWidth, + int depthHeight, + float imageRate, + const Transform & localTransform) : + Camera(imageRate, localTransform) +#ifdef RTABMAP_ORBBEC_SDK + , deviceId_(deviceId), + colorWidth_(colorWidth), + colorHeight_(colorHeight), + depthWidth_(depthWidth), + depthHeight_(depthHeight), + pipeline_(nullptr), + imuPipeline_(nullptr), + alignFilter_(nullptr), + imuLocalTransformInitialized_(false), + lastAccStamp_(0), + lastImageStamp_(0), + globalTimestampAvailable_(false), + rectifyColor_(false), + convertDepthToMM_(true), + imuPublished_(true) +#endif +{ +} + +CameraOrbbecSDK::~CameraOrbbecSDK() +{ +#ifdef RTABMAP_ORBBEC_SDK + this->close(); +#endif +} + +void CameraOrbbecSDK::close() +{ +#ifdef RTABMAP_ORBBEC_SDK + if(imuPipeline_) { + imuPipeline_->stop(); + delete imuPipeline_; + imuPipeline_=nullptr; + } + + if(pipeline_) { + pipeline_->stop(); + delete pipeline_; + pipeline_=nullptr; + } + delete alignFilter_; + alignFilter_ = nullptr; + imuLocalTransform_ = Transform(); + imuLocalTransformInitialized_ = false; + lastAccStamp_ = 0; + lastImageStamp_ = 0; + globalTimestampAvailable_ = false; + model_ = CameraModel(); + imuBuffer_.clear(); +#endif +} + +bool CameraOrbbecSDK::init(const std::string & calibrationFolder, const std::string & cameraName) +{ +#ifdef RTABMAP_ORBBEC_SDK + this->close(); + + std::shared_ptr device; + + ob::Context context; + auto devices = context.queryDeviceList(); + UINFO("%d device(s) found", devices->getCount()); + for(uint32_t i=0; igetCount(); ++i) + { + auto currentDevice = devices->getDevice(i); + auto info = currentDevice->getDeviceInfo(); + + if(deviceId_.find('-') != std::string::npos) + { + // UID + if(deviceId_.compare(info->getUid()) == 0) { + device = currentDevice; + } + } + else if(uSplitNumChar(deviceId_).size() > 1) + { + // Serial + if(deviceId_.compare(info->getSerialNumber()) == 0) { + device = currentDevice; + } + } + else if((deviceId_.empty() && i==0) || + (!deviceId_.empty() && uStr2Int(deviceId_) == (int)i)) { + // Index + device = currentDevice; + } + + std::string type = "Unknown"; + switch(info->getDeviceType()) + { + case OB_STRUCTURED_LIGHT_MONOCULAR_CAMERA: + type = "Structured Light Monocular Camera"; + break; + case OB_STRUCTURED_LIGHT_BINOCULAR_CAMERA: + type = "Structured Light Binocular Camera"; + break; + case OB_TOF_CAMERA: + type = "TOF Camera"; + break; + default: + break; + } + UINFO("Device %ld:", i); + UINFO(" Name: %s", info->getName()); + UINFO(" Type: %s", type.c_str()); + UINFO(" Serial: %s", info->getSerialNumber()); + UINFO(" UID: %s", info->getUid()); + UINFO(" Chip: %s", info->getAsicName()); + UINFO(" Hardware version: %s", info->getHardwareVersion()); + UINFO(" Firmware version: %s", info->getFirmwareVersion()); + } + + if(device.get() == nullptr) { + if(deviceId_.empty()) { + UERROR( "Could not find any orbbec compatible devices! Verify that the " + "camera is correctly connected and the udev rules are installed."); + } + else { + UERROR("Could not find an orbbec device with ID \"%s\"! Verify that the " + "camera is correctly connected and the udev rules are installed. " + "Unset the ID to choose the first camera found."); + } + + return false; + } + + bool hasGyro = false; + bool hasAccel = false; + auto sensors = device->getSensorList(); + + if(device->isGlobalTimestampSupported()) + { + UINFO("Global (host time sync) timestamp is supported."); + device->enableGlobalTimestamp(true); + globalTimestampAvailable_ = true; + } + else + { + UWARN("Global (host time sync) timestamp is not supported! We will use device timestamp, so the camera frames won't be synchronizable with other sensors."); + } + + uint32_t maxColorFps = 0; + uint32_t maxDepthFps = 0; + for(uint32_t i=0; igetCount(); ++i) + { + if(sensors->getSensorType(i) == OB_SENSOR_GYRO) + { + hasGyro = true; + } + if(sensors->getSensorType(i) == OB_SENSOR_ACCEL) + { + hasAccel = true; + } + if( sensors->getSensorType(i) == OB_SENSOR_DEPTH || + sensors->getSensorType(i) == OB_SENSOR_COLOR) + { + auto profiles = sensors->getSensor(i)->getStreamProfileList(); + UINFO("Supported %s profiles:", sensors->getSensorType(i) == OB_SENSOR_DEPTH?"depth":"color"); + for(uint32_t j=0; jgetCount(); ++j) + { + auto profile = profiles->getProfile(j)->as(); + UINFO("Resolution: %ldx%ld, FPS: %ld, Format: %d", + profile->getWidth(), profile->getHeight(), profile->getFps(), profile->getFormat(), j==0?" (default)":""); + + if(sensors->getSensorType(i) == OB_SENSOR_DEPTH) { + if(profile->getFps() > maxDepthFps) { + maxDepthFps = profile->getFps(); + } + } + else + { + if(profile->getFps() > maxColorFps) { + maxColorFps = profile->getFps(); + } + } + + } + } + } + + std::shared_ptr imuConfig; + if(imuPublished_) + { + if(hasGyro && hasAccel) + { + imuPipeline_ = new ob::Pipeline(device); + imuConfig = std::make_shared(); + imuConfig->enableGyroStream(); + imuConfig->enableAccelStream(); + try { + UINFO("Starting imu pipeline"); + imuPipeline_->start(imuConfig, [&](std::shared_ptr frameSet) { + if(frameSet->getCount() != 2) + { + return; + } + + if(!imuLocalTransformInitialized_) + { + return; + } + + UASSERT(frameSet->getFrame(OB_FRAME_ACCEL) != nullptr && + frameSet->getFrame(OB_FRAME_GYRO) != nullptr); + + auto accel = frameSet->getFrame(OB_FRAME_ACCEL)->as(); + auto gyro = frameSet->getFrame(OB_FRAME_GYRO)->as(); + + uint64_t accelStampUs = globalTimestampAvailable_?accel->getGlobalTimeStampUs():accel->getTimeStampUs(); + uint64_t gyroStampUs = globalTimestampAvailable_?gyro->getGlobalTimeStampUs():gyro->getTimeStampUs(); + + if(accelStampUs != gyroStampUs) + { + UWARN("Received accel and gyro frames with different timestamps (%llu vs %llu), skipping.", + accelStampUs, gyroStampUs); + return; + } + + double accStamp = double(accelStampUs)/1e6; + + if(accelStampUs <= lastAccStamp_) { + return; + } + + lastAccStamp_ = accelStampUs; + + auto accelValue = accel->getValue(); + auto gyroValue = gyro->getValue(); + if(isInterIMUPublishing()) + { + IMU imu(cv::Vec3f(gyroValue.x, gyroValue.y, gyroValue.z), cv::Mat::eye(3,3,CV_64FC1), + cv::Vec3f(accelValue.x, accelValue.y, accelValue.z), cv::Mat::eye(3,3,CV_64FC1), + imuLocalTransform_); + this->postInterIMU(imu, accStamp); + } + else + { + UScopeMutex lock(imuMutex_); + imuBuffer_.emplace_hint(imuBuffer_.end(), accStamp, cv::Vec6f(gyroValue.x, gyroValue.y, gyroValue.z, accelValue.x, accelValue.y, accelValue.z)); + if(imuBuffer_.size()>1000) { + imuBuffer_.erase(imuBuffer_.begin()); + } + } + }); + } + catch(const ob::Error & e) { + UERROR("Unexpected error when configuring IMU stream: %s", e.what()); + } + } + else + { + UWARN("IMU option is enabled but the camera doesn't have an IMU, ignoring."); + } + } + + pipeline_ = new ob::Pipeline(device); + auto config = std::make_shared(); + + // Set highest frame rate possible to reduce color/depth sync diff + config->enableVideoStream(OB_STREAM_COLOR, colorWidth_, colorHeight_, maxColorFps, OB_FORMAT_RGB); + config->enableVideoStream(OB_STREAM_DEPTH, depthWidth_, depthHeight_, maxDepthFps, OB_FORMAT_Y16); + + UINFO("Using color profile: %dx%d", colorWidth_, colorHeight_); + UINFO("Using depth profile: %dx%d", depthWidth_, depthHeight_); + + config->setFrameAggregateOutputMode(OB_FRAME_AGGREGATE_OUTPUT_ALL_TYPE_FRAME_REQUIRE); + + config->setAlignMode(ALIGN_DISABLE); + config->setDepthScaleRequire(true); + + pipeline_->enableFrameSync(); + + try { + UINFO("Starting camera pipeline"); + pipeline_->start(config); + + auto enabledStreams = pipeline_->getConfig()->getEnabledStreamProfileList(); + if(imuPipeline_ != nullptr) { + for(uint32_t i=0; igetCount() && !imuLocalTransformInitialized_; ++i) + { + if(enabledStreams->getProfile(i)->getType() == OB_STREAM_COLOR) + { + auto enabledImuStreams = imuPipeline_->getConfig()->getEnabledStreamProfileList(); + for(uint32_t j=0; jgetCount(); ++j) + { + if(enabledImuStreams->getProfile(j)->getType() == OB_STREAM_ACCEL) + { + auto extrinsics = enabledStreams->getProfile(i)->as()->getExtrinsicTo(enabledImuStreams->getProfile(j)->as()); + // base -> color -> imu + imuLocalTransform_ = this->getLocalTransform() * obToRtabmap(extrinsics); + UINFO("IMU local transform: %s", imuLocalTransform_.prettyPrint().c_str()); + imuLocalTransformInitialized_ = true; + break; + } + } + } + } + } + + std::shared_ptr colorProfile; + std::shared_ptr depthProfile; + for(uint32_t i=0; igetCount(); ++i) + { + if(enabledStreams->getProfile(i)->getType() == OB_STREAM_COLOR) { + colorProfile = enabledStreams->getProfile(i); + } + else if(enabledStreams->getProfile(i)->getType() == OB_STREAM_DEPTH) { + depthProfile = enabledStreams->getProfile(i); + } + } + bool currentSelectionSupportsHwD2C = false; + auto hwD2CSupportedDepthStreamProfiles = pipeline_->getD2CDepthProfileList(colorProfile, ALIGN_D2C_HW_MODE); + if(hwD2CSupportedDepthStreamProfiles->count() == 0) { + UWARN("Current color profile selected doesn't support any hardware depth to color registration. Software registration is done instead."); + } + else + { + auto depthVsp = depthProfile->as(); + auto count = hwD2CSupportedDepthStreamProfiles->getCount(); + for(uint32_t i = 0; i < count; i++) { + auto vsp = hwD2CSupportedDepthStreamProfiles->getProfile(i)->as(); + UINFO("Supported depth to color format: Resolution: %ldx%ld, FPS: %ld, Format: %d", vsp->getWidth(), vsp->getHeight(), vsp->getFps(), vsp->getFormat(), i==0?" (default)":""); + if(vsp->getWidth() == depthVsp->getWidth() && vsp->getHeight() == depthVsp->getHeight() && vsp->getFormat() == depthVsp->getFormat() + && vsp->getFps() == depthVsp->getFps()) { + currentSelectionSupportsHwD2C = true; + } + } + } + if(!currentSelectionSupportsHwD2C) { + UWARN("Hardware depth to color registration cannot be done with the selected color and depth profiles. " + "Software registration is done instead, so more CPU will be needed on the host computer. " + "Set logger level to info to see comptible depth formats for the selected color profile."); + alignFilter_ = new ob::Align(OB_STREAM_COLOR); + alignFilter_->setMatchTargetResolution(false); + } + else { + UINFO("Enabling hardware depth to color registration!"); + config->setAlignMode(ALIGN_D2C_HW_MODE); + config->setDepthScaleRequire(false); + pipeline_->stop(); + pipeline_->start(config); + } + } + catch(const ob::Error & e) + { + UERROR("Configuration not supported! Exception: %s", e.what()); + UERROR("Supported formats:"); + for(uint32_t i=0; igetCount(); ++i) + { + if( sensors->getSensorType(i) == OB_SENSOR_DEPTH || + sensors->getSensorType(i) == OB_SENSOR_COLOR) + { + auto profiles = sensors->getSensor(i)->getStreamProfileList(); + for(uint32_t j=0; jgetCount(); ++j) + { + auto profile = profiles->getProfile(j)->as(); + UERROR("%sResolution: %ldx%ld, FPS: %ld, Format: %d", + sensors->getSensorType(i) == OB_SENSOR_DEPTH?"Depth":"Color", + profile->getWidth(), + profile->getHeight(), + profile->getFps(), + profile->getFormat(), + j==0?" (default)":""); + } + } + } + return false; + } + + return true; +#else + UERROR("CameraOrbbecSDK: RTAB-Map is not built with Orbbec SDK support!"); + return false; +#endif +} + +bool CameraOrbbecSDK::isCalibrated() const +{ + return true; +} + +std::string CameraOrbbecSDK::getSerial() const +{ +#ifdef RTABMAP_ORBBEC_SDK + if(pipeline_) { + return pipeline_->getDevice()->getDeviceInfo()->getSerialNumber(); + } +#endif + return ""; +} + +void CameraOrbbecSDK::enableColorRectification(bool enabled) +{ +#ifdef RTABMAP_ORBBEC_SDK + rectifyColor_ = enabled; +#endif +} + +void CameraOrbbecSDK::enableImu(bool enabled) +{ +#ifdef RTABMAP_ORBBEC_SDK + imuPublished_ = enabled; +#endif +} + +void CameraOrbbecSDK::enableDepthMM(bool enabled) +{ +#ifdef RTABMAP_ORBBEC_SDK + convertDepthToMM_ = enabled; +#endif +} + +SensorData CameraOrbbecSDK::captureImage(SensorCaptureInfo * info) +{ + SensorData data; + +#ifdef RTABMAP_ORBBEC_SDK + if(!pipeline_) { + UERROR("Camera is not initialized!"); + return data; + } + auto frameset = pipeline_->waitForFrameset(); + if(frameset == nullptr || frameset->getCount() == 0) { + UWARN("No frame received!"); + return data; + } + if(frameset->getCount() != 2) { + UWARN("Received %s frames, expecting 2!", frameset->getCount()); + return data; + } + + if(alignFilter_ != nullptr) { + // Software depth to color registration + frameset = alignFilter_->process(frameset)->as(); + UASSERT(frameset != nullptr); + } + + auto colorFrame = frameset->getFrame(OB_FRAME_COLOR); + UASSERT(colorFrame != nullptr); + + auto depthFrame = frameset->getFrame(OB_FRAME_DEPTH); + UASSERT(depthFrame != nullptr); + + auto colorVideoFrame = colorFrame->as(); + auto depthVideoFrame = depthFrame->as(); + + cv::Mat rgb = obColorFrameToCv(*colorVideoFrame); + cv::Mat depth = obDepthFrameToCv(*depthVideoFrame); + + if(rgb.empty()) { + UERROR("Could not convert the color frame! Type=%d Format=%d", colorFrame->getType(), colorVideoFrame->getFormat()); + } + else if(depth.empty()) { + UERROR("Could not convert the depth frame! Type=%d Format=%d", depthFrame->getType(), depthVideoFrame->getFormat()); + } + else if(!rgb.empty() && !depth.empty()) + { + if(!model_.isValidForProjection()) + { + auto streamProfile = colorFrame->getStreamProfile(); + auto videoStreamProfile = streamProfile->as(); + + auto intrinsics = videoStreamProfile->getIntrinsic(); + model_ = CameraModel( + getSerial(), + cv::Size(intrinsics.width, intrinsics.height), + obIntrinsicToK(intrinsics), + obDistortionToD(videoStreamProfile->getDistortion()), + cv::Mat::eye(3,3,CV_64FC1), + obIntrinsicToP(intrinsics), + this->getLocalTransform()); + if(rectifyColor_ && !model_.initRectificationMap()) { + UWARN("Could not initialize rectification map, color images won't be rectified."); + } + } + + if(rectifyColor_ && model_.isValidForRectification()) + { + rgb = model_.rectifyImage(rgb); + } + + if(convertDepthToMM_) + { + depth = util2d::cvtDepthFromFloat(depth); + } + + uint64_t colorStampUs = globalTimestampAvailable_?colorFrame->getGlobalTimeStampUs():colorFrame->getTimeStampUs(); + uint64_t depthStampUs = globalTimestampAvailable_?depthFrame->getGlobalTimeStampUs():depthFrame->getTimeStampUs(); + double colorStamp = double(colorStampUs) / 1e6; + double depthStamp = double(depthStampUs) / 1e6; + if(fabs(colorStamp - depthStamp) > 0.018) { + // The difference seems varying between 0 and 17 ms normally + UWARN("Large timestamp difference (%fs) between color (%f) and depth (%f) frames. " + "Depth registration would be wrong on fast motion.", + colorStamp - depthStamp, colorStamp, depthStamp); + } + + uint64_t stampUs = colorStampUs < depthStampUs ? colorStampUs : depthStampUs; + +#ifdef WIN32 + // On Windows, there is an issue that timestamps are not populated by default without following instructions from: + // https://github.com/orbbec/OrbbecSDK_v2/blob/main/scripts/env_setup/obsensor_metadata_win10.md + // Detect if the consecutive timestamps are identical, then send error! + if (stampUs <= lastImageStamp_) + { + UERROR("We detected non-consecutive timestamps, make sure you applied the fix from https://github.com/orbbec/OrbbecSDK_v2/blob/main/scripts/env_setup/obsensor_metadata_win10.md ."); + } + lastImageStamp_ = stampUs; +#endif + double stamp = double(stampUs)/1e6; + + data = SensorData(rgb, depth, model_, this->getNextSeqID(), stamp); + + if(imuPublished_ && !imuBuffer_.empty() && !this->isInterIMUPublishing()) + { + cv::Vec6f imuVec; + std::map::const_iterator iterA, iterB; + + imuMutex_.lock(); + int maximumTries = 10; + while(imuBuffer_.rbegin()->first < stamp && maximumTries-- > 0) + { + imuMutex_.unlock(); + uSleep(1); + imuMutex_.lock(); + } + + if(imuBuffer_.rbegin()->first < stamp) + { + UWARN("Could not get IMU data at request image stamp %f after waiting 10 ms, latest imu stamp is %f", stamp, imuBuffer_.rbegin()->first); + imuMutex_.unlock(); + } + else + { + // Interpolate imu data on image stamp + iterB = imuBuffer_.lower_bound(stamp); + iterA = iterB; + if(iterA != imuBuffer_.begin()) + iterA = --iterA; + if(iterA == iterB || stamp == iterB->first) + { + imuVec = iterB->second; + } + else if(stamp > iterA->first && stamp < iterB->first) + { + float t = (stamp-iterA->first) / (iterB->first-iterA->first); + imuVec = iterA->second + t*(iterB->second - iterA->second); + } + imuBuffer_.erase(imuBuffer_.begin(), iterB); + + imuMutex_.unlock(); + data.setIMU(IMU(cv::Vec3d(imuVec[0], imuVec[1], imuVec[2]), cv::Mat::eye(3, 3, CV_64FC1), cv::Vec3d(imuVec[3], imuVec[4], imuVec[5]), cv::Mat::eye(3, 3, CV_64FC1), imuLocalTransform_)); + } + } + } + +#else + UERROR("CameraOrbbecSDK: RTAB-Map is not built with Orbbec SDK support!"); +#endif + return data; +} + +} // namespace rtabmap diff --git a/examples/RGBDMapping/main.cpp b/examples/RGBDMapping/main.cpp index 5f2b75fd..94545e1e 100644 --- a/examples/RGBDMapping/main.cpp +++ b/examples/RGBDMapping/main.cpp @@ -50,7 +50,24 @@ void showUsage() { printf("\nUsage:\n" "rtabmap-rgbd_mapping driver\n" - " driver Driver number to use: 0=OpenNI-PCL, 1=OpenNI2, 2=Freenect, 3=OpenNI-CV, 4=OpenNI-CV-ASUS, 5=Freenect2, 6=ZED SDK, 7=RealSense, 8=RealSense2 9=Kinect for Azure SDK 10=MYNT EYE S\n\n"); + " driver Driver number to use: 0=OpenNI-PCL (Kinect)\n" + " 1=OpenNI2 (Kinect and Xtion PRO Live)\n" + " 2=Freenect (Kinect)\n" + " 3=OpenNI-CV (Kinect)\n" + " 4=OpenNI-CV-ASUS (Xtion PRO Live)\n" + " 5=Freenect2 (Kinect v2)\n" + " 6=DC1394 (Bumblebee2)\n" + " 7=FlyCapture2 (Bumblebee2)\n" + " 8=ZED stereo\n" + " 9=RealSense\n" + " 10=Kinect for Windows 2 SDK\n" + " 11=RealSense2\n" + " 12=Kinect for Azure SDK\n" + " 13=MYNT EYE S\n" + " 14=ZED Open Capture\n" + " 15=depthai-core\n" + " 16=XVSDK (SeerSense)\n" + " 17=Orbbec SDK\n\n"); exit(1); } @@ -58,7 +75,7 @@ using namespace rtabmap; int main(int argc, char * argv[]) { ULogger::setType(ULogger::kTypeConsole); - ULogger::setLevel(ULogger::kWarning); + ULogger::setLevel(ULogger::kInfo); #ifdef RTABMAP_PYTHON PythonInterface python; // Make sure we initialize python in main thread @@ -72,92 +89,120 @@ int main(int argc, char * argv[]) else { driver = atoi(argv[argc-1]); - if(driver < 0 || driver > 10) + if(driver < 0 || driver > 17) { - UERROR("driver should be between 0 and 10."); + UERROR("driver should be between 0 and 17."); showUsage(); } } // Here is the pipeline that we will use: // CameraOpenni -> "SensorEvent" -> OdometryThread -> "OdometryEvent" -> RtabmapThread -> "RtabmapEvent" - - // Create the OpenNI camera, it will send a SensorEvent at the rate specified. - // Set transform to camera so z is up, y is left and x going forward Camera * camera = 0; - if(driver == 1) + if (driver == 0) { - if(!CameraOpenNI2::available()) + camera = new rtabmap::CameraOpenni(); + } + else if (driver == 1) + { + if (!rtabmap::CameraOpenNI2::available()) { UERROR("Not built with OpenNI2 support..."); exit(-1); } - camera = new CameraOpenNI2(); + camera = new rtabmap::CameraOpenNI2(); } - else if(driver == 2) + else if (driver == 2) { - if(!CameraFreenect::available()) + if (!rtabmap::CameraFreenect::available()) { UERROR("Not built with Freenect support..."); exit(-1); } - camera = new CameraFreenect(); + camera = new rtabmap::CameraFreenect(); } - else if(driver == 3) + else if (driver == 3) { - if(!CameraOpenNICV::available()) + if (!rtabmap::CameraOpenNICV::available()) { UERROR("Not built with OpenNI from OpenCV support..."); exit(-1); } - camera = new CameraOpenNICV(); + camera = new rtabmap::CameraOpenNICV(false); } - else if(driver == 4) + else if (driver == 4) { - if(!CameraOpenNICV::available()) + if (!rtabmap::CameraOpenNICV::available()) { UERROR("Not built with OpenNI from OpenCV support..."); exit(-1); } - camera = new CameraOpenNICV(true); + camera = new rtabmap::CameraOpenNICV(true); } else if (driver == 5) { - if (!CameraFreenect2::available()) + if (!rtabmap::CameraFreenect2::available()) { UERROR("Not built with Freenect2 support..."); exit(-1); } - camera = new CameraFreenect2(0, CameraFreenect2::kTypeColor2DepthSD); + camera = new rtabmap::CameraFreenect2(0, rtabmap::CameraFreenect2::kTypeColor2DepthSD); } else if (driver == 6) { - if (!CameraStereoZed::available()) + if (!rtabmap::CameraStereoDC1394::available()) { - UERROR("Not built with ZED SDK support..."); + UERROR("Not built with DC1394 support..."); exit(-1); } - camera = new CameraStereoZed(0, -1, 1, 1, 100, false); + camera = new rtabmap::CameraStereoDC1394(); } else if (driver == 7) { - if (!CameraRealSense::available()) + if (!rtabmap::CameraStereoFlyCapture2::available()) + { + UERROR("Not built with FlyCapture2/Triclops support..."); + exit(-1); + } + camera = new rtabmap::CameraStereoFlyCapture2(); + } + else if (driver == 8) + { + if (!rtabmap::CameraStereoZed::available()) + { + UERROR("Not built with ZED sdk support..."); + exit(-1); + } + camera = new rtabmap::CameraStereoZed(0); + } + else if (driver == 9) + { + if (!rtabmap::CameraRealSense::available()) { UERROR("Not built with RealSense support..."); exit(-1); } - camera = new CameraRealSense(); + camera = new rtabmap::CameraRealSense(); } - else if (driver == 8) + else if (driver == 10) { - if (!CameraRealSense2::available()) + if (!rtabmap::CameraK4W2::available()) { - UERROR("Not built with RealSense2 support..."); + UERROR("Not built with Kinect for Windows 2 SDK support..."); exit(-1); } - camera = new CameraRealSense2(); + camera = new rtabmap::CameraK4W2(); } - else if (driver == 9) + else if (driver == 11) + { + if (!rtabmap::CameraRealSense2::available()) + { + UERROR("Not built with RealSense2 SDK support..."); + exit(-1); + } + camera = new rtabmap::CameraRealSense2(); + } + else if (driver == 12) { if (!rtabmap::CameraK4A::available()) { @@ -166,7 +211,7 @@ int main(int argc, char * argv[]) } camera = new rtabmap::CameraK4A(1); } - else if (driver == 10) + else if (driver == 13) { if (!rtabmap::CameraMyntEye::available()) { @@ -175,9 +220,45 @@ int main(int argc, char * argv[]) } camera = new rtabmap::CameraMyntEye(); } + else if (driver == 14) + { + if (!rtabmap::CameraStereoZedOC::available()) + { + UERROR("Not built with Zed Open Capture support..."); + exit(-1); + } + camera = new rtabmap::CameraStereoZedOC(0); + } + else if (driver == 15) + { + if (!rtabmap::CameraDepthAI::available()) + { + UERROR("Not built with depthai-core support..."); + exit(-1); + } + camera = new rtabmap::CameraDepthAI(); + } + else if (driver == 16) + { + if (!rtabmap::CameraSeerSense::available()) + { + UERROR("Not built with XVisio SDK support..."); + exit(-1); + } + camera = new rtabmap::CameraSeerSense(); + } + else if (driver == 17) + { + if (!rtabmap::CameraOrbbecSDK::available()) + { + UERROR("Not built with Orbbec SDK support..."); + exit(-1); + } + camera = new rtabmap::CameraOrbbecSDK(); + } else { - camera = new rtabmap::CameraOpenni(); + UFATAL(""); } if(!camera->init()) diff --git a/guilib/include/rtabmap/gui/MainWindow.h b/guilib/include/rtabmap/gui/MainWindow.h index c0badb39..2f8b14b5 100644 --- a/guilib/include/rtabmap/gui/MainWindow.h +++ b/guilib/include/rtabmap/gui/MainWindow.h @@ -178,6 +178,7 @@ protected Q_SLOTS: void selectFreenect2(); void selectK4W2(); void selectK4A(); + void selectOrbbecSDK(); void selectRealSense(); void selectRealSense2(); void selectRealSense2L515(); diff --git a/guilib/include/rtabmap/gui/PreferencesDialog.h b/guilib/include/rtabmap/gui/PreferencesDialog.h index 72f019e3..8678b846 100644 --- a/guilib/include/rtabmap/gui/PreferencesDialog.h +++ b/guilib/include/rtabmap/gui/PreferencesDialog.h @@ -98,6 +98,7 @@ public: kSrcRealSense2 = 9, kSrcK4A = 10, kSrcSeerSense = 11, + kSrcOrbbecSDK = 12, kSrcStereo = 100, kSrcDC1394 = 100, diff --git a/guilib/src/AboutDialog.cpp b/guilib/src/AboutDialog.cpp index ab96c410..439f85fe 100644 --- a/guilib/src/AboutDialog.cpp +++ b/guilib/src/AboutDialog.cpp @@ -179,6 +179,8 @@ AboutDialog::AboutDialog(QWidget * parent) : _ui->label_depthai->setText(CameraDepthAI::available() ? "Yes" : "No"); _ui->label_depthai_license->setEnabled(CameraDepthAI::available()); _ui->label_xvsdk->setText(CameraSeerSense::available() ? "Yes" : "No"); + _ui->label_orbbec_sdk->setText(CameraOrbbecSDK::available() ? "Yes" : "No"); + _ui->label_orbbec_sdk_license->setEnabled(CameraOrbbecSDK::available()); _ui->label_toro->setText(Optimizer::isAvailable(Optimizer::kTypeTORO)?"Yes":"No"); _ui->label_toro_license->setEnabled(Optimizer::isAvailable(Optimizer::kTypeTORO)?true:false); diff --git a/guilib/src/GuiLib.qrc b/guilib/src/GuiLib.qrc index 5880443f..710b690b 100644 --- a/guilib/src/GuiLib.qrc +++ b/guilib/src/GuiLib.qrc @@ -44,6 +44,7 @@ images/oakd.png images/oakd_lite.png images/astra.png + images/astra2.png images/oakdpro.png images/seer_sense_DS80.png diff --git a/guilib/src/ImageView.cpp b/guilib/src/ImageView.cpp index ccf4b14d..5ab33397 100644 --- a/guilib/src/ImageView.cpp +++ b/guilib/src/ImageView.cpp @@ -1141,14 +1141,14 @@ void ImageView::mouseMoveEvent(QMouseEvent * event) if(_mouseTracking->isChecked() && !_graphicsView->scene()->sceneRect().isNull() && !_image.isNull() && - !_imageDepthCv.empty() &&(_imageDepthCv.type() == CV_16UC1 || _imageDepthCv.type() == CV_32FC1)) + !_imageDepthCv.empty() && (_imageDepthCv.type() == CV_16UC1 || _imageDepthCv.type() == CV_32FC1)) { float scale, offsetX, offsetY; computeScaleOffsets(this->rect(), scale, offsetX, offsetY); float u = (event->pos().x() - offsetX) / scale; float v = (event->pos().y() - offsetY) / scale; float depthScale = 1; - if(_image.width() > _imageDepthCv.cols) + if(_image.width() != _imageDepthCv.cols) { depthScale = float(_imageDepthCv.cols) / float(_image.width()); } @@ -1448,8 +1448,8 @@ void ImageView::setImageDepth(const QImage & imageDepth, const QImage & imageDep UASSERT(_imageDepth.width() && _imageDepth.height()); if( _image.width() > 0 && - _image.width() > _imageDepth.width() && - _image.height() > _imageDepth.height()) + _image.width() != _imageDepth.width() && + _image.height() != _imageDepth.height()) { // scale depth to rgb _imageDepth = _imageDepth.scaled(_image.size()); diff --git a/guilib/src/MainWindow.cpp b/guilib/src/MainWindow.cpp index 9ddcf68e..5e3c7e66 100644 --- a/guilib/src/MainWindow.cpp +++ b/guilib/src/MainWindow.cpp @@ -470,6 +470,7 @@ MainWindow::MainWindow(PreferencesDialog * prefDialog, QWidget * parent, bool sh connect(_ui->actionDepthAI_oakdlite, SIGNAL(triggered()), this, SLOT(selectDepthAIOAKDLite())); connect(_ui->actionDepthAI_oakdpro, SIGNAL(triggered()), this, SLOT(selectDepthAIOAKDPro())); connect(_ui->actionXvisio_SeerSense, SIGNAL(triggered()), this, SLOT(selectXvisioSeerSense())); + connect(_ui->actionOrbbecSDK_astra2, SIGNAL(triggered()), this, SLOT(selectOrbbecSDK())); connect(_ui->actionVelodyne_VLP_16, SIGNAL(triggered()), this, SLOT(selectVLP16())); _ui->actionFreenect->setEnabled(CameraFreenect::available()); _ui->actionOpenNI_PCL->setEnabled(CameraOpenni::available()); @@ -499,6 +500,7 @@ MainWindow::MainWindow(PreferencesDialog * prefDialog, QWidget * parent, bool sh _ui->actionDepthAI_oakdlite->setEnabled(CameraDepthAI::available()); _ui->actionDepthAI_oakdpro->setEnabled(CameraDepthAI::available()); _ui->actionXvisio_SeerSense->setEnabled(CameraSeerSense::available()); + _ui->actionOrbbecSDK_astra2->setEnabled(CameraOrbbecSDK::available()); this->updateSelectSourceMenu(); connect(_ui->actionPreferences, SIGNAL(triggered()), this, SLOT(openPreferences())); @@ -5337,6 +5339,7 @@ void MainWindow::updateSelectSourceMenu() _ui->actionDepthAI_oakdlite->setChecked(_preferencesDialog->getSourceDriver() == PreferencesDialog::kSrcStereoDepthAI); _ui->actionDepthAI_oakdpro->setChecked(_preferencesDialog->getSourceDriver() == PreferencesDialog::kSrcStereoDepthAI); _ui->actionXvisio_SeerSense->setChecked(_preferencesDialog->getSourceDriver() == PreferencesDialog::kSrcSeerSense); + _ui->actionOrbbecSDK_astra2->setChecked(_preferencesDialog->getSourceDriver() == PreferencesDialog::kSrcOrbbecSDK); _ui->actionVelodyne_VLP_16->setChecked(_preferencesDialog->getLidarSourceDriver() == PreferencesDialog::kSrcLidarVLP16); } @@ -7213,6 +7216,11 @@ void MainWindow::selectK4A() _preferencesDialog->selectSourceDriver(PreferencesDialog::kSrcK4A); } +void MainWindow::selectOrbbecSDK() +{ + _preferencesDialog->selectSourceDriver(PreferencesDialog::kSrcOrbbecSDK); +} + void MainWindow::selectRealSense() { _preferencesDialog->selectSourceDriver(PreferencesDialog::kSrcRealSense); diff --git a/guilib/src/PreferencesDialog.cpp b/guilib/src/PreferencesDialog.cpp index 898a3904..e4581e93 100644 --- a/guilib/src/PreferencesDialog.cpp +++ b/guilib/src/PreferencesDialog.cpp @@ -398,6 +398,10 @@ PreferencesDialog::PreferencesDialog(QWidget * parent) : { _ui->comboBox_cameraRGBD->setItemData(kSrcK4A - kSrcRGBD, 0, Qt::UserRole - 1); } + if (!CameraOrbbecSDK::available()) + { + _ui->comboBox_cameraRGBD->setItemData(kSrcOrbbecSDK - kSrcRGBD, 0, Qt::UserRole - 1); + } if (!CameraRealSense::available()) { _ui->comboBox_cameraRGBD->setItemData(kSrcRealSense - kSrcRGBD, 0, Qt::UserRole - 1); @@ -767,19 +771,39 @@ PreferencesDialog::PreferencesDialog(QWidget * parent) : connect(_ui->openni2_hshift, SIGNAL(valueChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->openni2_vshift, SIGNAL(valueChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->openni2_depth_decimation, SIGNAL(valueChanged(int)), this, SLOT(makeObsoleteSourcePanel())); + connect(_ui->comboBox_freenect2Format, SIGNAL(currentIndexChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->doubleSpinBox_freenect2MinDepth, SIGNAL(valueChanged(double)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->doubleSpinBox_freenect2MaxDepth, SIGNAL(valueChanged(double)), this, SLOT(makeObsoleteSourcePanel())); + connect(_ui->checkBox_freenect2BilateralFiltering, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->checkBox_freenect2EdgeAwareFiltering, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->checkBox_freenect2NoiseFiltering, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->lineEdit_freenect2Pipeline, SIGNAL(textChanged(const QString &)), this, SLOT(makeObsoleteSourcePanel())); + connect(_ui->comboBox_k4w2Format, SIGNAL(currentIndexChanged(int)), this, SLOT(makeObsoleteSourcePanel())); + + connect(_ui->comboBox_k4a_rgb_resolution, SIGNAL(currentIndexChanged(int)), this, SLOT(makeObsoleteSourcePanel())); + connect(_ui->comboBox_k4a_framerate, SIGNAL(currentIndexChanged(int)), this, SLOT(makeObsoleteSourcePanel())); + connect(_ui->comboBox_k4a_depth_resolution, SIGNAL(currentIndexChanged(int)), this, SLOT(makeObsoleteSourcePanel())); + connect(_ui->checkbox_k4a_irDepth, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); + connect(_ui->lineEdit_k4a_mkv, SIGNAL(textChanged(const QString &)), this, SLOT(makeObsoleteSourcePanel())); + connect(_ui->source_checkBox_useMKVStamps, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); + + connect(_ui->spinBox_orbbec_sdk_color_width, SIGNAL(valueChanged(int)), this, SLOT(makeObsoleteSourcePanel())); + connect(_ui->spinBox_orbbec_sdk_color_height, SIGNAL(valueChanged(int)), this, SLOT(makeObsoleteSourcePanel())); + connect(_ui->spinBox_orbbec_sdk_depth_width, SIGNAL(valueChanged(int)), this, SLOT(makeObsoleteSourcePanel())); + connect(_ui->spinBox_orbbec_sdk_depth_height, SIGNAL(valueChanged(int)), this, SLOT(makeObsoleteSourcePanel())); + connect(_ui->checkBox_orbbec_sdk_color_rectification, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); + connect(_ui->checkBox_orbbec_sdk_imu, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); + connect(_ui->checkBox_orbbec_sdk_depth_mm, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); + connect(_ui->comboBox_realsensePresetRGB, SIGNAL(currentIndexChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->comboBox_realsensePresetDepth, SIGNAL(currentIndexChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->checkbox_realsenseOdom, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->checkbox_realsenseDepthScaledToRGBSize, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->comboBox_realsenseRGBSource, SIGNAL(currentIndexChanged(int)), this, SLOT(makeObsoleteSourcePanel())); + connect(_ui->checkbox_rs2_emitter, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->checkbox_rs2_irMode, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->checkbox_rs2_irDepth, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); @@ -2232,6 +2256,13 @@ void PreferencesDialog::resetSettings(QGroupBox * groupBox) _ui->comboBox_k4a_depth_resolution->setCurrentIndex(2); _ui->checkbox_k4a_irDepth->setChecked(false); _ui->lineEdit_k4a_mkv->clear(); + _ui->spinBox_orbbec_sdk_color_width->setValue(800); + _ui->spinBox_orbbec_sdk_color_height->setValue(600); + _ui->spinBox_orbbec_sdk_depth_width->setValue(800); + _ui->spinBox_orbbec_sdk_depth_height->setValue(600); + _ui->checkBox_orbbec_sdk_color_rectification->setChecked(false); + _ui->checkBox_orbbec_sdk_imu->setChecked(true); + _ui->checkBox_orbbec_sdk_depth_mm->setChecked(true); _ui->source_checkBox_useMKVStamps->setChecked(true); _ui->lineEdit_cameraRGBDImages_path_rgb->setText(""); _ui->lineEdit_cameraRGBDImages_path_depth->setText(""); @@ -2718,6 +2749,16 @@ void PreferencesDialog::readCameraSettings(const QString & filePath) _ui->source_checkBox_useMKVStamps->setChecked(settings.value("useMkvStamps", _ui->source_checkBox_useMKVStamps->isChecked()).toBool()); settings.endGroup(); // K4A + settings.beginGroup("OrbbecSDK"); + _ui->spinBox_orbbec_sdk_color_width->setValue(settings.value("color_width", _ui->spinBox_orbbec_sdk_color_width->value()).toInt()); + _ui->spinBox_orbbec_sdk_color_height->setValue(settings.value("color_height", _ui->spinBox_orbbec_sdk_color_height->value()).toInt()); + _ui->spinBox_orbbec_sdk_depth_width->setValue(settings.value("depth_width", _ui->spinBox_orbbec_sdk_depth_width->value()).toInt()); + _ui->spinBox_orbbec_sdk_depth_height->setValue(settings.value("depth_height", _ui->spinBox_orbbec_sdk_depth_height->value()).toInt()); + _ui->checkBox_orbbec_sdk_color_rectification->setChecked(settings.value("rectify_color", _ui->checkBox_orbbec_sdk_color_rectification->isChecked()).toBool()); + _ui->checkBox_orbbec_sdk_imu->setChecked(settings.value("enable_imu", _ui->checkBox_orbbec_sdk_imu->isChecked()).toBool()); + _ui->checkBox_orbbec_sdk_depth_mm->setChecked(settings.value("depth_mm", _ui->checkBox_orbbec_sdk_depth_mm->isChecked()).toBool()); + settings.endGroup(); // Orbbec SDK + settings.beginGroup("RealSense"); _ui->comboBox_realsensePresetRGB->setCurrentIndex(settings.value("presetRGB", _ui->comboBox_realsensePresetRGB->currentIndex()).toInt()); _ui->comboBox_realsensePresetDepth->setCurrentIndex(settings.value("presetDepth", _ui->comboBox_realsensePresetDepth->currentIndex()).toInt()); @@ -3325,6 +3366,16 @@ void PreferencesDialog::writeCameraSettings(const QString & filePath) const settings.setValue("useMkvStamps", _ui->source_checkBox_useMKVStamps->isChecked()); settings.endGroup(); // K4A + settings.beginGroup("OrbbecSDK"); + settings.setValue("color_width", _ui->spinBox_orbbec_sdk_color_width->value()); + settings.setValue("color_height", _ui->spinBox_orbbec_sdk_color_height->value()); + settings.setValue("depth_width", _ui->spinBox_orbbec_sdk_depth_width->value()); + settings.setValue("depth_height", _ui->spinBox_orbbec_sdk_depth_height->value()); + settings.setValue("rectify_color", _ui->checkBox_orbbec_sdk_color_rectification->isChecked()); + settings.setValue("enable_imu", _ui->checkBox_orbbec_sdk_imu->isChecked()); + settings.setValue("depth_mm", _ui->checkBox_orbbec_sdk_depth_mm->isChecked()); + settings.endGroup(); // Orbbec SDK + settings.beginGroup("RealSense"); settings.setValue("presetRGB", _ui->comboBox_realsensePresetRGB->currentIndex()); settings.setValue("presetDepth", _ui->comboBox_realsensePresetDepth->currentIndex()); @@ -5729,6 +5780,7 @@ void PreferencesDialog::updateSourceGrpVisibility() _ui->comboBox_cameraRGBD->currentIndex() == kSrcFreenect2-kSrcRGBD || _ui->comboBox_cameraRGBD->currentIndex() == kSrcK4W2 - kSrcRGBD || _ui->comboBox_cameraRGBD->currentIndex() == kSrcK4A - kSrcRGBD || + _ui->comboBox_cameraRGBD->currentIndex() == kSrcOrbbecSDK - kSrcRGBD || _ui->comboBox_cameraRGBD->currentIndex() == kSrcRealSense - kSrcRGBD || _ui->comboBox_cameraRGBD->currentIndex() == kSrcRGBDImages-kSrcRGBD || _ui->comboBox_cameraRGBD->currentIndex() == kSrcOpenNI_PCL-kSrcRGBD || @@ -5738,6 +5790,7 @@ void PreferencesDialog::updateSourceGrpVisibility() _ui->groupBox_freenect2->setVisible(_ui->comboBox_sourceType->currentIndex() == 0 && _ui->comboBox_cameraRGBD->currentIndex() == kSrcFreenect2-kSrcRGBD); _ui->groupBox_k4w2->setVisible(_ui->comboBox_sourceType->currentIndex() == 0 && _ui->comboBox_cameraRGBD->currentIndex() == kSrcK4W2 - kSrcRGBD); _ui->groupBox_k4a->setVisible(_ui->comboBox_sourceType->currentIndex() == 0 && _ui->comboBox_cameraRGBD->currentIndex() == kSrcK4A - kSrcRGBD); + _ui->groupBox_orbbec_sdk->setVisible(_ui->comboBox_sourceType->currentIndex() == 0 && _ui->comboBox_cameraRGBD->currentIndex() == kSrcOrbbecSDK - kSrcRGBD); _ui->groupBox_realsense->setVisible(_ui->comboBox_sourceType->currentIndex() == 0 && _ui->comboBox_cameraRGBD->currentIndex() == kSrcRealSense - kSrcRGBD); _ui->groupBox_realsense2->setVisible(_ui->comboBox_sourceType->currentIndex() == 0 && _ui->comboBox_cameraRGBD->currentIndex() == kSrcRealSense2 - kSrcRGBD); _ui->groupBox_cameraRGBDImages->setVisible(_ui->comboBox_sourceType->currentIndex() == 0 && _ui->comboBox_cameraRGBD->currentIndex() == kSrcRGBDImages-kSrcRGBD); @@ -5807,6 +5860,7 @@ void PreferencesDialog::updateSourceGrpVisibility() (_ui->comboBox_sourceType->currentIndex() == 2 && _ui->source_comboBox_image_type->currentIndex() == kSrcImages-kSrcRGB) || (_ui->comboBox_sourceType->currentIndex() == 0 && _ui->comboBox_cameraRGBD->currentIndex() == kSrcFreenect - kSrcRGBD) || //Kinect360 (_ui->comboBox_sourceType->currentIndex() == 0 && _ui->comboBox_cameraRGBD->currentIndex() == kSrcK4A - kSrcRGBD) || //K4A + (_ui->comboBox_sourceType->currentIndex() == 0 && _ui->comboBox_cameraRGBD->currentIndex() == kSrcOrbbecSDK - kSrcRGBD) || //Orbbec SDK (_ui->comboBox_sourceType->currentIndex() == 0 && _ui->comboBox_cameraRGBD->currentIndex() == kSrcRealSense2 - kSrcRGBD) || //D435i (_ui->comboBox_sourceType->currentIndex() == 0 && _ui->comboBox_cameraRGBD->currentIndex() == kSrcSeerSense - kSrcRGBD) || (_ui->comboBox_sourceType->currentIndex() == 1 && _ui->comboBox_cameraStereo->currentIndex() == kSrcStereoRealSense2 - kSrcStereo) || //T265 @@ -6639,6 +6693,25 @@ Camera * PreferencesDialog::createCamera( _ui->comboBox_k4a_framerate->currentIndex(), _ui->comboBox_k4a_depth_resolution->currentIndex()); } + else if (driver == kSrcOrbbecSDK) + { + camera = new CameraOrbbecSDK( + device.toStdString(), + _ui->spinBox_orbbec_sdk_color_width->value(), + _ui->spinBox_orbbec_sdk_color_height->value(), + _ui->spinBox_orbbec_sdk_depth_width->value(), + _ui->spinBox_orbbec_sdk_depth_height->value(), + this->getGeneralInputRate(), + this->getSourceLocalTransform()); + ((CameraOrbbecSDK*)camera)->enableColorRectification(_ui->checkBox_orbbec_sdk_color_rectification->isChecked()); + ((CameraOrbbecSDK*)camera)->enableImu(_ui->checkBox_orbbec_sdk_imu->isChecked()); + ((CameraOrbbecSDK*)camera)->enableDepthMM(_ui->checkBox_orbbec_sdk_depth_mm->isChecked()); + + camera->setInterIMUPublishing( + _ui->checkbox_publishInterIMU->isChecked(), + _ui->checkbox_publishInterIMU->isChecked() && getIMUFilteringStrategy()>0? + IMUFilter::create((IMUFilter::Type)(getIMUFilteringStrategy()-1), this->getAllParameters()):0); + } else if (driver == kSrcRealSense) { if(useRawImages && _ui->comboBox_realsenseRGBSource->currentIndex()!=2) diff --git a/guilib/src/images/astra2.png b/guilib/src/images/astra2.png new file mode 100644 index 0000000000000000000000000000000000000000..1abbf83882b07746d962bde4e38a9b8174dacc1a GIT binary patch literal 3334 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 Q>Hq)$07*qoM6N<$f>ina6aWAK literal 0 HcmV?d00001 diff --git a/guilib/src/ui/aboutDialog.ui b/guilib/src/ui/aboutDialog.ui index 8df68638..0a6bfcbc 100644 --- a/guilib/src/ui/aboutDialog.ui +++ b/guilib/src/ui/aboutDialog.ui @@ -162,9 +162,9 @@ p, li { white-space: pre-wrap; } 0 - -43 + -446 596 - 1165 + 1211 @@ -185,8 +185,18 @@ p, li { white-space: pre-wrap; } - - + + + + With Zed Open Capture : + + + true + + + + + @@ -198,108 +208,18 @@ p, li { white-space: pre-wrap; } - - + + - BSD + With OpenVINS : true - - - - GPLv3 - - - true - - - - - - - MIT - - - true - - - - - - - With Open3D : - - - true - - - - - - - GPLv3 - - - true - - - - - - - BSD - - - true - - - - - - - PCL version : - - - true - - - - - - - With Python3 : - - - true - - - - - - - With FastCV : - - - true - - - - - - - MPL2 - - - true - - - - - + + @@ -311,188 +231,20 @@ p, li { white-space: pre-wrap; } - - + + - MIT + With cuVSLAM : true - - + + - Apache-2 - - - true - - - - - - - BSD - - - true - - - - - - - With SuperPoint Torch : - - - true - - - - - - - Apache v2 and/or GPLv2 - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With stereo FlyCapture2 : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With Zed SDK : - - - true - - - - - - - Apache v2 - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With RealSense : - - - true - - - - - - - With OpenChisel : - - - true - - - - - - - With TORO : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + With OpenNI2 : true @@ -509,28 +261,21 @@ p, li { white-space: pre-wrap; } - - + + - With DepthAI : + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter true - - - - With AliceVision : - - - true - - - - - + + @@ -555,18 +300,28 @@ p, li { white-space: pre-wrap; } - - + + - With DVO : + With FOVIS : true - - + + + + PCL version : + + + true + + + + + @@ -578,8 +333,38 @@ p, li { white-space: pre-wrap; } - - + + + + With Octomap : + + + true + + + + + + + With Open3D : + + + true + + + + + + + With ORB SLAM 2 : + + + true + + + + + @@ -591,48 +376,18 @@ p, li { white-space: pre-wrap; } - - + + - LGPL + GPLv3 true - - - - With libpointmatcher : - - - true - - - - - - - With MSCKF : - - - true - - - - - - - With CPU-TSDF : - - - true - - - - - + + @@ -644,18 +399,44 @@ p, li { white-space: pre-wrap; } - - + + - With MYNTEYE S : + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter true - - + + + + With RealSense : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + @@ -677,8 +458,8 @@ p, li { white-space: pre-wrap; } - - + + MIT @@ -687,38 +468,8 @@ p, li { white-space: pre-wrap; } - - - - With g2o : - - - true - - - - - - - BSD - - - true - - - - - - - BSD - - - true - - - - - + + @@ -730,8 +481,71 @@ p, li { white-space: pre-wrap; } - - + + + + With OpenNI : + + + true + + + + + + + With Zed SDK : + + + true + + + + + + + Penn Software License + + + true + + + + + + + <html><head/><body><p><span style=" font-weight:600;">Third Party Libraries</span></p></body></html> + + + Qt::RichText + + + true + + + + + + + Apache v2 and/or GPLv2 + + + true + + + + + + + Apache v2 and/or GPLv2 + + + true + + + + + @@ -743,48 +557,8 @@ p, li { white-space: pre-wrap; } - - - - GPLv2 - - - true - - - - - - - With OpenNI2 : - - - true - - - - - - - Apache-2 - - - true - - - - - - - Qt version : - - - true - - - - - + + @@ -806,229 +580,8 @@ p, li { white-space: pre-wrap; } - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - BSD - - - true - - - - - - - With ORB SLAM 2 : - - - true - - - - - - - With Ceres : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - Apache v2 - - - true - - - - - - - With OKVIS : - - - true - - - - - - - With XVisio SDK : - - - true - - - - - - - VTK version : - - - true - - - - - - - Creative Commons [Attribution-NonCommercial-ShareAlike] - - - true - - - - - - - With K4A : - - - true - - - - - - - BSD - - - true - - - - - - - BSD - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - BSD - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With GridMap : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - + + GPLv3 @@ -1037,454 +590,6 @@ p, li { white-space: pre-wrap; } - - - - With Viso2 : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With GTSAM : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - MIT - - - true - - - - - - - With CCCoreLib : - - - true - - - - - - - Apache v2 and/or GPLv2 - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With FOVIS : - - - true - - - - - - - BSD - - - true - - - - - - - BSD - - - true - - - - - - - With PDAL : - - - true - - - - - - - With Zed Open Capture : - - - true - - - - - - - With ORB Octree : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - <html><head/><body><p><span style=" font-weight:600;">Third Party Libraries</span></p></body></html> - - - Qt::RichText - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - BSD - - - true - - - - - - - GPLv3 - - - true - - - - - - - With CudaSift : - - - true - - - - - - - GPLv2 - - - true - - - - - - - BSD - - - true - - - - - - - <html><head/><body><p><span style=" font-weight:600;">License</span></p></body></html> - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - BSD - - - true - - - - - - - With cvsba : - - - true - - - - - - - Open Source or Commercial - - - true - - - - - - - Apache v2 - - - true - - - - - - - With Freenect : - - - true - - - - - - - With OpenVINS : - - - true - - - - - - - With OpenNI : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - @@ -1498,122 +603,17 @@ p, li { white-space: pre-wrap; } - - + + - With Octomap : + With Freenect : true - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - GPLv2 - - - true - - - - - - - GPLv3 - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With Freenect2 : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With VINS-Fusion : - - - true - - - - + With MRPT : @@ -1623,28 +623,8 @@ p, li { white-space: pre-wrap; } - - - - GPLv3 - - - true - - - - - - - With cuVSLAM : - - - true - - - - - + + @@ -1656,10 +636,561 @@ p, li { white-space: pre-wrap; } - - + + - NVIDIA ISAAC ROS SOFTWARE LICENSE + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With Viso2 : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With libpointmatcher : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With stereo FlyCapture2 : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + Creative Commons [Attribution-NonCommercial-ShareAlike] + + + true + + + + + + + Apache-2 + + + true + + + + + + + BSD + + + true + + + + + + + With loam_velodyne : + + + true + + + + + + + BSD + + + true + + + + + + + BSD + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With DVO : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With PDAL : + + + true + + + + + + + With CCCoreLib : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + BSD + + + true + + + + + + + OpenCV version : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With MYNTEYE S : + + + true + + + + + + + Apache v2 + + + true + + + + + + + With FastCV : + + + true + + + + + + + With XVisio SDK : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With GTSAM : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + GPLv2 + + + true + + + + + + + BSD + + + true + + + + + + + GPLv3 + + + true + + + + + + + MPL2 + + + true + + + + + + + VTK version : + + + true + + + + + + + BSD + + + true + + + + + + + MIT + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With g2o : + + + true + + + + + + + With RealSense2 : + + + true + + + + + + + BSD + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With OpenChisel : + + + true + + + + + + + BSD + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With AliceVision : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + GPLv3 + + + true + + + + + + + With Python3 : true @@ -1667,9 +1198,55 @@ p, li { white-space: pre-wrap; } - + - With loam_velodyne : + With OKVIS : + + + true + + + + + + + With TORO : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With SuperPoint Torch : true @@ -1689,20 +1266,327 @@ p, li { white-space: pre-wrap; } - - + + - With RealSense2 : + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter true - - + + - OpenCV version : + BSD + + + true + + + + + + + With CPU-TSDF : + + + true + + + + + + + With CudaSift : + + + true + + + + + + + Apache v2 + + + true + + + + + + + With DepthAI : + + + true + + + + + + + With Ceres : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + BSD + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + <html><head/><body><p><span style=" font-weight:600;">License</span></p></body></html> + + + true + + + + + + + With libLAS : + + + true + + + + + + + NVIDIA ISAAC ROS SOFTWARE LICENSE + + + true + + + + + + + GPLv3 + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With GridMap : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With ORB Octree : + + + true + + + + + + + MIT + + + true + + + + + + + BSD + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + GPLv2 + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + GPLv2 + + + true + + + + + + + Apache-2 + + + true + + + + + + + GPLv3 + + + true + + + + + + + With cvsba : + + + true + + + + + + + With VINS-Fusion : true @@ -1719,41 +1603,8 @@ p, li { white-space: pre-wrap; } - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - Penn Software License - - - true - - - - - - - GPLv3 - - - true - - - - - + + @@ -1775,18 +1626,200 @@ p, li { white-space: pre-wrap; } - - + + - With libLAS : + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter true - - + + + + MIT + + + true + + + + + + + Qt version : + + + true + + + + + + + LGPL + + + true + + + + + + + With MSCKF : + + + true + + + + + + + Open Source or Commercial + + + true + + + + + + + With K4A : + + + true + + + + + + + Apache v2 + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + GPLv3 + + + true + + + + + + + BSD + + + true + + + + + + + With Freenect2 : + + + true + + + + + + + BSD + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + BSD + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With Orbbec SDK : + + + true + + + + + + + MIT + + + true + + + + + @@ -1833,7 +1866,7 @@ p, li { white-space: pre-wrap; } - Copyright (C) 2010-2024 IntRoLab - Université de Sherbrooke + Copyright (C) 2010-2025 IntRoLab - Université de Sherbrooke Qt::AlignCenter diff --git a/guilib/src/ui/consoleWidget.ui b/guilib/src/ui/consoleWidget.ui index 112de6f7..d824ad19 100644 --- a/guilib/src/ui/consoleWidget.ui +++ b/guilib/src/ui/consoleWidget.ui @@ -35,7 +35,7 @@ 99999 - 100 + 500 diff --git a/guilib/src/ui/mainWindow.ui b/guilib/src/ui/mainWindow.ui index 666e809a..bbfee4ed 100644 --- a/guilib/src/ui/mainWindow.ui +++ b/guilib/src/ui/mainWindow.ui @@ -239,9 +239,20 @@ + + + Orbbec Astra 2 + + + + :/images/astra2.png:/images/astra2.png + + + + @@ -1749,6 +1760,14 @@ Xvisio + + + true + + + Orbbec SDK + + diff --git a/guilib/src/ui/preferencesDialog.ui b/guilib/src/ui/preferencesDialog.ui index 77ae0a9a..8cc3b2a1 100644 --- a/guilib/src/ui/preferencesDialog.ui +++ b/guilib/src/ui/preferencesDialog.ui @@ -63,7 +63,7 @@ 0 - 0 + -641 713 4779 @@ -95,7 +95,7 @@ QFrame::Raised - 19 + 5 @@ -3469,7 +3469,7 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - 1 + 0 @@ -3565,6 +3565,11 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki Xvisio SeerSense + + + Orbbec SDK + + @@ -3585,7 +3590,7 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - 11 + 12 @@ -3631,6 +3636,19 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki + + + + Qt::Vertical + + + + 0 + 0 + + + + @@ -3642,8 +3660,8 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - 20 - 696 + 0 + 0 @@ -3659,7 +3677,7 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - 20 + 0 0 @@ -3676,7 +3694,7 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - 20 + 0 0 @@ -3911,6 +3929,19 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki + + + + Qt::Vertical + + + + 0 + 0 + + + + @@ -4113,6 +4144,19 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki + + + + Qt::Vertical + + + + 0 + 0 + + + + @@ -4283,6 +4327,19 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki + + + + Qt::Vertical + + + + 0 + 0 + + + + @@ -4425,10 +4482,23 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki + + + + Qt::Vertical + + + + 0 + 0 + + + + - + @@ -4479,10 +4549,23 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki + + + + Qt::Vertical + + + + 0 + 0 + + + + - + @@ -4754,10 +4837,23 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki + + + + Qt::Vertical + + + + 0 + 0 + + + + - + @@ -4962,6 +5058,19 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki + + + + Qt::Vertical + + + + 0 + 0 + + + + @@ -4973,8 +5082,193 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - 20 - 40 + 0 + 0 + + + + + + + + + + + + Orbbec SDK + + + + + + + 0 + 0 + + + + Resolution + + + + + + 9999 + + + + + + + 9999 + + + + + + + Depth + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + 9999 + + + + + + + Color + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Width + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Height + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + 9999 + + + + + + + + + + + + Enable IMU + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Rectify color frames + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + + + + + + + + + + + + Convert depth in mm (16 bits format) + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + + + + + + + + + + Qt::Vertical + + + + 0 + 0 @@ -6942,19 +7236,6 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - - - Qt::Vertical - - - - 20 - 0 - - - - @@ -7145,6 +7426,19 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki + + + + Qt::Vertical + + + + 20 + 0 + + + + @@ -7186,29 +7480,10 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - + + - Ground truth file. Select the correct format below. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - Ground truth format. See tool tip for more details on formats. Note that formats without stamps should have the same number of values than the source images. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + ... @@ -7225,24 +7500,63 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - + + - ... + IMU Rate. To synchronize capture rate with IMU timestamps, set to 0. This can be set a little over the actual IMU rate to keep up with camera capture rate if images are dropped by odometry. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - + + - - + + + + <html><head/><body><p>Format (3 values): x y z<br/>Format (6 values): x y z roll pitch yaw<br/>Format (7 values): x y z qx qy qz qw<br/>Format (9 values, 3x3 rotation): r11 r12 r13 r21 r22 r23 r31 r32 r33<br/>Format (12 values, 3x4 transform): r11 r12 r13 tx r21 r22 r23 ty r31 r32 r33 tz</p><p>EuRoC: /base_link to /imu = 0 0 1 0 -1 0 1 0 0</p></body></html> + - Path to directory containing optional laser scans (*.pcd, *.ply, *.bin [KITTI format]). The directory should have the same size has the images directory. + 0 0 0 0 0 0 + + + + + + + Ground truth file. Select the correct format below. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + <html><head/><body><p>Format (3 values): x y z<br/>Format (6 values): x y z roll pitch yaw<br/>Format (7 values): x y z qx qy qz qw<br/>Format (9 values, 3x3 rotation): r11 r12 r13 r21 r22 r23 r31 r32 r33<br/>Format (12 values, 3x4 transform): r11 r12 r13 tx r21 r22 r23 ty r31 r32 r33 tz</p><p>KITTI: /base_link to /scan = -0.27 0 0.08 0 0 0<br/>KITTI: /base_footprint to /scan = -0.27 0 1.75 0 0 0</p></body></html> + + + 0 0 0 0 0 0 + + + + + + + Maximum laser scan points. true @@ -7265,10 +7579,10 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - + + - Use file names as timestamps. Format is epoch time. Example: "1305031102.175304.png". + Local transform from /base_link to /imu_link. Mouse over the box to show formats. true @@ -7278,54 +7592,13 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - - - ... - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - Maximum laser scan points. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - @@ -7339,186 +7612,6 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - - - - - - - - - - Timestamps file (*.txt). The file should contain one column. The number of rows should be the same than the number of images in the folder. Not used if "Use file names as timestamps" above is checked. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - <html><head/><body><p>KITTI: 130 000 points</p></body></html> - - - 99999999 - - - - - - - <html><head/><body><p>Raw Format (3 values): x y z<br/>Raw Format (6 values): x y z roll pitch yaw<br/>Raw Format (7 values): x y z qx qy qz qw<br/>Raw Format (9 values, 3x3 rotation): r11 r12 r13 r21 r22 r23 r31 r32 r33<br/>Raw Format (12 values, 3x4 transform): r11 r12 r13 tx r21 r22 r23 ty r31 r32 r33 tz</p><p>RGBD-SLAM (stamp tx ty tz qx qy qz qw)<br/>RGBD-SLAM + ID (stamp id tx ty tz qx qy qz qw)<br/>KITTI (stamp + 12 values transform)<br/>TORO<br/>g2o<br/>NewCollege (stamp x y)<br/>Malaga Urban (GPS)<br/>St Lucia Stereo (INS)<br/>EuRoC MAV (stamp,tx,ty,tz,qw,qx,qy,qz...)</p></body></html> - - - QComboBox::AdjustToContents - - - - Raw - - - - - RGBD-SLAM (motion capture) - - - - - KITTI - - - - - TORO - - - - - g2o - - - - - NewCollege - - - - - Malaga Urban - - - - - St Lucia Stereo - - - - - Karlsruhe - - - - - EuRoC MAV - - - - - RGBD-SLAM - - - - - RGBD-SLAM + ID - - - - - rgbd_bonn - - - - - - - - ... - - - - - - - <html><head/><body><p>Format (3 values): x y z<br/>Format (6 values): x y z roll pitch yaw<br/>Format (7 values): x y z qx qy qz qw<br/>Format (9 values, 3x3 rotation): r11 r12 r13 r21 r22 r23 r31 r32 r33<br/>Format (12 values, 3x4 transform): r11 r12 r13 tx r21 r22 r23 ty r31 r32 r33 tz</p><p>KITTI: /base_link to /scan = -0.27 0 0.08 0 0 0<br/>KITTI: /base_footprint to /scan = -0.27 0 1.75 0 0 0</p></body></html> - - - 0 0 0 0 0 0 - - - - - - - Local transform from /base_link to /laser_link. Mouse over the box to show formats. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - QComboBox::AdjustToContents - - - - Disabled - - - - - BG - - - - - GB - - - - - RG - - - - - GR - - - - - - - - Odometry format. See tool tip for more details on formats. Note that formats without stamps should have the same number of values than the source images. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - @@ -7594,10 +7687,29 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - + + - ... + Timestamps file (*.txt). The file should contain one column. The number of rows should be the same than the number of images in the folder. Not used if "Use file names as timestamps" above is checked. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Odometry format. See tool tip for more details on formats. Note that formats without stamps should have the same number of values than the source images. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse @@ -7614,6 +7726,158 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki + + + + + + + + + + + + + + + + + + ... + + + + + + + EuRoC: 200 Hz -> 250 Hz + + + 99999999 + + + + + + + Local transform from /base_link to /laser_link. Mouse over the box to show formats. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Path to directory containing optional laser scans (*.pcd, *.ply, *.bin [KITTI format]). The directory should have the same size has the images directory. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + ... + + + + + + + Use file names as timestamps. Format is epoch time. Example: "1305031102.175304.png". + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + <html><head/><body><p>Raw Format (3 values): x y z<br/>Raw Format (6 values): x y z roll pitch yaw<br/>Raw Format (7 values): x y z qx qy qz qw<br/>Raw Format (9 values, 3x3 rotation): r11 r12 r13 r21 r22 r23 r31 r32 r33<br/>Raw Format (12 values, 3x4 transform): r11 r12 r13 tx r21 r22 r23 ty r31 r32 r33 tz</p><p>RGBD-SLAM (stamp tx ty tz qx qy qz qw)<br/>RGBD-SLAM + ID (stamp id tx ty tz qx qy qz qw)<br/>KITTI (stamp + 12 values transform)<br/>TORO<br/>g2o<br/>NewCollege (stamp x y)<br/>Malaga Urban (GPS)<br/>St Lucia Stereo (INS)<br/>EuRoC MAV (stamp,tx,ty,tz,qw,qx,qy,qz...)</p></body></html> + + + QComboBox::AdjustToContents + + + + Raw + + + + + RGBD-SLAM (motion capture) + + + + + KITTI + + + + + TORO + + + + + g2o + + + + + NewCollege + + + + + Malaga Urban + + + + + St Lucia Stereo + + + + + Karlsruhe + + + + + EuRoC MAV + + + + + RGBD-SLAM + + + + + RGBD-SLAM + ID + + + + + rgbd_bonn + + + + @@ -7633,10 +7897,10 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - + + - Local transform from /base_link to /imu_link. Mouse over the box to show formats. + Load config file for each frame. For calibration, global calibration path above should be empty. Config files should be in the same directory than RGB frames and they should have the same name than the corresponding frame file. Currently supporting 3DScannerApp for iOS export config format (JSON, intrinsics, pose and stamp) and RTAB-Map calibration file format. true @@ -7646,6 +7910,55 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki + + + + <html><head/><body><p>KITTI: 130 000 points</p></body></html> + + + 99999999 + + + + + + + + + + + + + + QComboBox::AdjustToContents + + + + Disabled + + + + + BG + + + + + GB + + + + + RG + + + + + GR + + + + @@ -7659,6 +7972,33 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki + + + + + + + + + + + Ground truth format. See tool tip for more details on formats. Note that formats without stamps should have the same number of values than the source images. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + @@ -7666,63 +8006,17 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - + + - - - - <html><head/><body><p>Format (3 values): x y z<br/>Format (6 values): x y z roll pitch yaw<br/>Format (7 values): x y z qx qy qz qw<br/>Format (9 values, 3x3 rotation): r11 r12 r13 r21 r22 r23 r31 r32 r33<br/>Format (12 values, 3x4 transform): r11 r12 r13 tx r21 r22 r23 ty r31 r32 r33 tz</p><p>EuRoC: /base_link to /imu = 0 0 1 0 -1 0 1 0 0</p></body></html> - + + - 0 0 0 0 0 0 - - - - - - - IMU Rate. To synchronize capture rate with IMU timestamps, set to 0. This can be set a little over the actual IMU rate to keep up with camera capture rate if images are dropped by odometry. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - EuRoC: 200 Hz -> 250 Hz - - - 99999999 - - - - - - - Load config file for each frame. For calibration, global calibration path above should be empty. Config files should be in the same directory than RGB frames and they should have the same name than the corresponding frame file. Currently supporting 3DScannerApp for iOS export config format (JSON, intrinsics, pose and stamp) and RTAB-Map calibration file format. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - + ... diff --git a/tools/CameraRGBD/main.cpp b/tools/CameraRGBD/main.cpp index 1d562d92..4430b54c 100644 --- a/tools/CameraRGBD/main.cpp +++ b/tools/CameraRGBD/main.cpp @@ -66,13 +66,10 @@ void showUsage() " 14=ZED Open Capture\n" " 15=depthai-core\n" " 16=XVSDK (SeerSense)\n" + " 17=Orbbec SDK\n" " Options:\n" " -rate #.# Input rate Hz (default 0=inf)\n" - " -device # Device ID (number or string)\n" - " -save_stereo \"path\" Save stereo images in a folder or a video file (side by side *.avi).\n" - " -fourcc \"XXXX\" Four characters FourCC code (default is \"MJPG\") used\n" - " when saving stereo images to a video file.\n" - " See http://www.fourcc.org/codecs.php for more codes.\n"); + " -device # Device ID (number or string)\n"); exit(1); } @@ -92,9 +89,7 @@ int main(int argc, char * argv[]) //ULogger::setPrintWhere(false); int driver = 0; - std::string stereoSavePath; float rate = 0.0f; - std::string fourcc = "MJPG"; std::string deviceId; if(argc < 2) { @@ -134,37 +129,6 @@ int main(int argc, char * argv[]) } continue; } - if(strcmp(argv[i], "-save_stereo") == 0) - { - ++i; - if(i < argc) - { - stereoSavePath = argv[i]; - } - else - { - showUsage(); - } - continue; - } - if(strcmp(argv[i], "-fourcc") == 0) - { - ++i; - if(i < argc) - { - fourcc = argv[i]; - if(fourcc.size() != 4) - { - UERROR("fourcc should be 4 characters."); - showUsage(); - } - } - else - { - showUsage(); - } - continue; - } if(strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-help") == 0) { showUsage(); @@ -177,9 +141,9 @@ int main(int argc, char * argv[]) // last driver = atoi(argv[i]); - if(driver < 0 || driver > 15) + if(driver < 0 || driver > 17) { - UERROR("driver should be between 0 and 15."); + UERROR("driver should be between 0 and 17."); showUsage(); } } @@ -187,63 +151,54 @@ int main(int argc, char * argv[]) UINFO("Using driver %d (device=%s)", driver, deviceId.empty()?"0": deviceId.c_str()); rtabmap::Camera * camera = 0; - if(driver < 6) + if(driver == 0) { - if(!stereoSavePath.empty()) + camera = new rtabmap::CameraOpenni(deviceId); + } + else if(driver == 1) + { + if(!rtabmap::CameraOpenNI2::available()) { - UWARN("-save_stereo option cannot be used with RGB-D drivers."); - stereoSavePath.clear(); + UERROR("Not built with OpenNI2 support..."); + exit(-1); } - - if(driver == 0) + camera = new rtabmap::CameraOpenNI2(deviceId); + } + else if(driver == 2) + { + if(!rtabmap::CameraFreenect::available()) { - camera = new rtabmap::CameraOpenni(deviceId); + UERROR("Not built with Freenect support..."); + exit(-1); } - else if(driver == 1) + camera = new rtabmap::CameraFreenect(deviceId.empty()?0:uStr2Int(deviceId)); + } + else if(driver == 3) + { + if(!rtabmap::CameraOpenNICV::available()) { - if(!rtabmap::CameraOpenNI2::available()) - { - UERROR("Not built with OpenNI2 support..."); - exit(-1); - } - camera = new rtabmap::CameraOpenNI2(deviceId); + UERROR("Not built with OpenNI from OpenCV support..."); + exit(-1); } - else if(driver == 2) + camera = new rtabmap::CameraOpenNICV(false); + } + else if(driver == 4) + { + if(!rtabmap::CameraOpenNICV::available()) { - if(!rtabmap::CameraFreenect::available()) - { - UERROR("Not built with Freenect support..."); - exit(-1); - } - camera = new rtabmap::CameraFreenect(deviceId.empty()?0:uStr2Int(deviceId)); + UERROR("Not built with OpenNI from OpenCV support..."); + exit(-1); } - else if(driver == 3) + camera = new rtabmap::CameraOpenNICV(true); + } + else if(driver == 5) + { + if(!rtabmap::CameraFreenect2::available()) { - if(!rtabmap::CameraOpenNICV::available()) - { - UERROR("Not built with OpenNI from OpenCV support..."); - exit(-1); - } - camera = new rtabmap::CameraOpenNICV(false); - } - else if(driver == 4) - { - if(!rtabmap::CameraOpenNICV::available()) - { - UERROR("Not built with OpenNI from OpenCV support..."); - exit(-1); - } - camera = new rtabmap::CameraOpenNICV(true); - } - else if(driver == 5) - { - if(!rtabmap::CameraFreenect2::available()) - { - UERROR("Not built with Freenect2 support..."); - exit(-1); - } - camera = new rtabmap::CameraFreenect2(deviceId.empty()?0:uStr2Int(deviceId), rtabmap::CameraFreenect2::kTypeColor2DepthSD); + UERROR("Not built with Freenect2 support..."); + exit(-1); } + camera = new rtabmap::CameraFreenect2(deviceId.empty()?0:uStr2Int(deviceId), rtabmap::CameraFreenect2::kTypeColor2DepthSD); } else if(driver == 6) { @@ -344,6 +299,15 @@ int main(int argc, char * argv[]) } camera = new rtabmap::CameraSeerSense(); } + else if (driver == 17) + { + if (!rtabmap::CameraOrbbecSDK::available()) + { + UERROR("Not built with Orbbec SDK support..."); + exit(-1); + } + camera = new rtabmap::CameraOrbbecSDK(); + } else { UFATAL(""); @@ -379,54 +343,11 @@ int main(int argc, char * argv[]) viewer = new pcl::visualization::CloudViewer("cloud"); } - cv::VideoWriter videoWriter; - UDirectory dir; - if(!stereoSavePath.empty() && - !data.imageRaw().empty() && - !data.rightRaw().empty()) - { - if(UFile::getExtension(stereoSavePath).compare("avi") == 0) - { - if(data.imageRaw().size() == data.rightRaw().size()) - { - if(rate <= 0) - { - UERROR("You should set the input rate when saving stereo images to a video file."); - showUsage(); - } - cv::Size targetSize = data.imageRaw().size(); - targetSize.width *= 2; - UASSERT(fourcc.size() == 4); - videoWriter.open( - stereoSavePath, - CV_FOURCC(fourcc.at(0), fourcc.at(1), fourcc.at(2), fourcc.at(3)), - rate, - targetSize, - data.imageRaw().channels() == 3); - } - else - { - UERROR("Images not the same size, cannot save stereo images to the video file."); - } - } - else if(UDirectory::exists(stereoSavePath)) - { - UDirectory::makeDir(stereoSavePath+"/"+"left"); - UDirectory::makeDir(stereoSavePath+"/"+"right"); - } - else - { - UERROR("Directory \"%s\" doesn't exist.", stereoSavePath.c_str()); - stereoSavePath.clear(); - } - } - // to catch the ctrl-c signal(SIGABRT, &sighandler); signal(SIGTERM, &sighandler); signal(SIGINT, &sighandler); - int id=1; while(!data.imageRaw().empty() && (viewer==0 || !viewer->wasStopped()) && running) { cv::Mat rgb = data.imageRaw(); @@ -495,43 +416,6 @@ int main(int argc, char * argv[]) if(c == 27) break; // if ESC, break and quit - if(videoWriter.isOpened()) - { - cv::Mat left = data.imageRaw(); - cv::Mat right = data.rightRaw(); - if(left.size() == right.size()) - { - cv::Size targetSize = left.size(); - targetSize.width *= 2; - cv::Mat targetImage(targetSize, left.type()); - if(right.type() != left.type()) - { - cv::Mat tmp; - cv::cvtColor(right, tmp, left.channels()==3?CV_GRAY2BGR:CV_BGR2GRAY); - right = tmp; - } - UASSERT(left.type() == right.type()); - - cv::Mat roiA(targetImage, cv::Rect( 0, 0, left.size().width, left.size().height )); - left.copyTo(roiA); - cv::Mat roiB( targetImage, cvRect( left.size().width, 0, left.size().width, left.size().height ) ); - right.copyTo(roiB); - - videoWriter.write(targetImage); - printf("Saved frame %d to \"%s\"\n", id, stereoSavePath.c_str()); - } - else - { - UERROR("Left and right images are not the same size!?"); - } - } - else if(!stereoSavePath.empty()) - { - cv::imwrite(stereoSavePath+"/"+"left/"+uNumber2Str(id) + ".jpg", data.imageRaw()); - cv::imwrite(stereoSavePath+"/"+"right/"+uNumber2Str(id) + ".jpg", data.rightRaw()); - printf("Saved frames %d to \"%s/left\" and \"%s/right\" directories\n", id, stereoSavePath.c_str(), stereoSavePath.c_str()); - } - ++id; data = camera->takeData(); } printf("Closing...\n"); From 40ac961079f853c4c975c1ed91fd1bdbea4d9078 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Thu, 9 Oct 2025 11:03:04 -0700 Subject: [PATCH 027/126] Fixed typos --- tools/Reprocess/main.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/Reprocess/main.cpp b/tools/Reprocess/main.cpp index 10fe6eb6..eb4fdd35 100644 --- a/tools/Reprocess/main.cpp +++ b/tools/Reprocess/main.cpp @@ -76,8 +76,8 @@ void showUsage() " cannot be detected in this mode (assuming the database contains continuous frames\n" " of a single session).\n" " -odom_input_guess Forward input database's odometry (if exists) as guess when recompting odometry.\n" - " -odom_lin_var #.# Override computed odometry linear covariance." - " -odom_ang_var #.# Override computed odometry angular covariance." + " -odom_lin_var #.# Override computed odometry linear covariance.\n" + " -odom_ang_var #.# Override computed odometry angular covariance.\n" " -start # Start from this node ID.\n" " -stop # Last node to process.\n" " -start_s # Start from this map session ID.\n" @@ -321,6 +321,7 @@ int main(int argc, char * argv[]) } else if(strcmp(argv[i], "-odom_input_guess") == 0 || strcmp(argv[i], "--odom_input_guess") == 0) { + recomputeOdometry = true; useInputOdometryAsGuess = true; } else if (strcmp(argv[i], "-odom_lin_var") == 0 || strcmp(argv[i], "--odom_lin_var") == 0) From 1abb70ef3678e0096034e74ff8ae2044edc33360 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Thu, 9 Oct 2025 13:37:52 -0700 Subject: [PATCH 028/126] Let rtabmap-reprocess return 0 when there is no error --- tools/Reprocess/main.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tools/Reprocess/main.cpp b/tools/Reprocess/main.cpp index eb4fdd35..89fdb1a8 100644 --- a/tools/Reprocess/main.cpp +++ b/tools/Reprocess/main.cpp @@ -642,7 +642,7 @@ int main(int argc, char * argv[]) if (databases.empty()) { printf("No input database \"%s\" detected!\n", inputDatabasePath.c_str()); - return -1; + return 1; } for (std::list::iterator iter = databases.begin(); iter != databases.end(); ++iter) { @@ -653,20 +653,20 @@ int main(int argc, char * argv[]) { printf("Did you mean \"%s\"?\n", uReplaceChar(inputDatabasePath, ':', ";").c_str()); } - return -1; + return 1; } if (UFile::getExtension(*iter).compare("db") != 0) { printf("File \"%s\" is not a database format (*.db)!\n", iter->c_str()); - return -1; + return 1; } } if(UFile::getExtension(outputDatabasePath).compare("db") != 0) { printf("File \"%s\" is not a database format (*.db)!\n", outputDatabasePath.c_str()); - return -1; + return 1; } if(UFile::exists(outputDatabasePath)) @@ -680,7 +680,7 @@ int main(int argc, char * argv[]) { printf("Failed opening input database!\n"); delete dbDriver; - return -1; + return 1; } ParametersMap parameters; @@ -793,7 +793,7 @@ int main(int argc, char * argv[]) printf("Input database doesn't have any nodes saved in it.\n"); dbDriver->closeConnection(false); delete dbDriver; - return -1; + return 1; } if(!((!incrementalMemory || appendMode) && databases.size() > 1)) { @@ -815,7 +815,7 @@ int main(int argc, char * argv[]) { printf("Failed opening input database!\n"); delete dbDriver; - return -1; + return 1; } ids.clear(); dbDriver->getAllNodeIds(ids, false, false, !intermediateNodes); @@ -1524,5 +1524,5 @@ int main(int argc, char * argv[]) } #endif - return databasesMerged; + return 0; } From d60c15593ddcbbede49e47c5aae4543367ba9b0e Mon Sep 17 00:00:00 2001 From: matlabbe Date: Thu, 9 Oct 2025 14:04:29 -0700 Subject: [PATCH 029/126] reprocess: show number of sessions linked to last pose --- tools/Reprocess/main.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/Reprocess/main.cpp b/tools/Reprocess/main.cpp index 89fdb1a8..c09aa3d3 100644 --- a/tools/Reprocess/main.cpp +++ b/tools/Reprocess/main.cpp @@ -1299,7 +1299,6 @@ int main(int argc, char * argv[]) } } - int databasesMerged = 0; if(!incrementalMemory) { showLocalizationStats(outputDatabasePath); @@ -1322,7 +1321,7 @@ int main(int argc, char * argv[]) mapIds.insert(id); } } - databasesMerged = mapIds.size(); + printf("Sessions linked to last pose: %ld/%ld\n", mapIds.size(), databases.size()); } } From 059cda85dcdecb677b7640ef6d68cda5ec4978d6 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Thu, 9 Oct 2025 14:44:13 -0700 Subject: [PATCH 030/126] reprocess: fixed crash when odometry returns empty covariance. Also added more checks about that in the code in general. --- corelib/src/DBReader.cpp | 2 +- corelib/src/OdometryInfo.cpp | 8 ++++---- corelib/src/OdometryThread.cpp | 1 + corelib/src/Rtabmap.cpp | 14 +++++++------- corelib/src/RtabmapThread.cpp | 5 +++-- guilib/src/DatabaseViewer.cpp | 1 + guilib/src/MainWindow.cpp | 1 + 7 files changed, 18 insertions(+), 14 deletions(-) diff --git a/corelib/src/DBReader.cpp b/corelib/src/DBReader.cpp index 7eb69c3e..eebfd715 100644 --- a/corelib/src/DBReader.cpp +++ b/corelib/src/DBReader.cpp @@ -858,7 +858,7 @@ SensorData DBReader::getNextData(SensorCaptureInfo * info) info->odomPose = pose; info->odomCovariance = infMatrix.inv(); info->odomVelocity = s->getVelocity(); - UDEBUG("odom variance = %f/%f", info->odomCovariance.at(0,0), info->odomCovariance.at(5,5)); + UDEBUG("odom variance = %f/%f", info->odomCovariance.empty()?0:info->odomCovariance.at(0,0), info->odomCovariance.empty()?0:info->odomCovariance.at(5,5)); } } delete s; diff --git a/corelib/src/OdometryInfo.cpp b/corelib/src/OdometryInfo.cpp index c06d10d4..4b1d3c90 100644 --- a/corelib/src/OdometryInfo.cpp +++ b/corelib/src/OdometryInfo.cpp @@ -126,10 +126,10 @@ std::map OdometryInfo::statistics(const Transform & pose) stats.insert(std::make_pair("Odometry/ICPStructuralComplexity/", reg.icpStructuralComplexity)); stats.insert(std::make_pair("Odometry/ICPStructuralDistribution/", reg.icpStructuralDistribution)); stats.insert(std::make_pair("Odometry/ICPCorrespondences/", reg.icpCorrespondences)); - stats.insert(std::make_pair("Odometry/StdDevLin/", sqrt((float)reg.covariance.at(0,0)))); - stats.insert(std::make_pair("Odometry/StdDevAng/", sqrt((float)reg.covariance.at(5,5)))); - stats.insert(std::make_pair("Odometry/VarianceLin/", (float)reg.covariance.at(0,0))); - stats.insert(std::make_pair("Odometry/VarianceAng/", (float)reg.covariance.at(5,5))); + stats.insert(std::make_pair("Odometry/StdDevLin/", reg.covariance.empty()?0:sqrt((float)reg.covariance.at(0,0)))); + stats.insert(std::make_pair("Odometry/StdDevAng/", reg.covariance.empty()?0:sqrt((float)reg.covariance.at(5,5)))); + stats.insert(std::make_pair("Odometry/VarianceLin/", reg.covariance.empty()?0:(float)reg.covariance.at(0,0))); + stats.insert(std::make_pair("Odometry/VarianceAng/", reg.covariance.empty()?0:(float)reg.covariance.at(5,5))); stats.insert(std::make_pair("Odometry/TimeEstimation/ms", timeEstimation*1000.0f)); stats.insert(std::make_pair("Odometry/TimeFiltering/ms", timeParticleFiltering*1000.0f)); stats.insert(std::make_pair("Odometry/LocalMapSize/", localMapSize)); diff --git a/corelib/src/OdometryThread.cpp b/corelib/src/OdometryThread.cpp index fa216ccf..0adb3160 100644 --- a/corelib/src/OdometryThread.cpp +++ b/corelib/src/OdometryThread.cpp @@ -134,6 +134,7 @@ void OdometryThread::mainLoop() UDEBUG("Odom pose = %s", pose.prettyPrint().c_str()); if(!pose.isNull()) { _previousGuessPose = event.info().odomPose; + UASSERT(event.info().odomPose.isNull() || !info.reg.covariance.empty()); if(!event.info().odomPose.isNull() && info.reg.covariance.at(0,0) >= 9999 && (pose.x() != 0.0f || pose.y() != 0.0f || pose.z() != 0.0f)) // not the first frame { diff --git a/corelib/src/Rtabmap.cpp b/corelib/src/Rtabmap.cpp index 38df8fde..18b5fc55 100644 --- a/corelib/src/Rtabmap.cpp +++ b/corelib/src/Rtabmap.cpp @@ -1502,8 +1502,8 @@ bool Rtabmap::process( float angleToClosestNodeInTheGraph = 0; if(_rgbdSlamMode) { - double linVar = odomCovariance.empty()?1.0f:uMax3(odomCovariance.at(0,0), odomCovariance.at(1,1)>=9999?0:odomCovariance.at(1,1), odomCovariance.at(2,2)>=9999?0:odomCovariance.at(2,2)); - double angVar = odomCovariance.empty()?1.0f:uMax3(odomCovariance.at(3,3)>=9999?0:odomCovariance.at(3,3), odomCovariance.at(4,4)>=9999?0:odomCovariance.at(4,4), odomCovariance.at(5,5)); + double linVar = odomCovariance.empty()?0.0f:uMax3(odomCovariance.at(0,0), odomCovariance.at(1,1)>=9999?0:odomCovariance.at(1,1), odomCovariance.at(2,2)>=9999?0:odomCovariance.at(2,2)); + double angVar = odomCovariance.empty()?0.0f:uMax3(odomCovariance.at(3,3)>=9999?0:odomCovariance.at(3,3), odomCovariance.at(4,4)>=9999?0:odomCovariance.at(4,4), odomCovariance.at(5,5)); statistics_.addStatistic(Statistics::kMemoryOdometry_variance_lin(), (float)linVar); statistics_.addStatistic(Statistics::kMemoryOdometry_variance_ang(), (float)angVar); @@ -1607,6 +1607,7 @@ bool Rtabmap::process( Transform t = _memory->computeTransform(oldId, signature->id(), guess, &info); if(!t.isNull()) { + UASSERT(!info.covariance.empty() && info.covariance.at(0,0) > 0.0 && info.covariance.at(5,5) > 0.0); UINFO("Odometry refining: update neighbor link (%d->%d, variance:lin=%f, ang=%f) from %s to %s", oldId, signature->id(), @@ -1614,7 +1615,6 @@ bool Rtabmap::process( info.covariance.at(5,5), guess.prettyPrint().c_str(), t.prettyPrint().c_str()); - UASSERT(info.covariance.at(0,0) > 0.0 && info.covariance.at(5,5) > 0.0); _memory->updateLink(Link(oldId, signature->id(), signature->getLinks().begin()->second.type(), t, info.covariance.inv())); if(_optimizeFromGraphEnd) @@ -1894,7 +1894,7 @@ bool Rtabmap::process( *iter, transform.prettyPrint().c_str()); // Add a loop constraint - UASSERT(info.covariance.at(0,0) > 0.0 && info.covariance.at(5,5) > 0.0); + UASSERT(!info.covariance.empty() && info.covariance.at(0,0) > 0.0 && info.covariance.at(5,5) > 0.0); if(_memory->addLink(Link(signature->id(), *iter, Link::kLocalTimeClosure, transform, getInformation(info.covariance)))) { ++proximityDetectionsInTimeFound; @@ -2782,7 +2782,7 @@ bool Rtabmap::process( signature->id(), nearestId, transform.prettyPrint().c_str()); - UASSERT(info.covariance.at(0,0) > 0.0 && info.covariance.at(5,5) > 0.0); + UASSERT(!info.covariance.empty() && info.covariance.at(0,0) > 0.0 && info.covariance.at(5,5) > 0.0); //for statistics loopClosureVisualInliersMeanDist = info.inliersMeanDistance; @@ -2993,7 +2993,7 @@ bool Rtabmap::process( } // set Identify covariance for laser scan matching only - UASSERT(info.covariance.at(0,0) > 0.0 && info.covariance.at(5,5) > 0.0); + UASSERT(!info.covariance.empty() && info.covariance.at(0,0) > 0.0 && info.covariance.at(5,5) > 0.0); _memory->addLink(Link(signature->id(), nearestId, Link::kLocalSpaceClosure, transform, getInformation(info.covariance)/_proximityMergedScanCovFactor, scanMatchingIds)); loopClosureLinksAdded.push_back(std::make_pair(signature->id(), nearestId)); @@ -3081,7 +3081,7 @@ bool Rtabmap::process( if(!rejectedLoopClosure) { // Make the new one the parent of the old one - UASSERT(info.covariance.at(0,0) > 0.0 && info.covariance.at(5,5) > 0.0); + UASSERT(!info.covariance.empty() && info.covariance.at(0,0) > 0.0 && info.covariance.at(5,5) > 0.0); loopClosureLinearVariance = uMax3(info.covariance.at(0,0), info.covariance.at(1,1)>=9999?0:info.covariance.at(1,1), info.covariance.at(2,2)>=9999?0:info.covariance.at(2,2)); loopClosureAngularVariance = uMax3(info.covariance.at(3,3)>=9999?0:info.covariance.at(3,3), info.covariance.at(4,4)>=9999?0:info.covariance.at(4,4), info.covariance.at(5,5)); diff --git a/corelib/src/RtabmapThread.cpp b/corelib/src/RtabmapThread.cpp index d9f227b5..ad4c216b 100644 --- a/corelib/src/RtabmapThread.cpp +++ b/corelib/src/RtabmapThread.cpp @@ -506,9 +506,10 @@ void RtabmapThread::addData(const OdometryEvent & odomEvent) ignoreFrame = true; } } + UASSERT(!odomEvent.info().reg.covariance.empty()); if(!lastPose_.isIdentity() && - (odomEvent.pose().isIdentity() || - odomEvent.info().reg.covariance.at(0,0)>=9999)) + (odomEvent.pose().isIdentity() || + odomEvent.info().reg.covariance.at(0,0)>=9999)) { if(odomEvent.pose().isIdentity()) { diff --git a/guilib/src/DatabaseViewer.cpp b/guilib/src/DatabaseViewer.cpp index fea57a6b..beb808fb 100644 --- a/guilib/src/DatabaseViewer.cpp +++ b/guilib/src/DatabaseViewer.cpp @@ -8603,6 +8603,7 @@ void DatabaseViewer::refineConstraint(int from, int to, Registration * reg, Regi if(!transform.isNull()) { + UASSERT(!info.covariance.empty()); if(!transform.isIdentity()) { if(info.covariance.at(0,0)<=0.0) diff --git a/guilib/src/MainWindow.cpp b/guilib/src/MainWindow.cpp index 5e3c7e66..df953724 100644 --- a/guilib/src/MainWindow.cpp +++ b/guilib/src/MainWindow.cpp @@ -1797,6 +1797,7 @@ void MainWindow::processOdometry(const rtabmap::OdometryEvent & odom, bool dataI //Process info if(_preferencesDialog->isCacheSavedInFigures() || _ui->statsToolBox->isVisible()) { + UASSERT(odom.info().reg.covariance.total() == 36 && odom.info().reg.covariance.type() == CV_64FC1); double linVar = uMax3(odom.info().reg.covariance.at(0,0), odom.info().reg.covariance.at(1,1)>=9999?0:odom.info().reg.covariance.at(1,1), odom.info().reg.covariance.at(2,2)>=9999?0:odom.info().reg.covariance.at(2,2)); double angVar = uMax3(odom.info().reg.covariance.at(3,3)>=9999?0:odom.info().reg.covariance.at(3,3), odom.info().reg.covariance.at(4,4)>=9999?0:odom.info().reg.covariance.at(4,4), odom.info().reg.covariance.at(5,5)); _ui->statsToolBox->updateStat("Odometry/Inliers/", _preferencesDialog->isTimeUsedInFigures()?data->stamp()-_firstStamp:(float)data->id(), (float)odom.info().reg.inliers, _preferencesDialog->isCacheSavedInFigures()); From ad8bbdc4b202f54fff5e559d5fd1999d52c3bbd6 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Thu, 9 Oct 2025 15:39:19 -0700 Subject: [PATCH 031/126] reprocess: fixing lost odom recovery when -odom_guess_input option is used --- corelib/src/DBReader.cpp | 3 ++- tools/Reprocess/main.cpp | 35 ++++++++++++++++++++++++++++------- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/corelib/src/DBReader.cpp b/corelib/src/DBReader.cpp index eebfd715..55300b0c 100644 --- a/corelib/src/DBReader.cpp +++ b/corelib/src/DBReader.cpp @@ -856,9 +856,10 @@ SensorData DBReader::getNextData(SensorCaptureInfo * info) if(info) { info->odomPose = pose; + UASSERT(!infMatrix.empty()); info->odomCovariance = infMatrix.inv(); info->odomVelocity = s->getVelocity(); - UDEBUG("odom variance = %f/%f", info->odomCovariance.empty()?0:info->odomCovariance.at(0,0), info->odomCovariance.empty()?0:info->odomCovariance.at(5,5)); + UDEBUG("odom variance = %f/%f", info->odomCovariance.at(0,0), info->odomCovariance.at(5,5)); } } delete s; diff --git a/tools/Reprocess/main.cpp b/tools/Reprocess/main.cpp index c09aa3d3..321a7c53 100644 --- a/tools/Reprocess/main.cpp +++ b/tools/Reprocess/main.cpp @@ -929,7 +929,8 @@ int main(int argc, char * argv[]) } else { - printf("Odometry will be recomputed (odom option is set)\n"); + printf("Odometry will be recomputed (\"odom\" option is set)%s.\n", + useInputOdometryAsGuess?" with input odometry guess (\"odom_guess_input\" option is set)":""); Parameters::parse(parameters, Parameters::kRtabmapDetectionRate(), rtabmapUpdateRate); if(rtabmapUpdateRate!=0) { @@ -952,18 +953,28 @@ int main(int argc, char * argv[]) } camThread.postUpdate(&data, &info); Transform lastLocalizationOdomPose = info.odomPose; - Transform previousOdomPose = info.odomPose; + Transform previousOdomPose; cv::Mat odomCovariance; bool inMotion = true; while(data.isValid() && g_loopForever) { if(recomputeOdometry) { - OdometryInfo odomInfo; - Transform pose = odometry->process(data, useInputOdometryAsGuess && !info.odomPose.isNull()?previousOdomPose.inverse() * info.odomPose:Transform(), &odomInfo); - previousOdomPose = info.odomPose; - if(odomInfo.reg.covariance.total() == 36) + if(useInputOdometryAsGuess && !info.odomCovariance.empty() && info.odomCovariance.at(0,0) >= 9999) { + if(!odometry->getPose().isIdentity()) { + printf("Reset odometry as input odometry triggered new map\n"); + odometry->reset(odometry->getPose()); + } + previousOdomPose.setNull(); + } + OdometryInfo odomInfo; + Transform pose = odometry->process(data, + (useInputOdometryAsGuess && !info.odomPose.isNull() && !previousOdomPose.isNull())?previousOdomPose.inverse() * info.odomPose:Transform(), + &odomInfo); + if(!pose.isNull() && odomInfo.reg.covariance.total() == 36) + { + previousOdomPose = info.odomPose; if(odomLinVarOverride > 0.0) { odomInfo.reg.covariance.at(0,0) = odomLinVarOverride; @@ -980,8 +991,18 @@ int main(int argc, char * argv[]) odomInfo.reg.covariance.at(0,0) != 1.0 && odomInfo.reg.covariance.at(0,0)>0.0) { + if( useInputOdometryAsGuess && + odomInfo.reg.covariance.at(0,0) >= 9999 && + !previousOdomPose.isNull() && + (pose.x() != 0.0f || pose.y() != 0.0f || pose.z() != 0.0f)) // not the first frame + { + // In case of external guess and auto reset, keep reporting lost till we + // process the second frame with valid covariance. This way it + // won't trigger a new map. + pose = Transform(); + } // Use largest covariance error (to be independent of the odometry frame rate) - if(odomCovariance.empty() || odomInfo.reg.covariance.at(0,0) > odomCovariance.at(0,0)) + else if(odomCovariance.empty() || odomInfo.reg.covariance.at(0,0) > odomCovariance.at(0,0)) { odomCovariance = odomInfo.reg.covariance; } From 99471819d5a892660e8f1e8cd8ce8d32f62e0a33 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Fri, 10 Oct 2025 14:47:50 -0700 Subject: [PATCH 032/126] reprocess: accepting Identity covariance (cuvslam first frames) --- tools/Reprocess/main.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/Reprocess/main.cpp b/tools/Reprocess/main.cpp index 321a7c53..6ce14289 100644 --- a/tools/Reprocess/main.cpp +++ b/tools/Reprocess/main.cpp @@ -988,7 +988,6 @@ int main(int argc, char * argv[]) odomInfo.reg.covariance.at(5,5) = odomAngVarOverride; } if(uIsFinite(odomInfo.reg.covariance.at(0,0)) && - odomInfo.reg.covariance.at(0,0) != 1.0 && odomInfo.reg.covariance.at(0,0)>0.0) { if( useInputOdometryAsGuess && From f54b7685f3d13c9c62a478ada8092d6dc0a348e0 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 12 Oct 2025 10:35:30 -0700 Subject: [PATCH 033/126] cmake: removed QUIET keywords on default OFF options --- CMakeLists.txt | 62 +++++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f7e1551b..7fa7d8c0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -203,7 +203,7 @@ option(WITH_ZED "Include ZED sdk support" ON) option(WITH_ZEDOC "Include ZED Open Capture support" ON) option(WITH_REALSENSE "Include RealSense support" ON) option(WITH_REALSENSE_SLAM "Include RealSenseSlam support" ON) -option(WITH_REALSENSE2 "Include RealSense support" ON) +option(WITH_REALSENSE2 "Include RealSense2 support" ON) option(WITH_MYNTEYE "Include mynteye-s support" ON) option(WITH_DEPTHAI "Include depthai-core support" OFF) option(WITH_XVSDK "Include XVisio SDK support" OFF) @@ -398,7 +398,7 @@ IF(NOT VTK_FOUND) ENDIF(NOT VTK_FOUND) IF(WITH_TORCH) - FIND_PACKAGE(Torch QUIET) + FIND_PACKAGE(Torch) IF(TORCH_FOUND) MESSAGE(STATUS "Found Torch: ${TORCH_INCLUDE_DIRS}") ENDIF(TORCH_FOUND) @@ -420,14 +420,14 @@ IF(WITH_PDAL) ENDIF(WITH_PDAL) IF(WITH_LIBLAS) - FIND_PACKAGE(libLAS QUIET) + FIND_PACKAGE(libLAS) IF(libLAS_FOUND) MESSAGE(STATUS "Found libLAS ${libLAS_VERSION}: ${libLAS_INCLUDE_DIRS}") ENDIF(libLAS_FOUND) ENDIF(WITH_LIBLAS) IF(WITH_CUDASIFT) - FIND_PACKAGE(CudaSift 3 QUIET) + FIND_PACKAGE(CudaSift 3) IF(CudaSift_FOUND) MESSAGE(STATUS "Found CudaSift") ENDIF(CudaSift_FOUND) @@ -552,7 +552,7 @@ IF(WITH_FLYCAPTURE2) ENDIF(WITH_FLYCAPTURE2) IF(WITH_CVSBA) - FIND_PACKAGE(cvsba QUIET) + FIND_PACKAGE(cvsba) IF(cvsba_FOUND) MESSAGE(STATUS "Found cvsba: ${cvsba_INCLUDE_DIRS}") ENDIF(cvsba_FOUND) @@ -589,7 +589,7 @@ IF(libpointmatcher_FOUND OR GTSAM_FOUND) ENDIF(libpointmatcher_FOUND OR GTSAM_FOUND) IF(WITH_CCCORELIB) - find_package(CCCoreLib QUIET) + find_package(CCCoreLib) IF(CCCoreLib_FOUND) MESSAGE(STATUS "Found CCCoreLib: ${CCCoreLib_INCLUDE_DIRS}") ENDIF(CCCoreLib_FOUND) @@ -601,7 +601,7 @@ IF(WITH_OPEN3D) ELSE() # Build Open3D like this to avoid linker errors in rtabmap: # cmake -DBUILD_SHARED_LIBS=ON -DGLIBCXX_USE_CXX11_ABI=ON -DCMAKE_BUILD_TYPE=Release .. - find_package(Open3D QUIET) + find_package(Open3D) IF(Open3D_FOUND) MESSAGE(STATUS "Found Open3D: ${Open3DINCLUDE_DIRS}") ENDIF(Open3D_FOUND) @@ -609,17 +609,17 @@ IF(WITH_OPEN3D) ENDIF(WITH_OPEN3D) IF(WITH_LOAM) - find_package(loam_velodyne QUIET) + find_package(loam_velodyne) IF(loam_velodyne_FOUND) MESSAGE(STATUS "Found loam_velodyne: ${loam_velodyne_INCLUDE_DIRS}") ENDIF(loam_velodyne_FOUND) ENDIF(WITH_LOAM) IF(WITH_FLOAM) - find_package(floam QUIET) + find_package(floam) IF(floam_FOUND) MESSAGE(STATUS "Found floam: ${floam_INCLUDE_DIRS}") - FIND_PACKAGE(Ceres QUIET REQUIRED) + FIND_PACKAGE(Ceres REQUIRED) ENDIF(floam_FOUND) ENDIF(WITH_FLOAM) @@ -686,21 +686,21 @@ IF(WITH_MYNTEYE) ENDIF(WITH_MYNTEYE) IF(WITH_DEPTHAI) - FIND_PACKAGE(depthai 2.24 QUIET) + FIND_PACKAGE(depthai 2.24) IF(depthai_FOUND) MESSAGE(STATUS "Found depthai-core (targets)") ENDIF(depthai_FOUND) ENDIF(WITH_DEPTHAI) IF(WITH_XVSDK) - FIND_PACKAGE(xvsdk QUIET) + FIND_PACKAGE(xvsdk) IF(xvsdk_FOUND) MESSAGE(STATUS "Found xvsdk (targets)") ENDIF(xvsdk_FOUND) ENDIF(WITH_XVSDK) IF(WITH_ORBBEC_SDK) - FIND_PACKAGE(OrbbecSDK 2 QUIET) + FIND_PACKAGE(OrbbecSDK 2) IF(OrbbecSDK_FOUND) MESSAGE(STATUS "Found OrbbecSDK v2 (targets)") ENDIF(OrbbecSDK_FOUND) @@ -717,35 +717,35 @@ IF(WITH_OCTOMAP) ENDIF(WITH_OCTOMAP) IF(WITH_GRIDMAP) - FIND_PACKAGE(grid_map_core QUIET) + FIND_PACKAGE(grid_map_core) IF(grid_map_core_FOUND) MESSAGE(STATUS "Found grid_map_core ${grid_map_core_VERSION}: ${grid_map_core_INCLUDE_DIRS}") ENDIF(grid_map_core_FOUND) ENDIF(WITH_GRIDMAP) IF(WITH_CPUTSDF) - FIND_PACKAGE(CPUTSDF QUIET) + FIND_PACKAGE(CPUTSDF) IF(CPUTSDF_FOUND) MESSAGE(STATUS "Found CPUTSDF: ${CPUTSDF_INCLUDE_DIRS}") ENDIF(CPUTSDF_FOUND) ENDIF(WITH_CPUTSDF) IF(WITH_OPENCHISEL) - find_package(open_chisel QUIET) + find_package(open_chisel) if(open_chisel_FOUND) MESSAGE(STATUS "Found open_chisel: ${open_chisel_INCLUDE_DIRS}") endif(open_chisel_FOUND) ENDIF(WITH_OPENCHISEL) IF(WITH_ALICE_VISION) - find_package(AliceVision CONFIG QUIET) + find_package(AliceVision CONFIG) IF(AliceVision_FOUND) IF(${AliceVision_VERSION} VERSION_LESS_EQUAL "2.2") find_package(Boost COMPONENTS log log_setup container REQUIRED) ENDIF(${AliceVision_VERSION} VERSION_LESS_EQUAL "2.2") SET(CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH};/usr/local/lib/cmake/modules") - find_package(Geogram REQUIRED QUIET) - find_package(assimp QUIET) + find_package(Geogram REQUIRED) + find_package(assimp) add_definitions("-DRTABMAP_ALICE_VISION_MAJOR=${AliceVision_VERSION_MAJOR}") add_definitions("-DRTABMAP_ALICE_VISION_MINOR=${AliceVision_VERSION_MINOR}") add_definitions("-DRTABMAP_ALICE_VISION_PATCH=${AliceVision_VERSION_PATCH}") @@ -753,28 +753,28 @@ IF(WITH_ALICE_VISION) ENDIF(WITH_ALICE_VISION) IF(WITH_FOVIS) - FIND_PACKAGE(libfovis QUIET) + FIND_PACKAGE(libfovis) IF(libfovis_FOUND) MESSAGE(STATUS "Found libfovis: ${libfovis_INCLUDE_DIRS}") ENDIF(libfovis_FOUND) ENDIF(WITH_FOVIS) IF(WITH_VISO2) - FIND_PACKAGE(libviso2 QUIET) + FIND_PACKAGE(libviso2) IF(libviso2_FOUND) MESSAGE(STATUS "Found libviso2: ${libviso2_INCLUDE_DIRS}") ENDIF(libviso2_FOUND) ENDIF(WITH_VISO2) IF(WITH_DVO) - FIND_PACKAGE(dvo_core QUIET) + FIND_PACKAGE(dvo_core) IF(dvo_core_FOUND) MESSAGE(STATUS "Found dvo_core: ${dvo_core_INCLUDE_DIRS}") ENDIF(dvo_core_FOUND) ENDIF(WITH_DVO) IF(WITH_OKVIS) - FIND_PACKAGE(okvis 1.1 QUIET) + FIND_PACKAGE(okvis 1.1) IF(okvis_FOUND) MESSAGE(STATUS "Found okvis: ${OKVIS_INCLUDE_DIRS}") find_package(brisk 2 REQUIRED) @@ -789,7 +789,7 @@ ENDIF(WITH_OKVIS) # If built with okvis, we found already ceres above IF(WITH_CERES) IF(NOT okvis_FOUND AND NOT floam_FOUND) - FIND_PACKAGE(Ceres QUIET) + FIND_PACKAGE(Ceres) MESSAGE(STATUS "Found ceres ${Ceres_VERSION}: ${CERES_INCLUDE_DIRS}") ENDIF(NOT okvis_FOUND AND NOT floam_FOUND) ELSEIF(Ceres_FOUND) @@ -797,7 +797,7 @@ ELSEIF(Ceres_FOUND) ENDIF() IF(WITH_MSCKF_VIO) - FIND_PACKAGE(msckf_vio QUIET) + FIND_PACKAGE(msckf_vio) IF(msckf_vio_FOUND) MESSAGE(STATUS "Found msckf_vio: ${msckf_vio_INCLUDE_DIRS}") ENDIF(msckf_vio_FOUND) @@ -809,7 +809,7 @@ set(WITH_VINS_FUSION ON) ENDIF(WITH_VINS AND NOT WITH_VINS_FUSION) IF(WITH_VINS_FUSION) - FIND_PACKAGE(vins QUIET) + FIND_PACKAGE(vins) IF(vins_FOUND) MESSAGE(STATUS "Found vins-fusion: ${vins_INCLUDE_DIRS}") IF(okvis_FOUND) @@ -819,11 +819,11 @@ IF(WITH_VINS_FUSION) ENDIF(WITH_VINS_FUSION) IF(WITH_OPENVINS) - FIND_PACKAGE(ov_msckf QUIET) + FIND_PACKAGE(ov_msckf) # On ROS2, the indirect includes and libraries # are not forwarded by ov_msckf target, append them manually - FIND_PACKAGE(ov_core QUIET) - FIND_PACKAGE(ov_init QUIET) + FIND_PACKAGE(ov_core) + FIND_PACKAGE(ov_init) IF(ov_msckf_FOUND AND ov_core_FOUND AND ov_init_FOUND) SET(ov_msckf_INCLUDE_DIRS ${ov_msckf_INCLUDE_DIRS} @@ -852,14 +852,14 @@ IF(WITH_OPENGV) ENDIF(WITH_OPENGV) IF(WITH_ORB_SLAM AND NOT G2O_FOUND) - FIND_PACKAGE(ORB_SLAM QUIET) + FIND_PACKAGE(ORB_SLAM) IF(ORB_SLAM_FOUND) MESSAGE(STATUS "Found ORB_SLAM${ORB_SLAM_VERSION}: ${ORB_SLAM_INCLUDE_DIRS}") ENDIF(ORB_SLAM_FOUND) ENDIF(WITH_ORB_SLAM AND NOT G2O_FOUND) IF(WITH_CUVSLAM) - FIND_PACKAGE(CuVSLAM QUIET) + FIND_PACKAGE(CuVSLAM) IF(CUVSLAM_FOUND) MESSAGE(STATUS "Found cuVSLAM: ${CUVSLAM_INCLUDE_DIRS}") ENDIF() From 739628aef9cd765025dd2a0b99c8540abe65e808 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 12 Oct 2025 13:30:16 -0700 Subject: [PATCH 034/126] MacOSX: added entitlements to access Documents and camera --- CMakeLists.txt | 10 +++++++--- app/src/CMakeLists.txt | 8 ++++++++ app/src/app.entitlements | 14 ++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 app/src/app.entitlements diff --git a/CMakeLists.txt b/CMakeLists.txt index 7fa7d8c0..24c86f42 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -578,10 +578,14 @@ IF(WITH_POINTMATCHER) ENDIF(WITH_POINTMATCHER) IF(libpointmatcher_FOUND OR GTSAM_FOUND) - find_package(Boost COMPONENTS thread filesystem system program_options date_time REQUIRED) - IF(Boost_MINOR_VERSION GREATER 47) + find_package(Boost COMPONENTS thread filesystem program_options date_time REQUIRED) + IF(Boost_MINOR_VERSION GREATER 80) + find_package(Boost COMPONENTS thread filesystem program_options date_time chrono timer serialization REQUIRED) + ELSEIF(Boost_MINOR_VERSION GREATER 47) find_package(Boost COMPONENTS thread filesystem system program_options date_time chrono timer serialization REQUIRED) - ENDIF(Boost_MINOR_VERSION GREATER 47) + ELSE() + find_package(Boost COMPONENTS thread filesystem system program_options date_time REQUIRED) + ENDIF() IF(WIN32) MESSAGE(STATUS "Boost_LIBRARY_DIRS=${Boost_LIBRARY_DIRS}") link_directories(${Boost_LIBRARY_DIRS}) diff --git a/app/src/CMakeLists.txt b/app/src/CMakeLists.txt index 1a16cb60..0a755038 100644 --- a/app/src/CMakeLists.txt +++ b/app/src/CMakeLists.txt @@ -77,6 +77,14 @@ IF(BUILD_AS_BUNDLE AND (APPLE OR WIN32)) SET(qtconf_dest_dir Resources) SET(thirdparty_dest_dir MacOS) SET(APPS "\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/MacOS/${CMAKE_BUNDLE_NAME}") + + # Add the entitlement file to the target's properties + set(MACOS_ENTITLEMENTS_FILE "${CMAKE_CURRENT_SOURCE_DIR}/app.entitlements") + + # Set the entitlement file for the target + set_target_properties(rtabmap_app PROPERTIES + MACOSX_BUNDLE_INFO_PLIST "${MACOS_ENTITLEMENTS_FILE}" + ) ENDIF(APPLE) IF(OpenNI2_FOUND) diff --git a/app/src/app.entitlements b/app/src/app.entitlements new file mode 100644 index 00000000..8fd11c0e --- /dev/null +++ b/app/src/app.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.downloads.read-write + + com.apple.security.files.downloads.read-only + + com.apple.security.device.camera + + +s From 56aa26ce1d9f496405330b4efa776e368b193268 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 12 Oct 2025 15:48:50 -0700 Subject: [PATCH 035/126] MacOSX: fixed combined entitlements and bundle icon --- app/src/CMakeLists.txt | 8 ---- app/src/MacOSXBundleInfo.plist.in | 56 +++++++++++++++++++++++++ app/src/app.entitlements | 14 ------- cmake_modules/MacOSXBundleInfo.plist.in | 10 ++++- 4 files changed, 64 insertions(+), 24 deletions(-) create mode 100644 app/src/MacOSXBundleInfo.plist.in delete mode 100644 app/src/app.entitlements diff --git a/app/src/CMakeLists.txt b/app/src/CMakeLists.txt index 0a755038..1a16cb60 100644 --- a/app/src/CMakeLists.txt +++ b/app/src/CMakeLists.txt @@ -77,14 +77,6 @@ IF(BUILD_AS_BUNDLE AND (APPLE OR WIN32)) SET(qtconf_dest_dir Resources) SET(thirdparty_dest_dir MacOS) SET(APPS "\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/MacOS/${CMAKE_BUNDLE_NAME}") - - # Add the entitlement file to the target's properties - set(MACOS_ENTITLEMENTS_FILE "${CMAKE_CURRENT_SOURCE_DIR}/app.entitlements") - - # Set the entitlement file for the target - set_target_properties(rtabmap_app PROPERTIES - MACOSX_BUNDLE_INFO_PLIST "${MACOS_ENTITLEMENTS_FILE}" - ) ENDIF(APPLE) IF(OpenNI2_FOUND) diff --git a/app/src/MacOSXBundleInfo.plist.in b/app/src/MacOSXBundleInfo.plist.in new file mode 100644 index 00000000..7d5f67f8 --- /dev/null +++ b/app/src/MacOSXBundleInfo.plist.in @@ -0,0 +1,56 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleGetInfoString + ${MACOSX_BUNDLE_INFO_STRING} + CFBundleIconFile + ${MACOSX_BUNDLE_ICON_FILE} + CFBundleIdentifier + ${MACOSX_BUNDLE_GUI_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLongVersionString + ${MACOSX_BUNDLE_LONG_VERSION_STRING} + CFBundleName + ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + ${MACOSX_BUNDLE_SHORT_VERSION_STRING} + CFBundleSignature + ???? + CFBundleVersion + ${MACOSX_BUNDLE_BUNDLE_VERSION} + CSResourcesFileMapped + + LSRequiresCarbon + + NSHumanReadableCopyright + ${MACOSX_BUNDLE_COPYRIGHT} + + +CFBundleDocumentTypes + + + CFBundleTypeExtensions + + db + + + + CFBundleTypeName + RTAB-Map Database + CFBundleTypeRole + Editor + LSIsAppleDefaultForType + Yes + + + + + diff --git a/app/src/app.entitlements b/app/src/app.entitlements deleted file mode 100644 index 8fd11c0e..00000000 --- a/app/src/app.entitlements +++ /dev/null @@ -1,14 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.files.downloads.read-write - - com.apple.security.files.downloads.read-only - - com.apple.security.device.camera - - -s diff --git a/cmake_modules/MacOSXBundleInfo.plist.in b/cmake_modules/MacOSXBundleInfo.plist.in index 7d5f67f8..322b94f9 100644 --- a/cmake_modules/MacOSXBundleInfo.plist.in +++ b/cmake_modules/MacOSXBundleInfo.plist.in @@ -10,8 +10,6 @@ ${MACOSX_BUNDLE_INFO_STRING} CFBundleIconFile ${MACOSX_BUNDLE_ICON_FILE} - CFBundleIdentifier - ${MACOSX_BUNDLE_GUI_IDENTIFIER} CFBundleInfoDictionaryVersion 6.0 CFBundleLongVersionString @@ -32,6 +30,14 @@ NSHumanReadableCopyright ${MACOSX_BUNDLE_COPYRIGHT} + com.apple.security.app-sandbox + + com.apple.security.files.downloads.read-write + + com.apple.security.files.downloads.read-only + + com.apple.security.device.camera + CFBundleDocumentTypes From 4e5a0944915ae52aebf398236682eb2b314f7911 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 12 Oct 2025 16:28:25 -0700 Subject: [PATCH 036/126] cleanup --- app/src/MacOSXBundleInfo.plist.in | 56 ------------------------------- 1 file changed, 56 deletions(-) delete mode 100644 app/src/MacOSXBundleInfo.plist.in diff --git a/app/src/MacOSXBundleInfo.plist.in b/app/src/MacOSXBundleInfo.plist.in deleted file mode 100644 index 7d5f67f8..00000000 --- a/app/src/MacOSXBundleInfo.plist.in +++ /dev/null @@ -1,56 +0,0 @@ - - - - - CFBundleDevelopmentRegion - English - CFBundleExecutable - ${MACOSX_BUNDLE_EXECUTABLE_NAME} - CFBundleGetInfoString - ${MACOSX_BUNDLE_INFO_STRING} - CFBundleIconFile - ${MACOSX_BUNDLE_ICON_FILE} - CFBundleIdentifier - ${MACOSX_BUNDLE_GUI_IDENTIFIER} - CFBundleInfoDictionaryVersion - 6.0 - CFBundleLongVersionString - ${MACOSX_BUNDLE_LONG_VERSION_STRING} - CFBundleName - ${MACOSX_BUNDLE_BUNDLE_NAME} - CFBundlePackageType - APPL - CFBundleShortVersionString - ${MACOSX_BUNDLE_SHORT_VERSION_STRING} - CFBundleSignature - ???? - CFBundleVersion - ${MACOSX_BUNDLE_BUNDLE_VERSION} - CSResourcesFileMapped - - LSRequiresCarbon - - NSHumanReadableCopyright - ${MACOSX_BUNDLE_COPYRIGHT} - - -CFBundleDocumentTypes - - - CFBundleTypeExtensions - - db - - - - CFBundleTypeName - RTAB-Map Database - CFBundleTypeRole - Editor - LSIsAppleDefaultForType - Yes - - - - - From 17fc6e34855957d42b75369b904838a2722827a2 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 12 Oct 2025 16:54:05 -0700 Subject: [PATCH 037/126] OrbbecSDK: install extensions folder when packaging --- app/src/CMakeLists.txt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/src/CMakeLists.txt b/app/src/CMakeLists.txt index 1a16cb60..a6600f70 100644 --- a/app/src/CMakeLists.txt +++ b/app/src/CMakeLists.txt @@ -119,6 +119,23 @@ IF(BUILD_AS_BUNDLE AND (APPLE OR WIN32)) COMPONENT runtime) ENDIF(WIN32) ENDIF(ZED_FOUND) + + IF(OrbbecSDK_FOUND) + # Install needed "extensions" folder + IF(WIN32) + find_path(OrbbecSDK_BIN_DIR NAMES OrbbecSDK.dll) + IF(NOT OrbbecSDK_BIN_DIR) + MESSAGE(FATAL_ERROR "OrbbecSDK.dll not found! Verify your PATH.") + ENDIF(NOT OrbbecSDK_BIN_DIR) + MESSAGE(FATAL "OrbbecSDK_BIN_DIR=${OrbbecSDK_BIN_DIR}") + INSTALL(DIRECTORY "${OrbbecSDK_BIN_DIR}/extensions" + DESTINATION ${thirdparty_dest_dir} + COMPONENT runtime + FILES_MATCHING + PATTERN "*.lib" EXCLUDE + PATTERN "*") + ENDIF(WIN32) + ENDIF(OrbbecSDK_FOUND) IF(Torch_FOUND) # Install needed cudnn_ops_infer64_8.dll and cudnn_cnn_infer64_8.dll From 1c8ac2b82b2b85a1a896dc4b26de5b15cd777698 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Mon, 13 Oct 2025 13:53:56 -0700 Subject: [PATCH 038/126] report: missing end of line in help --- tools/Report/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/Report/main.cpp b/tools/Report/main.cpp index 6548c4ac..29cfd837 100644 --- a/tools/Report/main.cpp +++ b/tools/Report/main.cpp @@ -62,7 +62,7 @@ void showUsage() " and compute error based on the scaled path.\n" " --poses Export odometry to [path]_odom.txt, optimized graph to [path]_slam.txt \n" " and ground truth to [path]_gt.txt in TUM RGB-D format.\n" - " --poses_raw Same as --poses, but poses are not aligned to gt." + " --poses_raw Same as --poses, but poses are not aligned to gt.\n" " --gt FILE.txt Use this file as ground truth (TUM RGB-D format). It will\n" " override the ground truth set in database if there is one.\n" " If extension is *.db, the optimized poses of that database will\n" From edebf180434202b4592d17f54e2c16510a51b2a9 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Mon, 13 Oct 2025 15:46:58 -0700 Subject: [PATCH 039/126] reprocess: handling Rtabmap/CreateIntermediateNodes when recomputing odometry --- tools/Report/main.cpp | 2 +- tools/Reprocess/main.cpp | 27 +++++++++++++++++---------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/tools/Report/main.cpp b/tools/Report/main.cpp index 29cfd837..88eea111 100644 --- a/tools/Report/main.cpp +++ b/tools/Report/main.cpp @@ -53,7 +53,7 @@ void showUsage() "[Not built with Qt, statistics cannot be plotted]\n" #endif " path Directory containing rtabmap databases or path of a database.\n" - " Options:" + " Options:\n" " --latex Print table formatted in LaTeX with results.\n" " --kitti Compute error based on KITTI benchmark.\n" " --relative Compute relative motion error between poses.\n" diff --git a/tools/Reprocess/main.cpp b/tools/Reprocess/main.cpp index 6ce14289..867f6381 100644 --- a/tools/Reprocess/main.cpp +++ b/tools/Reprocess/main.cpp @@ -73,9 +73,8 @@ void showUsage() " -default Input database's parameters are ignored, using default ones instead.\n" " -odom Recompute odometry. See \"Odom/\" parameters with --params. If -skip option\n" " is used, it will be applied to odometry frames, not rtabmap frames. Multi-session\n" - " cannot be detected in this mode (assuming the database contains continuous frames\n" - " of a single session).\n" - " -odom_input_guess Forward input database's odometry (if exists) as guess when recompting odometry.\n" + " may not be detected correctly if the input covariance between sessions doesn't have 9999.\n" + " -odom_input_guess Forward input database's odometry (if exists) as guess when recomputing odometry.\n" " -odom_lin_var #.# Override computed odometry linear covariance.\n" " -odom_ang_var #.# Override computed odometry angular covariance.\n" " -start # Start from this node ID.\n" @@ -1026,19 +1025,27 @@ int main(int argc, char * argv[]) } } - data = dbReader->takeData(&info); - if(scanFromDepth) + if(framesToSkip==0 && intermediateNodes) { - data.setLaserScan(LaserScan()); + data.setId(-1); // intermediate node + } + else + { + data = dbReader->takeData(&info); + if(scanFromDepth) + { + data.setLaserScan(LaserScan()); + } + camThread.postUpdate(&data, &info); + ++processed; + continue; } - camThread.postUpdate(&data, &info); - ++processed; - continue; } info.odomPose = pose; info.odomCovariance = odomCovariance; odomCovariance = cv::Mat(); - lastUpdateStamp = data.stamp(); + if(data.id() != -1) + lastUpdateStamp = data.stamp(); uInsert(globalMapStats, odomInfo.statistics(pose)); } From 5cc16e371c969be1c5f0ca5859913b9cc845fe2f Mon Sep 17 00:00:00 2001 From: matlabbe Date: Mon, 13 Oct 2025 22:54:35 -0700 Subject: [PATCH 040/126] 0.23.2: make g2o sba type check more portable (vcpkg build issue) (#1597) * make g2o sba type check more portable * try fixing latest g2o build --- CMakeLists.txt | 43 ++++++++++++++------------ Version.h.in | 1 + cmake_modules/FindG2O.cmake | 33 +++++++++++++------- corelib/src/optimizer/OptimizerG2O.cpp | 8 ++--- 4 files changed, 48 insertions(+), 37 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 24c86f42..72ac0d98 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,7 +20,7 @@ SET(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake_modules") ####################### SET(RTABMAP_MAJOR_VERSION 0) SET(RTABMAP_MINOR_VERSION 23) -SET(RTABMAP_PATCH_VERSION 1) +SET(RTABMAP_PATCH_VERSION 2) SET(RTABMAP_VERSION ${RTABMAP_MAJOR_VERSION}.${RTABMAP_MINOR_VERSION}.${RTABMAP_PATCH_VERSION}) @@ -498,35 +498,34 @@ ENDIF(WITH_DC1394) IF(WITH_G2O) FIND_PACKAGE(g2o NO_MODULE) IF(g2o_FOUND) - MESSAGE(STATUS "Found g2o (targets)") - SET(G2O_FOUND ${g2o_FOUND}) - get_target_property(G2O_INCLUDES g2o::core INTERFACE_INCLUDE_DIRECTORIES) - MESSAGE(STATUS "g2o include dir: ${G2O_INCLUDES}") - FIND_FILE(G2O_FACTORY_FILE g2o/core/factory.h + MESSAGE(STATUS "Found g2o (targets)") + SET(G2O_FOUND ${g2o_FOUND}) + get_target_property(G2O_INCLUDES g2o::core INTERFACE_INCLUDE_DIRECTORIES) + MESSAGE(STATUS "g2o include dir: ${G2O_INCLUDES}") + FIND_FILE(G2O_FACTORY_FILE g2o/core/factory.h PATHS ${G2O_INCLUDES} NO_DEFAULT_PATH) - FILE(READ ${G2O_FACTORY_FILE} TMPTXT) - STRING(FIND "${TMPTXT}" "shared_ptr" matchres) - IF(${matchres} EQUAL -1) + FILE(READ ${G2O_FACTORY_FILE} TMPTXT) + STRING(FIND "${TMPTXT}" "shared_ptr" matchres) + IF(${matchres} EQUAL -1) MESSAGE(STATUS "Old g2o factory version detected without shared ptr (factory file: ${G2O_FACTORY_FILE}).") SET(G2O_CPP11 2) - ELSE() + ELSE() MESSAGE(STATUS "Latest g2o factory version detected with shared ptr (factory file: ${G2O_FACTORY_FILE}).") SET(G2O_CPP11 1) - ENDIF() + ENDIF() + FIND_FILE(G2O_SBA_UTILS_FILE g2o/types/sba/sba_utils.h + PATHS ${G2O_INCLUDES} + NO_DEFAULT_PATH) + IF(G2O_SBA_UTILS_FILE) + SET(G2O_WITH_SBA_UTILS 1) + ELSE() + SET(G2O_WITH_SBA_UTILS 0) + ENDIF() ELSE() FIND_PACKAGE(G2O QUIET) IF(G2O_FOUND) MESSAGE(STATUS "Found g2o: ${G2O_INCLUDE_DIRS}") - FIND_FILE(G2O_FACTORY_FILE g2o/core/factory.h - PATHS ${G2O_INCLUDES} - NO_DEFAULT_PATH) - FILE(READ ${G2O_FACTORY_FILE} TMPTXT) - STRING(FIND "${TMPTXT}" "shared_ptr" matchres) - IF(NOT ${matchres} EQUAL -1) - MESSAGE(STATUS "Latest g2o factory version detected with shared ptr (factory file: ${G2O_FACTORY_FILE}).") - SET(G2O_CPP11 1) - ENDIF() ENDIF(G2O_FOUND) ENDIF() ENDIF(WITH_G2O) @@ -963,10 +962,14 @@ ENDIF() IF(NOT G2O_FOUND) SET(G2O "//") SET(G2O_CPP_CONF "//") + SET(G2O_WITH_SBA_UTILS "//") ELSE() IF(NOT G2O_CPP11) SET(G2O_CPP_CONF "//") ENDIF(NOT G2O_CPP11) + IF(NOT G2O_WITH_SBA_UTILS) + SET(G2O_WITH_SBA_UTILS_CONF "//") + ENDIF(NOT G2O_WITH_SBA_UTILS) ENDIF() IF(NOT GTSAM_FOUND) SET(GTSAM "//") diff --git a/Version.h.in b/Version.h.in index dc7ac90d..faebf7c2 100644 --- a/Version.h.in +++ b/Version.h.in @@ -41,6 +41,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. @TORO@#define RTABMAP_TORO @G2O@#define RTABMAP_G2O @G2O_CPP_CONF@#define RTABMAP_G2O_CPP11 @G2O_CPP11@ +@G2O_WITH_SBA_UTILS_CONF@#define RTABMAP_G2O_WITH_SBA_UTILS @GTSAM@#define RTABMAP_GTSAM @CERES@#define RTABMAP_CERES @MRPT@#define RTABMAP_MRPT diff --git a/cmake_modules/FindG2O.cmake b/cmake_modules/FindG2O.cmake index ffc9c9a7..3adc2c4d 100644 --- a/cmake_modules/FindG2O.cmake +++ b/cmake_modules/FindG2O.cmake @@ -26,6 +26,10 @@ FIND_FILE(G2O_FACTORY_FILE g2o/core/factory.h PATHS ${G2O_INCLUDE_DIR} NO_DEFAULT_PATH) +FIND_FILE(G2O_SBA_UTILS_FILE g2o/types/sba/sba_utils.h + PATHS ${G2O_INCLUDE_DIR} + NO_DEFAULT_PATH) + #ifdef G2O_NUMBER_FORMAT_STR #define G2O_CPP11 // we assume that if G2O_NUMBER_FORMAT_STR is defined, this is the new g2o code with c++11 interface #endif @@ -118,22 +122,29 @@ IF(G2O_STUFF_LIBRARY AND G2O_CORE_LIBRARY AND G2O_INCLUDE_DIR AND G2O_CONFIG_FIL ${CHOLMOD_LIB}) ENDIF(G2O_SOLVER_CHOLMOD) - FILE(READ ${G2O_CONFIG_FILE} TMPTXT) - STRING(FIND "${TMPTXT}" "G2O_NUMBER_FORMAT_STR" matchres) + FILE(READ ${G2O_FACTORY_FILE} TMPTXT) + STRING(FIND "${TMPTXT}" "shared_ptr" matchres) IF(${matchres} EQUAL -1) - MESSAGE(STATUS "Old g2o version detected with c++03 interface (config file: ${G2O_CONFIG_FILE}).") - SET(G2O_CPP11 0) - ELSE() - MESSAGE(WARNING "Latest g2o version detected with c++11 interface (config file: ${G2O_CONFIG_FILE}). Make sure g2o is built with \"-DBUILD_WITH_MARCH_NATIVE=OFF\" to avoid segmentation faults caused by Eigen.") - FILE(READ ${G2O_FACTORY_FILE} TMPTXT) - STRING(FIND "${TMPTXT}" "shared_ptr" matchres) + FILE(READ ${G2O_CONFIG_FILE} TMPTXT) + STRING(FIND "${TMPTXT}" "G2O_NUMBER_FORMAT_STR" matchres) IF(${matchres} EQUAL -1) + MESSAGE(STATUS "Old g2o version detected with c++03 interface (config file: ${G2O_CONFIG_FILE}).") + SET(G2O_CPP11 0) + ELSE() + MESSAGE(WARNING "Latest g2o version detected with c++11 interface (config file: ${G2O_CONFIG_FILE}). Make sure g2o is built with \"-DBUILD_WITH_MARCH_NATIVE=OFF\" to avoid segmentation faults caused by Eigen.") MESSAGE(STATUS "Old g2o factory version detected without shared ptr (factory file: ${G2O_FACTORY_FILE}).") SET(G2O_CPP11 2) - ELSE() - MESSAGE(STATUS "Latest g2o factory version detected with shared ptr (factory file: ${G2O_FACTORY_FILE}).") - SET(G2O_CPP11 1) ENDIF() + ELSE() + MESSAGE(WARNING "Latest g2o version detected with c++11 interface (config file: ${G2O_CONFIG_FILE}). Make sure g2o is built with \"-DBUILD_WITH_MARCH_NATIVE=OFF\" to avoid segmentation faults caused by Eigen.") + MESSAGE(STATUS "Latest g2o factory version detected with shared ptr (factory file: ${G2O_FACTORY_FILE}).") + SET(G2O_CPP11 1) + ENDIF() + + IF(G2O_SBA_UTILS_FILE) + SET(G2O_WITH_SBA_UTILS 1) + ELSE() + SET(G2O_WITH_SBA_UTILS 0) ENDIF() SET(G2O_FOUND "YES") diff --git a/corelib/src/optimizer/OptimizerG2O.cpp b/corelib/src/optimizer/OptimizerG2O.cpp index cdf5619c..41408c24 100644 --- a/corelib/src/optimizer/OptimizerG2O.cpp +++ b/corelib/src/optimizer/OptimizerG2O.cpp @@ -90,14 +90,10 @@ typedef g2o::LinearSolverCSparse SlamLinearCSpa typedef g2o::LinearSolverCholmod SlamLinearCholmodSolver; #endif -// We use G2O_SRC_DIR to know we are version after December 24 2020 +// We check if g2o/types/sba/sba_utils.h exists to know we use a version after December 24 2020 // where VertexSBAPointXYZ has been renamed to VertexPointXYZ // (g2o: 0fcccb302787e70ff19f65e70fb103a1295b33a2) -// -// VCPKG commented G2O_SRC_DIR from their port so we cannot use -// G2O_SRC_DIR on windows to deduce it, we then assume it is the -// latest version without VertexSBAPointXYZ -#if defined(G2O_SRC_DIR) or defined(WIN32) +#ifdef RTABMAP_G2O_WITH_SBA_UTILS namespace g2o { typedef VertexPointXYZ VertexSBAPointXYZ; } From 098db33547aead3ab69de2e9a5120444d69ba648 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sat, 18 Oct 2025 14:09:44 -0700 Subject: [PATCH 041/126] ios: updated rtabmap include path --- app/ios/RTABMapApp.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/ios/RTABMapApp.xcodeproj/project.pbxproj b/app/ios/RTABMapApp.xcodeproj/project.pbxproj index f571574a..9074aa74 100644 --- a/app/ios/RTABMapApp.xcodeproj/project.pbxproj +++ b/app/ios/RTABMapApp.xcodeproj/project.pbxproj @@ -1078,7 +1078,7 @@ "\"$(SRCROOT)/RTABMapApp/Libraries/include\"", "\"$(SRCROOT)/RTABMapApp/Libraries/include/eigen3\"", "\"$(SRCROOT)/RTABMapApp/Libraries/include/pcl-1.15\"", - "\"$(SRCROOT)/RTABMapApp/Libraries/include/rtabmap-0.22\"", + "\"$(SRCROOT)/RTABMapApp/Libraries/include/rtabmap-0.23\"", "\"$(SRCROOT)/../android/jni/tango-gl/include\"", "\"$(SRCROOT)/../android/jni/third-party/include\"", "\"$(SRCROOT)/RTABMapApp/Libraries/lib/vtk.framework/Headers\"", @@ -1139,7 +1139,7 @@ "\"$(SRCROOT)/RTABMapApp/Libraries/include\"", "\"$(SRCROOT)/RTABMapApp/Libraries/include/eigen3\"", "\"$(SRCROOT)/RTABMapApp/Libraries/include/pcl-1.15\"", - "\"$(SRCROOT)/RTABMapApp/Libraries/include/rtabmap-0.22\"", + "\"$(SRCROOT)/RTABMapApp/Libraries/include/rtabmap-0.23\"", "\"$(SRCROOT)/../android/jni/tango-gl/include\"", "\"$(SRCROOT)/../android/jni/third-party/include\"", "\"$(SRCROOT)/RTABMapApp/Libraries/lib/vtk.framework/Headers\"", From b9d7e8077c9ebe250dff7727d13a8ca08e61310f Mon Sep 17 00:00:00 2001 From: matlabbe Date: Mon, 20 Oct 2025 17:38:46 -0700 Subject: [PATCH 042/126] DbViewer: fixed new priors added 2 times --- guilib/src/DatabaseViewer.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/guilib/src/DatabaseViewer.cpp b/guilib/src/DatabaseViewer.cpp index beb808fb..33fa3452 100644 --- a/guilib/src/DatabaseViewer.cpp +++ b/guilib/src/DatabaseViewer.cpp @@ -9346,14 +9346,18 @@ std::multimap DatabaseViewer::updateLinksWithModifications( if(findIter!=linksRefined_.end()) { links.insert(*findIter); // add the refined link - links.insert(std::make_pair(findIter->second.to(), findIter->second.inverse())); // return both ways + if(findIter->second.from() != findIter->second.to()) { + links.insert(std::make_pair(findIter->second.to(), findIter->second.inverse())); // return both ways + } UDEBUG("Added refined link (%d->%d, %d)", findIter->second.from(), findIter->second.to(), findIter->second.type()); continue; } UDEBUG("Added link (%d->%d, %d)", iter->second.from(), iter->second.to(), iter->second.type()); links.insert(*iter); - links.insert(std::make_pair(iter->second.to(), iter->second.inverse())); // return both ways + if(iter->second.from() != iter->second.to()) { + links.insert(std::make_pair(iter->second.to(), iter->second.inverse())); // return both ways + } } return links; From 1def66857894cda75328001603ddd48300ddd048 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Mon, 20 Oct 2025 20:27:14 -0700 Subject: [PATCH 043/126] export: support --color_radius disabled (-1) --- tools/Export/main.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/Export/main.cpp b/tools/Export/main.cpp index d89c7733..48575636 100644 --- a/tools/Export/main.cpp +++ b/tools/Export/main.cpp @@ -171,7 +171,7 @@ void showUsage() " --prop_radius_factor # Proportional radius filter factor (default 0, 0=disabled). Start tuning from 0.01.\n" " --prop_radius_scale # Proportional radius filter neighbor scale (default 2).\n" " --random_samples # Number of output samples using a random filter (default 0, 0=disabled).\n" - " --color_radius # Radius used to colorize polygons (default 0.05 m, 0 m with --scan). Set 0 for nearest color.\n" + " --color_radius # Radius used to colorize polygons (default 0.05 m, 0 m with --scan). Set 0 for nearest color, -1 to disable.\n" " --scan Use laser scan for the point cloud.\n" " --save_in_db Save resulting optimized poses, assembled point cloud, mesh or 2D occupancy grid in the database.\n" " --xmin # Minimum range on X axis to keep nodes to export.\n" @@ -284,7 +284,7 @@ int main(int argc, char * argv[]) double multibandBestScoreThr = 0.1; double multibandAngleHardthr = 90; bool multibandForceVisible = false; - float colorRadius = -1.0f; + float colorRadius = -100.0f; int textureVertexColorPolicy = 0; bool cloudFromScan = false; bool saveInDb = false; @@ -1142,7 +1142,7 @@ int main(int argc, char * argv[]) { voxelSize = cloudFromScan?0:0.01f; } - if(colorRadius < 0.0f) + if(colorRadius < -1.0f) { colorRadius = cloudFromScan?0:0.05f; } From 210aadb92bde47a5f594096c76742a47667daa85 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Tue, 28 Oct 2025 20:56:54 -0700 Subject: [PATCH 044/126] RGBD/LinearUpdate and RGBD/AngularUpdate: updated description and now require both to be 0 to skip motion check. --- corelib/include/rtabmap/core/Parameters.h | 4 ++-- corelib/src/Rtabmap.cpp | 7 ++++--- corelib/src/optimizer/OptimizerG2O.cpp | 5 ++++- guilib/src/ui/preferencesDialog.ui | 10 +++++----- tools/EurocDataset/main.cpp | 1 + tools/KittiDataset/main.cpp | 1 + tools/RgbdDataset/main.cpp | 1 + 7 files changed, 18 insertions(+), 11 deletions(-) diff --git a/corelib/include/rtabmap/core/Parameters.h b/corelib/include/rtabmap/core/Parameters.h index 6284d2cb..74d73476 100644 --- a/corelib/include/rtabmap/core/Parameters.h +++ b/corelib/include/rtabmap/core/Parameters.h @@ -362,8 +362,8 @@ class RTABMAP_CORE_EXPORT Parameters // RGB-D SLAM RTABMAP_PARAM(RGBD, Enabled, bool, true, "Activate metric SLAM. If set to false, classic RTAB-Map loop closure detection is done using only images and without any metric information."); - RTABMAP_PARAM(RGBD, LinearUpdate, float, 0.1, "Minimum linear displacement (m) to update the map. Rehearsal is done prior to this, so weights are still updated."); - RTABMAP_PARAM(RGBD, AngularUpdate, float, 0.1, "Minimum angular displacement (rad) to update the map. Rehearsal is done prior to this, so weights are still updated."); + RTABMAP_PARAM(RGBD, LinearUpdate, float, 0.1, uFormat("Minimum linear displacement (m) to update the map. Rehearsal is done prior to this, so weights are still updated. To update the map when not moving, both %s and %s should be set to 0.", Parameters::kRGBDLinearUpdate().c_str(), Parameters::kRGBDAngularUpdate().c_str())); + RTABMAP_PARAM(RGBD, AngularUpdate, float, 0.1, uFormat("Minimum angular displacement (rad) to update the map. Rehearsal is done prior to this, so weights are still updated. To update the map when not moving, both %s and %s should be set to 0.", Parameters::kRGBDLinearUpdate().c_str(), Parameters::kRGBDAngularUpdate().c_str())); RTABMAP_PARAM(RGBD, LinearSpeedUpdate, float, 0.0, "Maximum linear speed (m/s) to update the map (0 means not limit)."); RTABMAP_PARAM(RGBD, AngularSpeedUpdate, float, 0.0, "Maximum angular speed (rad/s) to update the map (0 means not limit)."); RTABMAP_PARAM(RGBD, AggressiveLoopThr, float, 0.05, uFormat("Loop closure threshold used (overriding %s) when a new mapping session is not yet linked to a map of the highest loop closure hypothesis. In localization mode, this threshold is used when there are no loop closure constraints with any map in the cache (%s). In all cases, the goal is to aggressively loop on a previous map in the database. Only used when %s is enabled. Set 1 to disable.", kRtabmapLoopThr().c_str(), kRGBDMaxOdomCacheSize().c_str(), kRGBDEnabled().c_str())); diff --git a/corelib/src/Rtabmap.cpp b/corelib/src/Rtabmap.cpp index 18b5fc55..270cc8c3 100644 --- a/corelib/src/Rtabmap.cpp +++ b/corelib/src/Rtabmap.cpp @@ -1544,9 +1544,10 @@ bool Rtabmap::process( { float x,y,z, roll,pitch,yaw; t.getTranslationAndEulerAngles(x,y,z, roll,pitch,yaw); - bool isMoving = fabs(x) > _rgbdLinearUpdate || - fabs(y) > _rgbdLinearUpdate || - fabs(z) > _rgbdLinearUpdate || + bool isMoving = (_rgbdLinearUpdate > 0.0f) && ( + fabs(x) > _rgbdLinearUpdate || + fabs(y) > _rgbdLinearUpdate || + fabs(z) > _rgbdLinearUpdate) || (_rgbdAngularUpdate>0.0f && ( fabs(roll) > _rgbdAngularUpdate || fabs(pitch) > _rgbdAngularUpdate || diff --git a/corelib/src/optimizer/OptimizerG2O.cpp b/corelib/src/optimizer/OptimizerG2O.cpp index 41408c24..1fbb852d 100644 --- a/corelib/src/optimizer/OptimizerG2O.cpp +++ b/corelib/src/optimizer/OptimizerG2O.cpp @@ -216,7 +216,7 @@ std::map OptimizerG2O::optimize( outputCovariance = cv::Mat::eye(6,6,CV_64FC1); std::map optimizedPoses; #ifdef RTABMAP_G2O - UDEBUG("Optimizing graph..."); + UDEBUG("Optimizing graph... (rootId=%d)", rootId); #ifndef RTABMAP_VERTIGO if(this->isRobust()) @@ -348,6 +348,9 @@ std::map OptimizerG2O::optimize( { if(!priorsIgnored() && iter->second.type() == Link::kPosePrior) { + if(rootId!=0) { + UDEBUG("Removed rootId=%d because there are priors."); + } rootId = 0; break; } diff --git a/guilib/src/ui/preferencesDialog.ui b/guilib/src/ui/preferencesDialog.ui index 8cc3b2a1..fc04f50f 100644 --- a/guilib/src/ui/preferencesDialog.ui +++ b/guilib/src/ui/preferencesDialog.ui @@ -63,7 +63,7 @@ 0 - -641 + 0 713 4779 @@ -95,7 +95,7 @@ QFrame::Raised - 5 + 12 @@ -13141,7 +13141,7 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - Linear update: Minimum linear displacement to update the map. Note that Weight Update is done prior to this, so weights are still updated. + Linear update: Minimum linear displacement to update the map. Note that Weight Update is done prior to this, so weights are still updated. To update the map when not moving, both linear and angular update parameters should be 0. true @@ -13231,7 +13231,7 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - Angular update: Minimum angular displacement to update the map. Note that Weight Update is done prior to this, so weights are still updated. + Angular update: Minimum angular displacement to update the map. Note that Weight Update is done prior to this, so weights are still updated. To update the map when not moving, both linear and angular update parameters should be 0. true @@ -13303,7 +13303,7 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - Maximum linear speed to update the map (0 means not limit). + Maximum linear speed to update the map (0 means not limit). true diff --git a/tools/EurocDataset/main.cpp b/tools/EurocDataset/main.cpp index 34c4f1e5..4653d548 100644 --- a/tools/EurocDataset/main.cpp +++ b/tools/EurocDataset/main.cpp @@ -70,6 +70,7 @@ void showUsage() " --Rtabmap/PublishRAMUsage true\\\n" " --Rtabmap/DetectionRate 2\\\n" " --RGBD/LinearUpdate 0\\\n" + " --RGBD/AngularUpdate 0\\\n" " --Mem/STMSize 30\\\n" " ~/EuRoC/V1_03_difficult\n\n", rtabmap::Parameters::showUsage()); exit(1); diff --git a/tools/KittiDataset/main.cpp b/tools/KittiDataset/main.cpp index 6ba82e79..34fec543 100644 --- a/tools/KittiDataset/main.cpp +++ b/tools/KittiDataset/main.cpp @@ -73,6 +73,7 @@ void showUsage() " --Rtabmap/DetectionRate 2\\\n" " --Rtabmap/CreateIntermediateNodes true\\\n" " --RGBD/LinearUpdate 0\\\n" + " --RGBD/AngularUpdate 0\\\n" " --GFTT/QualityLevel 0.01\\\n" " --GFTT/MinDistance 7\\\n" " --OdomF2M/MaxSize 3000\\\n" diff --git a/tools/RgbdDataset/main.cpp b/tools/RgbdDataset/main.cpp index 74ed4b6d..57f2fa62 100644 --- a/tools/RgbdDataset/main.cpp +++ b/tools/RgbdDataset/main.cpp @@ -66,6 +66,7 @@ void showUsage() " --Rtabmap/PublishRAMUsage true\\\n" " --Rtabmap/DetectionRate 2\\\n" " --RGBD/LinearUpdate 0\\\n" + " --RGBD/AngularUpdate 0\\\n" " --Mem/STMSize 30\\\n" " ~/rgbd_dataset_freiburg3_long_office_household\n\n", rtabmap::Parameters::showUsage()); exit(1); From a3076392b5fc4273a6d42f03261bfc6281c02a58 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Tue, 28 Oct 2025 20:57:32 -0700 Subject: [PATCH 045/126] Fixed RGBD/LocalizationPriorError not used when adding external links --- corelib/src/Rtabmap.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/corelib/src/Rtabmap.cpp b/corelib/src/Rtabmap.cpp index 270cc8c3..4cb64718 100644 --- a/corelib/src/Rtabmap.cpp +++ b/corelib/src/Rtabmap.cpp @@ -6315,6 +6315,7 @@ bool Rtabmap::addLink(const Link & link) std::map poses = _odomCachePoses; std::multimap constraints = _odomCacheConstraints; constraints.insert(std::make_pair(link.from(), link)); + cv::Mat priorInfMat = cv::Mat::eye(6,6, CV_64FC1)*_localizationPriorInf; for(std::multimap::iterator iter=constraints.begin(); iter!=constraints.end(); ++iter) { std::map::iterator iterPose = _optimizedPoses.find(iter->second.to()); @@ -6322,7 +6323,7 @@ bool Rtabmap::addLink(const Link & link) { poses.insert(*iterPose); // make the poses in the map fixed - constraints.insert(std::make_pair(iterPose->first, Link(iterPose->first, iterPose->first, Link::kPosePrior, iterPose->second, cv::Mat::eye(6,6, CV_64FC1)*999999))); + constraints.insert(std::make_pair(iterPose->first, Link(iterPose->first, iterPose->first, Link::kPosePrior, iterPose->second, priorInfMat))); } } From b04f6078284b52ac163337ea3549a0019cfa6886 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sat, 1 Nov 2025 10:12:38 -0700 Subject: [PATCH 046/126] fixed warning --- corelib/src/Rtabmap.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/corelib/src/Rtabmap.cpp b/corelib/src/Rtabmap.cpp index 4cb64718..5bf76e83 100644 --- a/corelib/src/Rtabmap.cpp +++ b/corelib/src/Rtabmap.cpp @@ -1544,10 +1544,10 @@ bool Rtabmap::process( { float x,y,z, roll,pitch,yaw; t.getTranslationAndEulerAngles(x,y,z, roll,pitch,yaw); - bool isMoving = (_rgbdLinearUpdate > 0.0f) && ( + bool isMoving = (_rgbdLinearUpdate > 0.0f && ( fabs(x) > _rgbdLinearUpdate || fabs(y) > _rgbdLinearUpdate || - fabs(z) > _rgbdLinearUpdate) || + fabs(z) > _rgbdLinearUpdate)) || (_rgbdAngularUpdate>0.0f && ( fabs(roll) > _rgbdAngularUpdate || fabs(pitch) > _rgbdAngularUpdate || From a20e161a5b1d4ad1c658c8b5713dac0022758b13 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sat, 1 Nov 2025 13:07:50 -0700 Subject: [PATCH 047/126] Fixed a realsense2 camera crash on start. Refactored VINS-Fusion to always use rtabmap's calibration model no matter what (improved usability). Preferences/IMU: fixed publish inter imu checkbox disappearing if no imu filter is selected. --- corelib/src/SensorCaptureThread.cpp | 1 - corelib/src/camera/CameraRealSense2.cpp | 61 ++++--- corelib/src/odometry/OdometryVINSFusion.cpp | 191 ++++++++++++++------ guilib/src/PreferencesDialog.cpp | 3 +- guilib/src/ui/preferencesDialog.ui | 6 +- 5 files changed, 170 insertions(+), 92 deletions(-) diff --git a/corelib/src/SensorCaptureThread.cpp b/corelib/src/SensorCaptureThread.cpp index 8941c3d0..6414fb9f 100644 --- a/corelib/src/SensorCaptureThread.cpp +++ b/corelib/src/SensorCaptureThread.cpp @@ -960,7 +960,6 @@ void SensorCaptureThread::postUpdate(SensorData * dataPtr, SensorCaptureInfo * i { UASSERT(!data.imu().localTransform().isNull()); imu.convertToBaseFrame(); - } _imuFilter->update( imu.angularVelocity()[0], diff --git a/corelib/src/camera/CameraRealSense2.cpp b/corelib/src/camera/CameraRealSense2.cpp index 4ddfc775..5f986a4f 100644 --- a/corelib/src/camera/CameraRealSense2.cpp +++ b/corelib/src/camera/CameraRealSense2.cpp @@ -712,14 +712,14 @@ bool CameraRealSense2::init(const std::string & calibrationFolder, const std::st for (auto& profile : profiles) { auto video_profile = profile.as(); - UINFO("%s %d %d %d %d %s type=%d", rs2_format_to_string( - video_profile.format()), - video_profile.width(), - video_profile.height(), - video_profile.fps(), - video_profile.stream_index(), - video_profile.stream_name().c_str(), - video_profile.stream_type()); + UINFO("%s %d %d %d %d %s type=%d", + rs2_format_to_string(profile.format()), + video_profile.get()?video_profile.width():-1, + video_profile.get()?video_profile.height():-1, + profile.fps(), + profile.stream_index(), + profile.stream_name().c_str(), + profile.stream_type()); } } int pi = 0; @@ -728,7 +728,8 @@ bool CameraRealSense2::init(const std::string & calibrationFolder, const std::st auto video_profile = profile.as(); if(!stereo) { - if( (video_profile.width() == cameraWidth_ && + if( (video_profile.get() && + video_profile.width() == cameraWidth_ && video_profile.height() == cameraHeight_ && video_profile.fps() == cameraFps_) || (strcmp(sensors[i].get_info(RS2_CAMERA_INFO_NAME), "L500 Depth Sensor")==0 && @@ -778,7 +779,7 @@ bool CameraRealSense2::init(const std::string & calibrationFolder, const std::st } } } - else if(video_profile.format() == RS2_FORMAT_MOTION_XYZ32F || video_profile.format() == RS2_FORMAT_6DOF) + else if(profile.format() == RS2_FORMAT_MOTION_XYZ32F || profile.format() == RS2_FORMAT_6DOF) { //D435i: //MOTION_XYZ32F 0 0 200 (gyro) @@ -817,6 +818,7 @@ bool CameraRealSense2::init(const std::string & calibrationFolder, const std::st { //T265: if(!dualMode_ && + video_profile.get() && video_profile.format() == RS2_FORMAT_Y8 && video_profile.width() == 848 && video_profile.height() == 800 && @@ -865,7 +867,7 @@ bool CameraRealSense2::init(const std::string & calibrationFolder, const std::st } added = true; } - else if(video_profile.format() == RS2_FORMAT_MOTION_XYZ32F || video_profile.format() == RS2_FORMAT_6DOF) + else if(profile.format() == RS2_FORMAT_MOTION_XYZ32F || profile.format() == RS2_FORMAT_6DOF) { //MOTION_XYZ32F 0 0 200 //MOTION_XYZ32F 0 0 62 @@ -884,14 +886,14 @@ bool CameraRealSense2::init(const std::string & calibrationFolder, const std::st for (auto& profile : profiles) { auto video_profile = profile.as(); - UERROR("%s %d %d %d %d %s type=%d", rs2_format_to_string( - video_profile.format()), - video_profile.width(), - video_profile.height(), - video_profile.fps(), - video_profile.stream_index(), - video_profile.stream_name().c_str(), - video_profile.stream_type()); + UERROR("%s %d %d %d %d %s type=%d", + rs2_format_to_string(profile.format()), + video_profile.get()?video_profile.width():-1, + video_profile.get()?video_profile.height():-1, + profile.fps(), + profile.stream_index(), + profile.stream_name().c_str(), + profile.stream_type()); } return false; } @@ -1075,13 +1077,13 @@ bool CameraRealSense2::init(const std::string & calibrationFolder, const std::st { auto video_profile = profilesPerSensor[i][j].as(); UINFO("Opening: %s %d %d %d %d %s type=%d", rs2_format_to_string( - video_profile.format()), - video_profile.width(), - video_profile.height(), - video_profile.fps(), - video_profile.stream_index(), - video_profile.stream_name().c_str(), - video_profile.stream_type()); + profilesPerSensor[i][j].format()), + video_profile.get()?video_profile.width():-1, + video_profile.get()?video_profile.height():-1, + profilesPerSensor[i][j].fps(), + profilesPerSensor[i][j].stream_index(), + profilesPerSensor[i][j].stream_name().c_str(), + profilesPerSensor[i][j].stream_type()); } if(globalTimeSync_ && sensors[i].supports(rs2_option::RS2_OPTION_GLOBAL_TIME_ENABLED)) { @@ -1525,6 +1527,13 @@ SensorData CameraRealSense2::captureImage(SensorCaptureInfo * info) else { UERROR("Missing frames (received %d, needed=%d)", (int)frameset.size(), desiredFramesetSize); + if(frameset.size()>0) + { + for (auto it = frameset.begin(); it != frameset.end(); ++it) + { + UERROR("Received frame only from %s", (*it).get_profile().stream_name().c_str()); + } + } } } catch(const std::exception& ex) diff --git a/corelib/src/odometry/OdometryVINSFusion.cpp b/corelib/src/odometry/OdometryVINSFusion.cpp index 93df6353..9c633605 100644 --- a/corelib/src/odometry/OdometryVINSFusion.cpp +++ b/corelib/src/odometry/OdometryVINSFusion.cpp @@ -39,7 +39,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include -#include +#include #include #endif @@ -49,10 +49,12 @@ namespace rtabmap { class VinsFusionEstimator: public Estimator { public: - VinsFusionEstimator( - const Transform & imuLocalTransform, + VinsFusionEstimator() : Estimator() + {} + + bool init(const Transform & imuLocalTransform, const StereoCameraModel & model, - bool rectified) : Estimator() + bool rectified) { MULTIPLE_THREAD = 0; setParameter(); @@ -63,71 +65,133 @@ public: //overwrite camera calibration only if received model is radtan, otherwise use config UASSERT(NUM_OF_CAM >= 1 && NUM_OF_CAM <=2); - if( (NUM_OF_CAM == 2 && model.left().D_raw().cols == 4 && model.right().D_raw().cols == 4) || - (NUM_OF_CAM == 1 && model.left().D_raw().cols == 4)) + if( (NUM_OF_CAM == 2 && (rectified || (model.left().D_raw().cols >= 4 && model.right().D_raw().cols >= 4))) || + (NUM_OF_CAM == 1 && (rectified || model.left().D_raw().cols >= 4))) { - UWARN("Overwriting VINS camera calibration config with received pinhole model... rectified=%d", rectified?1:0); + UINFO("Setting up VINS camera calibration config with received pinhole model... rectified=%d distortion coefficients=%d", + rectified?1:0, model.left().D_raw().cols); featureTracker.m_camera.clear(); - camodocal::PinholeCameraPtr camera( new camodocal::PinholeCamera ); - camodocal::PinholeCamera::Parameters params( - model.name(), - model.left().imageWidth(), model.left().imageHeight(), - rectified?0:model.left().D_raw().at(0,0), - rectified?0:model.left().D_raw().at(0,1), - rectified?0:model.left().D_raw().at(0,2), - rectified?0:model.left().D_raw().at(0,3), - rectified?model.left().fx():model.left().K_raw().at(0,0), - rectified?model.left().fy():model.left().K_raw().at(1,1), - rectified?model.left().cx():model.left().K_raw().at(0,2), - rectified?model.left().cy():model.left().K_raw().at(1,2)); - camera->setParameters(params); - featureTracker.m_camera.push_back(camera); + double fx = 0.0; + if(!rectified && model.left().D_raw().cols >= 8) + { + if(model.left().D_raw().cols > 8) + { + UWARN("Received %d distortion coefficients, but only the first 8 are supported, ignoring the last coefficents.", + model.left().D_raw().cols); + } + camodocal::PinholeFullCameraPtr camera( new camodocal::PinholeFullCamera ); + camodocal::PinholeFullCamera::Parameters params( + model.name(), + model.left().imageWidth(), model.left().imageHeight(), + model.left().D_raw().at(0,0), // k1 + model.left().D_raw().at(0,1), // k1 + model.left().D_raw().at(0,4), // k3 + model.left().D_raw().at(0,5), // k4 + model.left().D_raw().at(0,6), // k5 + model.left().D_raw().at(0,7), // k6 + model.left().D_raw().at(0,2), // p1 + model.left().D_raw().at(0,3), // p1 + model.left().K_raw().at(0,0), // fx + model.left().K_raw().at(1,1), // fy + model.left().K_raw().at(0,2), // cx + model.left().K_raw().at(1,2)); // cy + camera->setParameters(params); + featureTracker.m_camera.push_back(camera); + fx = params.fx(); + if(NUM_OF_CAM == 2) + { + UASSERT(model.left().D_raw().cols == model.right().D_raw().cols); + camodocal::PinholeFullCameraPtr camera2( new camodocal::PinholeFullCamera ); + camodocal::PinholeFullCamera::Parameters params2( + model.name(), + model.right().imageWidth(), model.right().imageHeight(), + model.right().D_raw().at(0,0), // k1 + model.right().D_raw().at(0,1), // k2 + model.right().D_raw().at(0,4), // k3 + model.right().D_raw().at(0,5), // k4 + model.right().D_raw().at(0,6), // k5 + model.right().D_raw().at(0,7), // k6 + model.right().D_raw().at(0,2), // p1 + model.right().D_raw().at(0,3), // p2 + model.right().K_raw().at(0,0), // fx + model.right().K_raw().at(1,1), // fy + model.right().K_raw().at(0,2), // cx + model.right().K_raw().at(1,2)); // cy + camera2->setParameters(params2); + featureTracker.m_camera.push_back(camera2); + } + } + else + { + if(!rectified) + { + if(model.left().D_raw().cols == 6) { + UERROR("Fisheye camera model support not implemented! Provide rectified images instead (see %s).", + Parameters::kRtabmapImagesAlreadyRectified().c_str()); + return false; + } + if(model.left().D_raw().cols > 4) + { + UWARN("Received %d distortion coefficients, but only 4 or 8 are supported, ignoring the last coefficents.", + model.left().D_raw().cols); + } + } + + camodocal::PinholeCameraPtr camera( new camodocal::PinholeCamera ); + camodocal::PinholeCamera::Parameters params( + model.name(), + model.left().imageWidth(), model.left().imageHeight(), + rectified?0:model.left().D_raw().at(0,0), // k1 + rectified?0:model.left().D_raw().at(0,1), // k2 + rectified?0:model.left().D_raw().at(0,2), // p1 + rectified?0:model.left().D_raw().at(0,3), // p2 + rectified?model.left().fx():model.left().K_raw().at(0,0), + rectified?model.left().fy():model.left().K_raw().at(1,1), + rectified?model.left().cx():model.left().K_raw().at(0,2), + rectified?model.left().cy():model.left().K_raw().at(1,2)); + camera->setParameters(params); + featureTracker.m_camera.push_back(camera); + fx = params.fx(); + if(NUM_OF_CAM == 2) + { + UASSERT(model.left().D_raw().cols == model.right().D_raw().cols); + camodocal::PinholeCameraPtr camera2( new camodocal::PinholeCamera ); + camodocal::PinholeCamera::Parameters params2( + model.name(), + model.right().imageWidth(), model.right().imageHeight(), + rectified?0:model.right().D_raw().at(0,0), // k1 + rectified?0:model.right().D_raw().at(0,1), // k2 + rectified?0:model.right().D_raw().at(0,2), // p1 + rectified?0:model.right().D_raw().at(0,3), // p2 + rectified?model.right().fx():model.right().K_raw().at(0,0), + rectified?model.right().fy():model.right().K_raw().at(1,1), + rectified?model.right().cx():model.right().K_raw().at(0,2), + rectified?model.right().cy():model.right().K_raw().at(1,2)); + camera2->setParameters(params2); + featureTracker.m_camera.push_back(camera2); + } + } double originalParalax = MIN_PARALLAX * FOCAL_LENGTH; - // If you have compiler error about FOCAL_LENGTH being const, make sure to use the following patch: + // If you have compiler error about FOCAL_LENGTH being const, make sure to use the following patch for ROS1: // https://gist.github.com/matlabbe/795ab37067367dca58bbadd8201d986c#file-vins-fusion_pull136-patch - FOCAL_LENGTH = params.fx(); + // Use this patch for ROS2: https://gist.github.com/matlabbe/ebbb343cd744da9d6d6d6ded2e1557fd + FOCAL_LENGTH = fx; MIN_PARALLAX = originalParalax / FOCAL_LENGTH; ProjectionTwoFrameOneCamFactor::sqrt_info = FOCAL_LENGTH / 1.5 * Matrix2d::Identity(); ProjectionTwoFrameTwoCamFactor::sqrt_info = FOCAL_LENGTH / 1.5 * Matrix2d::Identity(); ProjectionOneFrameTwoCamFactor::sqrt_info = FOCAL_LENGTH / 1.5 * Matrix2d::Identity(); - - if(NUM_OF_CAM == 2) - { - camodocal::PinholeCameraPtr camera( new camodocal::PinholeCamera ); - camodocal::PinholeCamera::Parameters params( - model.name(), - model.right().imageWidth(), model.right().imageHeight(), - rectified?0:model.right().D_raw().at(0,0), - rectified?0:model.right().D_raw().at(0,1), - rectified?0:model.right().D_raw().at(0,2), - rectified?0:model.right().D_raw().at(0,3), - rectified?model.right().fx():model.right().K_raw().at(0,0), - rectified?model.right().fy():model.right().K_raw().at(1,1), - rectified?model.right().cx():model.right().K_raw().at(0,2), - rectified?model.right().cy():model.right().K_raw().at(1,2)); - camera->setParameters(params); - featureTracker.m_camera.push_back(camera); - } } - else if(rectified) + else { - UWARN("Images are rectified but received calibration cannot be " - "used, make sure calibration in config file doesn't have " - "distortion or send raw images to VINS odometry."); - if(!featureTracker.m_camera.empty()) - { - if(featureTracker.m_camera.front()->imageWidth() != model.left().imageWidth() || - featureTracker.m_camera.front()->imageHeight() != model.left().imageHeight()) - { - UERROR("Received images don't have same size (%dx%d) than in the config file (%dx%d)!", - model.left().imageWidth(), - model.left().imageHeight(), - featureTracker.m_camera.front()->imageWidth(), - featureTracker.m_camera.front()->imageHeight()); - } + UERROR("Received stereo camera model is not compatible with VINS-Fusion."); + if(!rectified && model.left().D_raw().cols != 4) { + UERROR("When raw images are provided (%s=false), we expect 4 distortion coefficients (k1,k2,p1,p2), received %d", + Parameters::kRtabmapImagesAlreadyRectified().c_str(), + model.left().D_raw().cols); } + return false; } Transform imuCam0 = imuLocalTransform.inverse() * model.localTransform(); @@ -165,6 +229,7 @@ public: cout << " new intrinsic cam " << i << endl << featureTracker.m_camera[i]->parametersToString() << endl; } f_manager.setRic(ric); + return true; } // Copy of original inputImage() so that overridden processMeasurements() is used and threading is disabled. @@ -350,10 +415,16 @@ Transform OdometryVINSFusion::computeTransform( { // intialize UINFO("Initializing with image %f", data.stamp()); - vinsEstimator_ = new VinsFusionEstimator( - lastImu_.localTransform().isNull()?Transform::getIdentity():lastImu_.localTransform(), - data.stereoCameraModels()[0], - this->imagesAlreadyRectified()); + vinsEstimator_ = new VinsFusionEstimator(); + if(!vinsEstimator_->init( + lastImu_.localTransform().isNull()?Transform::getIdentity():lastImu_.localTransform(), + data.stereoCameraModels()[0], + this->imagesAlreadyRectified())) + { + delete vinsEstimator_; + vinsEstimator_ = 0; + return Transform(); + } if(USE_IMU) { double dx = lastImu_.linearAcceleration().val[0]; diff --git a/guilib/src/PreferencesDialog.cpp b/guilib/src/PreferencesDialog.cpp index e4581e93..d112f686 100644 --- a/guilib/src/PreferencesDialog.cpp +++ b/guilib/src/PreferencesDialog.cpp @@ -5868,7 +5868,6 @@ void PreferencesDialog::updateSourceGrpVisibility() (_ui->comboBox_sourceType->currentIndex() == 1 && _ui->comboBox_cameraStereo->currentIndex() == kSrcStereoMyntEye - kSrcStereo) || // MYNT EYE S (_ui->comboBox_sourceType->currentIndex() == 1 && _ui->comboBox_cameraStereo->currentIndex() == kSrcStereoZedOC - kSrcStereo) || (_ui->comboBox_sourceType->currentIndex() == 1 && _ui->comboBox_cameraStereo->currentIndex() == kSrcStereoDepthAI - kSrcStereo)); - _ui->frame_imu_filtering->setVisible(getIMUFilteringStrategy() > 0); // Not None _ui->stackedWidget_imuFilter->setVisible(_ui->comboBox_imuFilter_strategy->currentIndex() > 0); _ui->groupBox_madgwickfilter->setVisible(_ui->comboBox_imuFilter_strategy->currentIndex() == 1); _ui->groupBox_complementaryfilter->setVisible(_ui->comboBox_imuFilter_strategy->currentIndex() == 2); @@ -6706,7 +6705,7 @@ Camera * PreferencesDialog::createCamera( ((CameraOrbbecSDK*)camera)->enableColorRectification(_ui->checkBox_orbbec_sdk_color_rectification->isChecked()); ((CameraOrbbecSDK*)camera)->enableImu(_ui->checkBox_orbbec_sdk_imu->isChecked()); ((CameraOrbbecSDK*)camera)->enableDepthMM(_ui->checkBox_orbbec_sdk_depth_mm->isChecked()); - + camera->setInterIMUPublishing( _ui->checkbox_publishInterIMU->isChecked(), _ui->checkbox_publishInterIMU->isChecked() && getIMUFilteringStrategy()>0? diff --git a/guilib/src/ui/preferencesDialog.ui b/guilib/src/ui/preferencesDialog.ui index fc04f50f..f355ed6c 100644 --- a/guilib/src/ui/preferencesDialog.ui +++ b/guilib/src/ui/preferencesDialog.ui @@ -63,7 +63,7 @@ 0 - 0 + -2651 713 4779 @@ -95,7 +95,7 @@ QFrame::Raised - 12 + 5 @@ -8283,7 +8283,7 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - + Convert IMU in base frame before filtering. This can help to initialize correctly the yaw. From f44a4fc4786bd3c859a599718f42d3dee476ba8a Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 2 Nov 2025 17:53:01 -0800 Subject: [PATCH 048/126] GUI: disabling cuvslam from the detector comboboxes if not built with it. --- guilib/src/PreferencesDialog.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/guilib/src/PreferencesDialog.cpp b/guilib/src/PreferencesDialog.cpp index d112f686..ab272115 100644 --- a/guilib/src/PreferencesDialog.cpp +++ b/guilib/src/PreferencesDialog.cpp @@ -238,6 +238,9 @@ PreferencesDialog::PreferencesDialog(QWidget * parent) : #ifndef RTABMAP_OPEN3D _ui->odom_strategy->setItemData(12, 0, Qt::UserRole - 1); #endif +#ifndef RTABMAP_CUVSLAM + _ui->odom_strategy->setItemData(13, 0, Qt::UserRole - 1); +#endif #if CV_MAJOR_VERSION < 3 _ui->stereosgbm_mode->setItemData(2, 0, Qt::UserRole - 1); From 5e4fd171e2072a9699ffe80846f0e579aaa200dd Mon Sep 17 00:00:00 2001 From: Felix Toft Date: Fri, 7 Nov 2025 17:22:35 -0800 Subject: [PATCH 049/126] SuperPoint Rpautrat (MIT license) (#1603) * initial python implementation of superpoint rpautrat * working python implementation of superpoint * small tweaks to try and work around the GIL issue * fix missing os import * begging cpp impl of superpoint python model * finishing cpp superpoint impl using the same SPDetector interface * finalizing superpoint cpp impl, working with cpu but needs to be cleaned * fixing feature matching by reworking nms and filtering logic * speeding up nms with batched operations and cleaning up * updating conversion script * adding args for image dimensions and cuda usage to model tracer * wiring up UI to parse superpoint params * adding label to superpoint rpautrat ui * reverting some unintended ui changes * typo * oneline revert * removing nms and threshold filtering from cpp, this is handled internally by the superpoint model * using python interface to run the superpoint _to_torchscript.py script at runtime * generate and load model file on the first incoming frame * reverting unintented change * ui changes appear mysteriously again, reverting * remove topk from model to prevent issue when number of detected keypoints is lower then the k value (scripting fails in this case) * cleaning up for review * change dest for model file and remove debug logs * bump patch and add version comment * rm unucessary comment * use resources to load file to ensure it works when rtabmap isn't built from source * execute with pybind runpy instead of system call * only build superpoint rpautrat if we have torch and python support * changing param to accept a path to the weights .pth file directly * parse the default working directory to save the model file in * rm extraneous change * rm setters and re-initialize the detector whenever params change. We cannot support changing params after the model is constructed * update UI text * only enable superpoint rpautrat when built with python and torch * more build information regarding superpoint rpautrat * generate temporary python script in the working dir * rm unecessary changes to rtabmap_superpoint.py * fix comment and only add repo root to sys.path * introduce a new parameter for the superpoint python model definition * execute script from string instead of writing to a file * wrap parse params in a single compiler directive * remove descriptor spatial matching logic and rely on upstream RTAB-Map processes to take the top-K desc and kpts * only add python script to resources if built with superpoint rpautrat support. supress warning with type casting * isolate pybind11 setup so it can't affect any other modules or potential regnerations of the model file * update about dialog to show superpoint rpautrat * remove debug logs --------- Co-authored-by: Felix Toft --- CMakeLists.txt | 21 +- corelib/include/rtabmap/core/Features2d.h | 31 +- corelib/include/rtabmap/core/Parameters.h | 15 +- corelib/src/CMakeLists.txt | 19 +- corelib/src/Features2d.cpp | 112 +- corelib/src/Parameters.cpp | 7 + corelib/src/RegistrationVis.cpp | 4 + .../src/python/rtabmap_superpoint_rpautrat.py | 71 + .../SuperpointRpautrat.cpp | 230 ++ .../superpoint_rpautrat/SuperpointRpautrat.h | 52 + .../superpoint_to_torchscript.py | 107 + .../include/rtabmap/gui/PreferencesDialog.h | 2 + guilib/src/AboutDialog.cpp | 7 + guilib/src/PreferencesDialog.cpp | 49 + guilib/src/ui/aboutDialog.ui | 2347 +++++++++-------- guilib/src/ui/preferencesDialog.ui | 187 ++ 16 files changed, 2083 insertions(+), 1178 deletions(-) create mode 100644 corelib/src/python/rtabmap_superpoint_rpautrat.py create mode 100644 corelib/src/superpoint_rpautrat/SuperpointRpautrat.cpp create mode 100644 corelib/src/superpoint_rpautrat/SuperpointRpautrat.h create mode 100644 corelib/src/superpoint_rpautrat/superpoint_to_torchscript.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 72ac0d98..8ed56bcf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,7 +20,7 @@ SET(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake_modules") ####################### SET(RTABMAP_MAJOR_VERSION 0) SET(RTABMAP_MINOR_VERSION 23) -SET(RTABMAP_PATCH_VERSION 2) +SET(RTABMAP_PATCH_VERSION 3) SET(RTABMAP_VERSION ${RTABMAP_MAJOR_VERSION}.${RTABMAP_MINOR_VERSION}.${RTABMAP_PATCH_VERSION}) @@ -1453,15 +1453,26 @@ MESSAGE(STATUS " With ORB OcTree = NO (WITH_ORB_OCTREE=OFF)") ENDIF() IF(TORCH_FOUND) -MESSAGE(STATUS " With SuperPoint = YES (License: GPLv3) libtorch=${Torch_VERSION}") +MESSAGE(STATUS " With SuperPoint = YES (License: GPLv3) libtorch=${Torch_VERSION}") ELSEIF(NOT WITH_TORCH) -MESSAGE(STATUS " With SuperPoint = NO (WITH_TORCH=OFF)") +MESSAGE(STATUS " With SuperPoint = NO (WITH_TORCH=OFF)") ELSE() -MESSAGE(STATUS " With SuperPoint = NO (libtorch not found)") +MESSAGE(STATUS " With SuperPoint = NO (libtorch not found)") ENDIF() +IF(TORCH_FOUND AND WITH_PYTHON AND Python3_FOUND) +MESSAGE(STATUS " With Superpoint Rpautrat = YES (Liscense: MIT) libtorch=${Torch_VERSION}") +ELSEIF(NOT WITH_TORCH) +MESSAGE(STATUS " With Superpoint Rpautrat = NO (WITH_TORCH=OFF)") +ELSEIF(NOT WITH_PYTHON) +MESSAGE(STATUS " With Superpoint Rpautrat = NO (WITH_PYTHON=OFF)") +ELSE() +MESSAGE(STATUS " Wtih Superpoint Rpautrat = NO (libtorch and/or python3 not found)") +ENDIF() + + IF(WITH_PYTHON AND Python3_FOUND) -MESSAGE(STATUS " With Python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR} = YES (License: PSF)") +MESSAGE(STATUS " With Python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR} = YES (License: PSF)") ELSEIF(NOT WITH_PYTHON) MESSAGE(STATUS " With Python3 = NO (WITH_PYTHON=OFF)") ELSE() diff --git a/corelib/include/rtabmap/core/Features2d.h b/corelib/include/rtabmap/core/Features2d.h index f26f84be..e999a1fd 100644 --- a/corelib/include/rtabmap/core/Features2d.h +++ b/corelib/include/rtabmap/core/Features2d.h @@ -104,6 +104,7 @@ namespace rtabmap { class ORBextractor; class SPDetector; +class SPDetectorRpautrat; class Stereo; #if CV_MAJOR_VERSION < 3 @@ -129,7 +130,8 @@ public: kFeatureSurfFreak=12, //new 0.20.4 kFeatureGfttDaisy=13, //new 0.20.6 kFeatureSurfDaisy=14, //new 0.20.6 - kFeaturePyDetector=15}; //new 0.20.8 + kFeaturePyDetector=15, //new 0.20.8 + kFeatureSuperPointRpautrat=16}; // new 0.23.3 static std::string typeName(Type type) { @@ -164,6 +166,8 @@ public: return "GFTT+Daisy"; case kFeatureSurfDaisy: return "SURF+Daisy"; + case kFeatureSuperPointRpautrat: + return "SUPERPOINT-RPAUTRAT"; default: return "Unknown"; } @@ -626,6 +630,31 @@ private: bool cuda_; }; +//SuperPointRpautrat +class RTABMAP_CORE_EXPORT SuperPointRpautrat : public Feature2D +{ +public: + SuperPointRpautrat(const ParametersMap & parameters = ParametersMap()); + virtual ~SuperPointRpautrat(); + + virtual void parseParameters(const ParametersMap & parameters); + virtual Feature2D::Type getType() const { return kFeatureSuperPointRpautrat; } + +private: + virtual std::vector generateKeypointsImpl(const cv::Mat & image, const cv::Rect & roi, const cv::Mat & mask = cv::Mat()); + virtual cv::Mat generateDescriptorsImpl(const cv::Mat & image, std::vector & keypoints) const; + + cv::Ptr superPoint_; + + std::string superpointWeightsPath_; + std::string superpointModelPath_; + std::string outputDir_; + float threshold_; + bool nms_; + int minDistance_; + bool cuda_; +}; + //GFTT_DAISY class RTABMAP_CORE_EXPORT GFTT_DAISY : public GFTT { diff --git a/corelib/include/rtabmap/core/Parameters.h b/corelib/include/rtabmap/core/Parameters.h index 74d73476..aba8a89d 100644 --- a/corelib/include/rtabmap/core/Parameters.h +++ b/corelib/include/rtabmap/core/Parameters.h @@ -252,9 +252,9 @@ class RTABMAP_CORE_EXPORT Parameters RTABMAP_PARAM(Kp, NndrRatio, float, 0.8, "NNDR ratio (A matching pair is detected, if its distance is closer than X times the distance of the second nearest neighbor.)"); #if CV_MAJOR_VERSION > 2 && !defined(HAVE_OPENCV_XFEATURES2D) // OpenCV>2 without xFeatures2D module doesn't have BRIEF - RTABMAP_PARAM(Kp, DetectorStrategy, int, 8, "0=SURF 1=SIFT 2=ORB 3=FAST/FREAK 4=FAST/BRIEF 5=GFTT/FREAK 6=GFTT/BRIEF 7=BRISK 8=GFTT/ORB 9=KAZE 10=ORB-OCTREE 11=SuperPoint 12=SURF/FREAK 13=GFTT/DAISY 14=SURF/DAISY 15=PyDetector"); + RTABMAP_PARAM(Kp, DetectorStrategy, int, 8, "0=SURF 1=SIFT 2=ORB 3=FAST/FREAK 4=FAST/BRIEF 5=GFTT/FREAK 6=GFTT/BRIEF 7=BRISK 8=GFTT/ORB 9=KAZE 10=ORB-OCTREE 11=SuperPoint 12=SURF/FREAK 13=GFTT/DAISY 14=SURF/DAISY 15=PyDetector 16=SuperPoint-Rpautrat"); #else - RTABMAP_PARAM(Kp, DetectorStrategy, int, 6, "0=SURF 1=SIFT 2=ORB 3=FAST/FREAK 4=FAST/BRIEF 5=GFTT/FREAK 6=GFTT/BRIEF 7=BRISK 8=GFTT/ORB 9=KAZE 10=ORB-OCTREE 11=SuperPoint 12=SURF/FREAK 13=GFTT/DAISY 14=SURF/DAISY 15=PyDetector"); + RTABMAP_PARAM(Kp, DetectorStrategy, int, 6, "0=SURF 1=SIFT 2=ORB 3=FAST/FREAK 4=FAST/BRIEF 5=GFTT/FREAK 6=GFTT/BRIEF 7=BRISK 8=GFTT/ORB 9=KAZE 10=ORB-OCTREE 11=SuperPoint 12=SURF/FREAK 13=GFTT/DAISY 14=SURF/DAISY 15=PyDetector 16=SuperPoint-Rpautrat"); #endif RTABMAP_PARAM(Kp, TfIdfLikelihoodUsed, bool, true, "Use of the td-idf strategy to compute the likelihood."); RTABMAP_PARAM(Kp, Parallelized, bool, true, "If the dictionary update and signature creation were parallelized."); @@ -346,6 +346,13 @@ class RTABMAP_CORE_EXPORT Parameters RTABMAP_PARAM(SuperPoint, NMSRadius, int, 4, uFormat("[%s=true] Minimum distance (pixels) between keypoints.", kSuperPointNMS().c_str())); RTABMAP_PARAM(SuperPoint, Cuda, bool, true, "Use Cuda device for Torch, otherwise CPU device is used by default."); + RTABMAP_PARAM_STR(SuperPointRpautrat, WeightsPath, "", "[Required] SuperPoint weights file (*.pth)."); + RTABMAP_PARAM_STR(SuperPointRpautrat, ModelPath, "", "[Required] SuperPoint python model file (superpoint_pytorch.py)."); + RTABMAP_PARAM(SuperPointRpautrat, Threshold, float, 0.005, "Detector response threshold to accept keypoint."); + RTABMAP_PARAM(SuperPointRpautrat, NMS, bool, true, "If true, non-maximum suppression is applied to detected keypoints."); + RTABMAP_PARAM(SuperPointRpautrat, NMSRadius, int, 4, uFormat("[%s=true] Minimum distance (pixels) between keypoints.", kSuperPointRpautratNMS().c_str())); + RTABMAP_PARAM(SuperPointRpautrat, Cuda, bool, true, "Use Cuda device for Torch, otherwise CPU device is used by default."); + RTABMAP_PARAM_STR(PyDetector, Path, "", "Path to python script file (see available ones in rtabmap/corelib/src/python/*). See the header to see where the script should be copied."); RTABMAP_PARAM(PyDetector, Cuda, bool, true, "Use cuda."); @@ -704,9 +711,9 @@ class RTABMAP_CORE_EXPORT Parameters RTABMAP_PARAM(Vis, Iterations, int, 300, "Maximum iterations to compute the transform."); #if CV_MAJOR_VERSION > 2 && !defined(HAVE_OPENCV_XFEATURES2D) // OpenCV>2 without xFeatures2D module doesn't have BRIEF - RTABMAP_PARAM(Vis, FeatureType, int, 8, "0=SURF 1=SIFT 2=ORB 3=FAST/FREAK 4=FAST/BRIEF 5=GFTT/FREAK 6=GFTT/BRIEF 7=BRISK 8=GFTT/ORB 9=KAZE 10=ORB-OCTREE 11=SuperPoint 12=SURF/FREAK 13=GFTT/DAISY 14=SURF/DAISY 15=PyDetector"); + RTABMAP_PARAM(Vis, FeatureType, int, 8, "0=SURF 1=SIFT 2=ORB 3=FAST/FREAK 4=FAST/BRIEF 5=GFTT/FREAK 6=GFTT/BRIEF 7=BRISK 8=GFTT/ORB 9=KAZE 10=ORB-OCTREE 11=SuperPoint 12=SURF/FREAK 13=GFTT/DAISY 14=SURF/DAISY 15=PyDetector 16=SuperPoint-Rpautrat"); #else - RTABMAP_PARAM(Vis, FeatureType, int, 6, "0=SURF 1=SIFT 2=ORB 3=FAST/FREAK 4=FAST/BRIEF 5=GFTT/FREAK 6=GFTT/BRIEF 7=BRISK 8=GFTT/ORB 9=KAZE 10=ORB-OCTREE 11=SuperPoint 12=SURF/FREAK 13=GFTT/DAISY 14=SURF/DAISY 15=PyDetector"); + RTABMAP_PARAM(Vis, FeatureType, int, 6, "0=SURF 1=SIFT 2=ORB 3=FAST/FREAK 4=FAST/BRIEF 5=GFTT/FREAK 6=GFTT/BRIEF 7=BRISK 8=GFTT/ORB 9=KAZE 10=ORB-OCTREE 11=SuperPoint 12=SURF/FREAK 13=GFTT/DAISY 14=SURF/DAISY 15=PyDetector 16=SuperPoint-Rpautrat"); #endif RTABMAP_PARAM(Vis, MaxFeatures, int, 1000, "0 no limits."); RTABMAP_PARAM(Vis, SSC, bool, false, "If true, SSC (Suppression via Square Covering) is applied to limit keypoints."); diff --git a/corelib/src/CMakeLists.txt b/corelib/src/CMakeLists.txt index efd8ece1..5ff638d2 100644 --- a/corelib/src/CMakeLists.txt +++ b/corelib/src/CMakeLists.txt @@ -215,11 +215,21 @@ IF(TORCH_FOUND) ${SRC_FILES} superpoint_torch/SuperPoint.cc ) - SET(INCLUDE_DIRS + SET(INCLUDE_DIRS ${TORCH_INCLUDE_DIRS} ${CMAKE_CURRENT_SOURCE_DIR}/superpoint_torch ${INCLUDE_DIRS} ) + IF(WITH_PYTHON AND Python3_FOUND) + SET(SRC_FILES + ${SRC_FILES} + superpoint_rpautrat/SuperpointRpautrat.cpp + ) + SET(INCLUDE_DIRS + ${CMAKE_CURRENT_SOURCE_DIR}/superpoint_rpautrat + ${INCLUDE_DIRS} + ) + ENDIF(WITH_PYTHON AND Python3_FOUND) ENDIF(TORCH_FOUND) IF(WITH_PYTHON AND Python3_FOUND) @@ -838,6 +848,13 @@ SET(RESOURCES ${CMAKE_CURRENT_SOURCE_DIR}/resources/backward_compatibility/DatabaseSchema_0_16_0.sql ) +IF(TORCH_FOUND AND WITH_PYTHON AND Python3_FOUND) + SET(RESOURCES + ${RESOURCES} + ${CMAKE_CURRENT_SOURCE_DIR}/superpoint_rpautrat/superpoint_to_torchscript.py + ) +ENDIF() + foreach(arg ${RESOURCES}) get_filename_component(filename ${arg} NAME) string(REPLACE "." "_" output ${filename}) diff --git a/corelib/src/Features2d.cpp b/corelib/src/Features2d.cpp index 90555db4..b18efb23 100644 --- a/corelib/src/Features2d.cpp +++ b/corelib/src/Features2d.cpp @@ -47,6 +47,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #ifdef RTABMAP_TORCH #include "superpoint_torch/SuperPoint.h" #endif +#if defined(RTABMAP_TORCH) && defined(RTABMAP_PYTHON) +#include "superpoint_rpautrat/SuperpointRpautrat.h" +#endif #ifdef RTABMAP_PYTHON #include "python/PyDetector.h" @@ -730,9 +733,14 @@ Feature2D * Feature2D::create(Feature2D::Type type, const ParametersMap & parame feature2D = new ORBOctree(parameters); break; #ifdef RTABMAP_TORCH - case Feature2D::kFeatureSuperPointTorch: - feature2D = new SuperPointTorch(parameters); - break; +case Feature2D::kFeatureSuperPointTorch: + feature2D = new SuperPointTorch(parameters); + break; +#endif +#if defined(RTABMAP_TORCH) && defined(RTABMAP_PYTHON) +case Feature2D::kFeatureSuperPointRpautrat: + feature2D = new SuperPointRpautrat(parameters); + break; #endif case Feature2D::kFeatureSurfFreak: feature2D = new SURF_FREAK(parameters); @@ -831,7 +839,7 @@ std::vector Feature2D::generateKeypoints(const cv::Mat & image, co cv::Rect roi(globalRoi.x + j*colSize, globalRoi.y + i*rowSize, colSize, rowSize); std::vector subKeypoints; subKeypoints = this->generateKeypointsImpl(image, roi, mask); - if (this->getType() != Feature2D::Type::kFeaturePyDetector) + if (this->getType() != Feature2D::Type::kFeaturePyDetector && this->getType() != Feature2D::Type::kFeatureSuperPointRpautrat) { limitKeypoints(subKeypoints, maxFeatures, roi.size(), this->getSSC()); } @@ -2615,6 +2623,102 @@ cv::Mat SuperPointTorch::generateDescriptorsImpl(const cv::Mat & image, std::vec } +////////////////////////// +//SuperPointRpautrat +////////////////////////// +SuperPointRpautrat::SuperPointRpautrat(const ParametersMap & parameters) : + superpointWeightsPath_(Parameters::defaultSuperPointRpautratWeightsPath()), + superpointModelPath_(Parameters::defaultSuperPointRpautratModelPath()), + outputDir_(""), + threshold_(Parameters::defaultSuperPointRpautratThreshold()), + nms_(Parameters::defaultSuperPointRpautratNMS()), + minDistance_(Parameters::defaultSuperPointRpautratNMSRadius()), + cuda_(Parameters::defaultSuperPointRpautratCuda()) +{ + parseParameters(parameters); +} + +SuperPointRpautrat::~SuperPointRpautrat() +{ +} + +void SuperPointRpautrat::parseParameters(const ParametersMap & parameters) +{ + Feature2D::parseParameters(parameters); + +#if defined(RTABMAP_TORCH) && defined(RTABMAP_PYTHON) + std::string previousWeightsPath = superpointWeightsPath_; + std::string previousModelPath = superpointModelPath_; + bool previousCuda = cuda_; + float previousThreshold = threshold_; + bool previousNms = nms_; + int previousMinDistance = minDistance_; + + Parameters::parse(parameters, Parameters::kSuperPointRpautratWeightsPath(), superpointWeightsPath_); + Parameters::parse(parameters, Parameters::kSuperPointRpautratModelPath(), superpointModelPath_); + Parameters::parse(parameters, Parameters::kSuperPointRpautratThreshold(), threshold_); + Parameters::parse(parameters, Parameters::kSuperPointRpautratNMS(), nms_); + Parameters::parse(parameters, Parameters::kSuperPointRpautratNMSRadius(), minDistance_); + Parameters::parse(parameters, Parameters::kSuperPointRpautratCuda(), cuda_); + Parameters::parse(parameters, Parameters::kRtabmapWorkingDirectory(), outputDir_); + + // If working directory is not set, use the default + if(outputDir_.empty()) + { + outputDir_ = Parameters::createDefaultWorkingDirectory(); + } + + // Delete the detector to force re-initialization on next frame if any parameter changed + if(superPoint_.get() == 0 || + superpointWeightsPath_.compare(previousWeightsPath) != 0 || + superpointModelPath_.compare(previousModelPath) != 0 || + previousCuda != cuda_ || + previousThreshold != threshold_ || + previousNms != nms_ || + previousMinDistance != minDistance_) + { + superPoint_ = cv::Ptr(new SPDetectorRpautrat(superpointWeightsPath_, superpointModelPath_, outputDir_, threshold_, nms_, minDistance_, cuda_)); + } +#else + UWARN("RTAB-Map is not built with Torch support so SuperPoint Rpautrat feature cannot be used!"); +#endif +} + +std::vector SuperPointRpautrat::generateKeypointsImpl(const cv::Mat & image, const cv::Rect & roi, const cv::Mat & mask) +{ +#if defined(RTABMAP_TORCH) && defined(RTABMAP_PYTHON) + UASSERT(!image.empty() && image.channels() == 1 && image.depth() == CV_8U); + if(roi.x!=0 || roi.y !=0) + { + UERROR("SuperPoint Rpautrat: Not supporting ROI (%d,%d,%d,%d). Make sure %s, %s, %s, %s, %s, %s are all set to default values.", + roi.x, roi.y, roi.width, roi.height, + Parameters::kKpRoiRatios().c_str(), + Parameters::kVisRoiRatios().c_str(), + Parameters::kVisGridRows().c_str(), + Parameters::kVisGridCols().c_str(), + Parameters::kKpGridRows().c_str(), + Parameters::kKpGridCols().c_str()); + return std::vector(); + } + return superPoint_->detect(image, mask); +#else + UWARN("RTAB-Map is not built with Torch support so SuperPoint Rpautrat feature cannot be used!"); + return std::vector(); +#endif +} + +cv::Mat SuperPointRpautrat::generateDescriptorsImpl(const cv::Mat & image, std::vector & keypoints) const +{ +#if defined(RTABMAP_TORCH) && defined(RTABMAP_PYTHON) + UASSERT(!image.empty() && image.channels() == 1 && image.depth() == CV_8U); + return superPoint_->compute(keypoints); +#else + UWARN("RTAB-Map is not built with Torch support so SuperPoint Rpautrat feature cannot be used!"); + return cv::Mat(); +#endif +} + + ////////////////////////// //GFTT-DAISY ////////////////////////// diff --git a/corelib/src/Parameters.cpp b/corelib/src/Parameters.cpp index be0300b0..0d4f8969 100644 --- a/corelib/src/Parameters.cpp +++ b/corelib/src/Parameters.cpp @@ -168,6 +168,7 @@ bool Parameters::isFeatureParameter(const std::string & parameter) group.compare("BRISK") == 0 || group.compare("KAZE") == 0 || group.compare("SuperPoint") == 0 || + group.compare("SuperPointRpautrat") == 0 || group.compare("PyDetector") == 0; } @@ -661,6 +662,12 @@ ParametersMap Parameters::parseArguments(int argc, char * argv[], bool onlyParam std::cout << str << std::setw(spacing - str.size()) << "true" << std::endl; #else std::cout << str << std::setw(spacing - str.size()) << "false" << std::endl; +#endif + str = "With SuperPoint Rpautrat:"; +#if defined(RTABMAP_TORCH) && defined(RTABMAP_PYTHON) + std::cout << str << std::setw(spacing - str.size()) << "true" << std::endl; +#else + std::cout << str << std::setw(spacing - str.size()) << "false" << std::endl; #endif str = "With Python3:"; #ifdef RTABMAP_PYTHON diff --git a/corelib/src/RegistrationVis.cpp b/corelib/src/RegistrationVis.cpp index 3695980b..26118830 100644 --- a/corelib/src/RegistrationVis.cpp +++ b/corelib/src/RegistrationVis.cpp @@ -286,6 +286,10 @@ void RegistrationVis::parseParameters(const ParametersMap & parameters) { uInsert(_featureParameters, ParametersPair(Parameters::kKpGridCols(), parameters.at(Parameters::kVisGridCols()))); } + if(uContains(parameters, Parameters::kRtabmapWorkingDirectory())) + { + uInsert(_featureParameters, ParametersPair(Parameters::kRtabmapWorkingDirectory(), parameters.at(Parameters::kRtabmapWorkingDirectory()))); + } delete _detectorFrom; delete _detectorTo; diff --git a/corelib/src/python/rtabmap_superpoint_rpautrat.py b/corelib/src/python/rtabmap_superpoint_rpautrat.py new file mode 100644 index 00000000..ebcf18b4 --- /dev/null +++ b/corelib/src/python/rtabmap_superpoint_rpautrat.py @@ -0,0 +1,71 @@ +#! /usr/bin/env python3 +# +# Drop this file in the root folder of SuperPoint git: https://github.com/rpautrat/SuperPoint +# To use with rtabmap: +# --Vis/FeatureType 15 --Kp/DetectorStrategy 15 --PyDetector/Path "~/SuperPoint/rtabmap_superpoint_rpautrat.py" +# +import numpy as np +import os +import torch +from superpoint_pytorch import SuperPoint + +superpoint = [] +device = 'cpu' + +def init(cuda): + global superpoint, device + + superpoint = SuperPoint().eval() + + # set up device, gpu or cpu depending on the availability and the user's choice + device = 'cuda' if torch.cuda.is_available() and cuda else 'cpu' + + # Load weights directly to target device + # Get the directory where this script is located + script_dir = os.path.dirname(os.path.abspath(__file__)) + weights_path = os.path.join(script_dir, 'weights', 'superpoint_v6_from_tf.pth') + + # Load model weights with proper error handling + try: + state_dict = torch.load(weights_path, map_location=device, weights_only=True) + superpoint.load_state_dict(state_dict) + except Exception as e: + print(f"Error loading weights: {e}") + raise + + # Move the model to the target device + superpoint.to(device) + + # Ensure model is in eval mode for inference + superpoint.eval() + +def detect(imageBuffer): + global superpoint, device + + image = np.asarray(imageBuffer) + image = (image.astype('float32') / 255.) + + try: + image_with_dims = image[None, None] # Add batch and channel dims + image_tensor = torch.from_numpy(image_with_dims).float() + image_tensor = image_tensor.to(device) + except Exception as e: + print(f"Error creating tensor: {e}") + raise + # Result: (1, 1, H, W) - PyTorch tensor on correct device (CPU or GPU). + + with torch.no_grad(): + pred = superpoint({'image': image_tensor}) + + # Extract keypoints and descriptors + keypoints = pred['keypoints'][0].cpu().numpy() # Shape: (N, 2) + keypoints_response = pred['keypoint_scores'][0].cpu().numpy() + keypoints_with_response = np.column_stack([keypoints, keypoints_response]).astype(np.float32) + # Result: (N, 3) with [x, y, response] + + descriptors = pred['descriptors'][0].cpu().numpy() + # Result: (N, descriptor_dim) + + desc = np.float32(descriptors).copy() + pts = np.float32(keypoints_with_response).copy() + return pts, desc \ No newline at end of file diff --git a/corelib/src/superpoint_rpautrat/SuperpointRpautrat.cpp b/corelib/src/superpoint_rpautrat/SuperpointRpautrat.cpp new file mode 100644 index 00000000..b3391103 --- /dev/null +++ b/corelib/src/superpoint_rpautrat/SuperpointRpautrat.cpp @@ -0,0 +1,230 @@ +/** + * SuperPoint implementation based on the PyTorch version by Rémi Pautrat, Paul-Edouard Sarlin + * Adapted for RTAB-Map integration + */ + +#include "SuperpointRpautrat.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include "superpoint_to_torchscript_py.h" + +namespace rtabmap +{ + +// Run the python script to export the SuperPoint model file with the desired parameters +static std::string exportSuperPointTorchScript( + const std::string & superpointWeightsPath, + const std::string & superpointModelPath, + const std::string & outputDir, + const int & width, + const int & height, + const float & threshold, + const int & nms_radius, + const bool & cuda) +{ + // Validate output directory is explicitly set and exists + if(outputDir.empty()) + { + UERROR("Output directory is not set."); + return std::string(""); + } + if(!UDirectory::exists(outputDir)) + { + UERROR("Output directory does not exist: %s", outputDir.c_str()); + return std::string(""); + } + + // Resolve paths (no dependency on source tree) + const std::string weightsPath = superpointWeightsPath; + const std::string modelPath = superpointModelPath; + const std::string output = std::string(outputDir + "/superpoint_v6_from_tf.pt"); + + // Sanity checks + if(!UFile::exists(weightsPath)) { + UERROR("Weights not found: %s", weightsPath.c_str()); + return ""; + } + if(!UFile::exists(modelPath)) { + UERROR("Model not found: %s", modelPath.c_str()); + return ""; + } + + // Execute the script inside the embedded Python interpreter + try + { + pybind11::gil_scoped_acquire acquire; + pybind11::dict scope; + scope["__builtins__"] = pybind11::module_::import("builtins"); + + // set sys.path to the location of the model definition so it can be imported + std::string model_dir = UDirectory::getDir(modelPath); + auto sys = pybind11::module_::import("sys"); + pybind11::list sys_path = sys.attr("path"); + sys_path.attr("insert")(0, model_dir); + + try { + // execute the script to generate the model + pybind11::exec(uHex2Str(SUPERPOINT_TO_TORCHSCRIPT_PY), scope, scope); + pybind11::function generate_model = scope["generate_model"].cast(); + pybind11::object result = generate_model(weightsPath, output, cuda, nms_radius, threshold, width, height); + sys_path.attr("remove")(model_dir); + } + catch(...) { + // Ensure sys.path cleanup on any exception + sys_path.attr("remove")(model_dir); + throw; + } + + } + // pybind11 throws std::exception for RuntimeError + catch (const std::exception &e) + { + UERROR("Python export failed: %s", e.what()); + return ""; + } + + return output; +} + +SPDetectorRpautrat::SPDetectorRpautrat(std::string superpointWeightsPath, std::string superpointModelPath, std::string outputDir, float threshold, bool nms, int minDistance, bool cuda) : + device_(torch::kCPU), + superpointWeightsPath_(superpointWeightsPath), + superpointModelPath_(superpointModelPath), + outputDir_(outputDir), + threshold_(threshold), + nms_(nms), + minDistance_(minDistance), + detected_(false) +{ + if(cuda && !torch::cuda::is_available()) + { + UWARN("Cuda option is enabled but torch doesn't have cuda support on this platform, using CPU instead."); + } + cuda_ = cuda && torch::cuda::is_available(); + + if(!UFile::exists(superpointWeightsPath_)) { + UERROR("Superpoint weights not found: %s", superpointWeightsPath_.c_str()); + } + + // Update device based on cuda availability + device_ = torch::Device(cuda_ ? torch::kCUDA : torch::kCPU); +} + +SPDetectorRpautrat::~SPDetectorRpautrat() +{ +} + +cv::Mat SPDetectorRpautrat::compute(const std::vector &keypoints) +{ + if(!detected_) + { + UERROR("SPDetector has been reset before extracting the descriptors! detect() should be called before compute()."); + return cv::Mat(); + } + if(keypoints.empty()) + { + return cv::Mat(); + } + + // These should have the same size + UASSERT(static_cast(desc_.size(0)) == keypoints.size()); + + // Move to CPU and return descriptors computed in the forward pass + torch::Tensor desc_cpu = desc_.to(torch::kCPU); + cv::Mat desc_mat(cv::Size(desc_cpu.size(1), desc_cpu.size(0)), CV_32FC1, desc_cpu.data_ptr()); + return desc_mat.clone(); +} + +std::vector SPDetectorRpautrat::detect(const cv::Mat &img, const cv::Mat & mask) +{ + // On first frame, run a trace of the model with the desired parameters and load the model file + if(!detected_) + { + // effectively disable nms if it is not enabled by setting radius to 0 + int nms_radius = nms_ ? minDistance_ : 0; + + std::string modelPath = exportSuperPointTorchScript( + superpointWeightsPath_, + superpointModelPath_, + outputDir_, + img.cols, + img.rows, + threshold_, + nms_radius, + cuda_ + ); + + UDEBUG("Initializing SuperPoint Rpautrat detector with model: %s", modelPath.c_str()); + UDEBUG("modelPath=%s thr=%f nms=%d minDistance=%d cuda=%d", modelPath.c_str(), threshold_, nms_?1:0, minDistance_, cuda_?1:0); + if(modelPath.empty()) + { + UERROR("Model's path is empty! The model was not exported correctly."); + return std::vector(); + } + if(!UFile::exists(modelPath)) + { + UERROR("Model's path \"%s\" doesn't exist!", modelPath.c_str()); + return std::vector(); + } + + // Load TorchScript model + model_ = torch::jit::load(modelPath); + model_.eval(); // put in evaluation mode + model_.to(device_); + } + + // format the input tensor for the model + torch::NoGradGuard no_grad_guard; + auto x = torch::from_blob(img.data, {1, 1, img.rows, img.cols}, torch::kByte); + x = x.to(torch::kFloat) / 255; + x = x.set_requires_grad(false).to(device_); + + auto outputs = model_.forward({x}).toTuple(); + keypoints_tensor_ = outputs->elements()[0].toTensor(); // [N, 2] keypoint coordinates + auto scores_tensor = outputs->elements()[1].toTensor(); // [N] keypoint scores + desc_ = outputs->elements()[2].toTensor(); // [N, 256] descriptors + + // Convert to CPU for processing + auto keypoints_cpu = keypoints_tensor_.to(torch::kCPU); + auto scores_cpu = scores_tensor.to(torch::kCPU); + + std::vector filtered_keypoints; + std::vector keep_indices_vec; + + // Apply mask filtering + for(int i = 0; i < keypoints_cpu.size(0); i++) { + float score = scores_cpu[i].item(); + float x = keypoints_cpu[i][0].item(); // x coordinate + float y = keypoints_cpu[i][1].item(); // y coordinate + + // Check mask if provided + if(mask.empty() || mask.at((int)y, (int)x) != 0) { + keep_indices_vec.push_back(i); + filtered_keypoints.emplace_back(cv::KeyPoint(x, y, 8, -1, score)); + } + } + + // Update the stored tensors to maintain correspondence + // This way if keypoints are re-ordered, we can still match kpts->descs in the compute step + auto keep_indices = torch::from_blob(keep_indices_vec.data(), {(long int)keep_indices_vec.size()}, torch::kLong); + keep_indices = keep_indices.to(keypoints_tensor_.device()); + auto filtered_keypoints_tensor = keypoints_tensor_.index_select(0, keep_indices); + auto filtered_descriptors = desc_.index_select(0, keep_indices); + + keypoints_tensor_ = filtered_keypoints_tensor; + desc_ = filtered_descriptors; + + detected_ = true; + return filtered_keypoints; +} + +} // namespace rtabmap \ No newline at end of file diff --git a/corelib/src/superpoint_rpautrat/SuperpointRpautrat.h b/corelib/src/superpoint_rpautrat/SuperpointRpautrat.h new file mode 100644 index 00000000..9adb982f --- /dev/null +++ b/corelib/src/superpoint_rpautrat/SuperpointRpautrat.h @@ -0,0 +1,52 @@ +/** + * SuperPoint implementation based on the PyTorch version by Rémi Pautrat, Paul-Edouard Sarlin + * Adapted for RTAB-Map integration + */ + +#ifndef SUPERPOINT_RPAUTRAT_H +#define SUPERPOINT_RPAUTRAT_H + +#include +#include +#include +#include + +namespace rtabmap +{ + +class SPDetectorRpautrat { + public: + SPDetectorRpautrat( + std::string superpointWeightsPath, + std::string superpointModelPath, + std::string outputDir, + float threshold = 0.005f, + bool nms = true, + int nmsRadius = 4, + bool cuda = false + ); + virtual ~SPDetectorRpautrat(); + std::vector detect(const cv::Mat &img, const cv::Mat & mask = cv::Mat()); + cv::Mat compute(const std::vector &keypoints); + + private: + torch::jit::script::Module model_; + torch::Device device_; + torch::Tensor desc_; + torch::Tensor keypoints_tensor_; + + std::string superpointWeightsPath_; + std::string superpointModelPath_; + std::string outputDir_; + float threshold_; + bool nms_; + int minDistance_; + bool cuda_; + + bool detected_; + }; +} + + + +#endif // SUPERPOINT_RPAUTRAT_H diff --git a/corelib/src/superpoint_rpautrat/superpoint_to_torchscript.py b/corelib/src/superpoint_rpautrat/superpoint_to_torchscript.py new file mode 100644 index 00000000..da342c7e --- /dev/null +++ b/corelib/src/superpoint_rpautrat/superpoint_to_torchscript.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +""" +Convert PyTorch weights to TorchScript format for C++ usage. +""" + +import argparse +import os + +import torch +import torch.nn as nn +from superpoint_pytorch import SuperPoint + + +def wrap_model(model: nn.Module): + """ + Simple wrapper to fix SuperPoint input format for TorchScript. + Easier to call from C++ code since the input isn't a dictionary. + """ + class Wrapper(nn.Module): + def __init__(self, net: nn.Module): + super().__init__() + self.net = net + + def forward(self, x: torch.Tensor): + # SuperPoint expects {"image": tensor} but TorchScript doesn't like dict indexing + out = self.net.forward({"image": x}) + + # Return the format expected by C++ code: keypoints, scores, descriptors + # For single batch item, take the first (and only) element + keypoints = out["keypoints"][0] if out["keypoints"] else torch.empty(0, 2) + scores = out["keypoint_scores"][0] if out["keypoint_scores"] else torch.empty(0) + descriptors = out["descriptors"][0] if out["descriptors"] else torch.empty(0, 256) + + return (keypoints, scores, descriptors) + + return Wrapper(model) + +def generate_model( + weights_path: str, + output_path: str, + cuda: bool, + nms_radius: int, + threshold: float, + width: int, + height: int, +): + # Check if weights are already TorchScript + try: + scripted = torch.jit.load(weights_path, map_location="cpu") + scripted.eval() + torch.jit.save(scripted, output_path) + print(f"Converted TorchScript file: {output_path}") + return + except: + pass + + device = "cuda" if cuda else "cpu" + + # Load SuperPoint model and weights + model = SuperPoint( + nms_radius=nms_radius, + threshold=threshold, + ).eval().to(device) + + # Load weights without forcing CPU location to allow CUDA usage + weights = torch.load(weights_path, map_location=None) + if isinstance(weights, dict) and "state_dict" in weights: + weights = weights["state_dict"] + + model.load_state_dict(weights, strict=False) + + wrapped = wrap_model(model) + dummy = torch.randn(1, 1, height, width, device=device) # Dummy input, grayscale, using cuda. + + # Convert to TorchScript using trace (SuperPoint has dynamic behavior that scripting can't handle) + print("Using torch.jit.trace (SuperPoint has dynamic behavior)...") + scripted = torch.jit.trace(wrapped, (dummy,), strict=False) + print("Successfully traced SuperPoint model") + + # Save output + os.makedirs(os.path.dirname(output_path), exist_ok=True) + torch.jit.save(scripted, output_path) + print(f"Converted SuperPoint weights to TorchScript: {output_path}") + + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Convert SuperPoint weights to TorchScript") + parser.add_argument("--weights", required=True, help="Path to weights file") + parser.add_argument("--output", required=True, help="Output TorchScript file") + parser.add_argument("--cuda", action="store_true", help="Use CUDA") + parser.add_argument("--width", type=int, default=1920, help="Width of the input image") + parser.add_argument("--height", type=int, default=288, help="Height of the input image") + parser.add_argument("--nms_radius", type=int, default=4, help="NMS radius") + parser.add_argument("--threshold", type=float, default=0.005, help="Confidence threshold") + args = parser.parse_args() + print(f"Generating model from weights: {args.weights} to output: {args.output}") + + generate_model( + weights_path=args.weights, + output_path=args.output, + cuda=args.cuda, + nms_radius=args.nms_radius, + threshold=args.threshold, + width=args.width, + height=args.height, + ) diff --git a/guilib/include/rtabmap/gui/PreferencesDialog.h b/guilib/include/rtabmap/gui/PreferencesDialog.h index 8678b846..0df6bd43 100644 --- a/guilib/include/rtabmap/gui/PreferencesDialog.h +++ b/guilib/include/rtabmap/gui/PreferencesDialog.h @@ -374,6 +374,8 @@ private Q_SLOTS: void changeOdometryOpenVINSRightMask(); void changeIcpPMConfigPath(); void changeSuperPointModelPath(); + void changeSuperPointRpautratWeightsPath(); + void changeSuperPointRpautratModelPath(); void changePyMatcherPath(); void changePyMatcherModel(); void changePyDescriptorPath(); diff --git a/guilib/src/AboutDialog.cpp b/guilib/src/AboutDialog.cpp index 439f85fe..5cdf8c34 100644 --- a/guilib/src/AboutDialog.cpp +++ b/guilib/src/AboutDialog.cpp @@ -86,6 +86,13 @@ AboutDialog::AboutDialog(QWidget * parent) : _ui->label_sptorch->setText("No"); _ui->label_sptorch_license->setEnabled(false); #endif +#if defined(RTABMAP_TORCH) && defined(RTABMAP_PYTHON) + _ui->label_sprpautrat->setText("Yes"); + _ui->label_sprpautrat_license->setEnabled(true); +#else + _ui->label_sprpautrat->setText("No"); + _ui->label_sprpautrat_license->setEnabled(false); +#endif #ifdef RTABMAP_PYTHON _ui->label_pymatcher->setText("Yes"); _ui->label_pymatcher_license->setEnabled(true); diff --git a/guilib/src/PreferencesDialog.cpp b/guilib/src/PreferencesDialog.cpp index ab272115..933b8a17 100644 --- a/guilib/src/PreferencesDialog.cpp +++ b/guilib/src/PreferencesDialog.cpp @@ -294,6 +294,10 @@ PreferencesDialog::PreferencesDialog(QWidget * parent) : _ui->comboBox_detector_strategy->setItemData(11, 0, Qt::UserRole - 1); _ui->vis_feature_detector->setItemData(11, 0, Qt::UserRole - 1); #endif +#if !defined(RTABMAP_TORCH) || !defined(RTABMAP_PYTHON) + _ui->comboBox_detector_strategy->setItemData(16, 0, Qt::UserRole - 1); + _ui->vis_feature_detector->setItemData(16, 0, Qt::UserRole - 1); +#endif #ifndef RTABMAP_PYTHON _ui->comboBox_detector_strategy->setItemData(15, 0, Qt::UserRole - 1); @@ -1197,6 +1201,16 @@ PreferencesDialog::PreferencesDialog(QWidget * parent) : _ui->spinBox_sptorch_minDistance->setObjectName(Parameters::kSuperPointNMSRadius().c_str()); _ui->checkBox_sptorch_cuda->setObjectName(Parameters::kSuperPointCuda().c_str()); + // SuperPoint Rpautrat + _ui->lineEdit_sprpautrat_weights_path->setObjectName(Parameters::kSuperPointRpautratWeightsPath().c_str()); + connect(_ui->toolButton_sprpautrat_weights_path, SIGNAL(clicked()), this, SLOT(changeSuperPointRpautratWeightsPath())); + _ui->lineEdit_sprpautrat_model_path->setObjectName(Parameters::kSuperPointRpautratModelPath().c_str()); + connect(_ui->toolButton_sprpautrat_model_path, SIGNAL(clicked()), this, SLOT(changeSuperPointRpautratModelPath())); + _ui->doubleSpinBox_sprpautrat_threshold->setObjectName(Parameters::kSuperPointRpautratThreshold().c_str()); + _ui->checkBox_sprpautrat_nms->setObjectName(Parameters::kSuperPointRpautratNMS().c_str()); + _ui->spinBox_sprpautrat_minDistance->setObjectName(Parameters::kSuperPointRpautratNMSRadius().c_str()); + _ui->checkBox_sprpautrat_cuda->setObjectName(Parameters::kSuperPointRpautratCuda().c_str()); + // PyMatcher _ui->lineEdit_pymatcher_path->setObjectName(Parameters::kPyMatcherPath().c_str()); connect(_ui->toolButton_pymatcher_path, SIGNAL(clicked()), this, SLOT(changePyMatcherPath())); @@ -5701,6 +5715,41 @@ void PreferencesDialog::changeSuperPointModelPath() } } +void PreferencesDialog::changeSuperPointRpautratWeightsPath() +{ + QString path; + if(_ui->lineEdit_sprpautrat_weights_path->text().isEmpty()) + { + path = QFileDialog::getOpenFileName(this, tr("Select SuperPoint weights"), this->getWorkingDirectory(), tr("SuperPoint weights (*.pth)")); + } + else + { + path = QFileDialog::getOpenFileName(this, tr("Select SuperPoint weights"), _ui->lineEdit_sprpautrat_weights_path->text(), tr("SuperPoint weights (*.pth)")); + } + if(!path.isEmpty()) + { + _ui->lineEdit_sprpautrat_weights_path->setText(path); + } +} + +void PreferencesDialog::changeSuperPointRpautratModelPath() +{ + QString path; + if(_ui->lineEdit_sprpautrat_model_path->text().isEmpty()) + { + path = QFileDialog::getOpenFileName(this, tr("Select SuperPoint Python Model"), this->getWorkingDirectory(), tr("SuperPoint Python Model (*.py)")); + } + else + { + path = QFileDialog::getOpenFileName(this, tr("Select SuperPoint Python Model"), _ui->lineEdit_sprpautrat_model_path->text(), tr("SuperPoint Python Model (*.py)")); + } + if(!path.isEmpty()) + { + _ui->lineEdit_sprpautrat_model_path->setText(path); + } +} + + void PreferencesDialog::changePyMatcherPath() { QString path; diff --git a/guilib/src/ui/aboutDialog.ui b/guilib/src/ui/aboutDialog.ui index 0a6bfcbc..f35ced5d 100644 --- a/guilib/src/ui/aboutDialog.ui +++ b/guilib/src/ui/aboutDialog.ui @@ -162,9 +162,9 @@ p, li { white-space: pre-wrap; } 0 - -446 + 0 596 - 1211 + 1234 @@ -185,7 +185,37 @@ p, li { white-space: pre-wrap; } - + + + + With GridMap : + + + true + + + + + + + Apache v2 + + + true + + + + + + + With CCCoreLib : + + + true + + + + With Zed Open Capture : @@ -195,189 +225,28 @@ p, li { white-space: pre-wrap; } - - + + - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + With Python3 : true - - + + - With OpenVINS : + MIT true - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With cuVSLAM : - - - true - - - - - - - With OpenNI2 : - - - true - - - - - - - PSF - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With FOVIS : - - - true - - - - - - - PCL version : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With Octomap : - - - true - - - - - - - With Open3D : - - - true - - - - - - - With ORB SLAM 2 : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - + + GPLv3 @@ -386,20 +255,7 @@ p, li { white-space: pre-wrap; } - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - + @@ -412,18 +268,48 @@ p, li { white-space: pre-wrap; } - - + + - With RealSense : + With MYNTEYE S : true - - + + + + With FastCV : + + + true + + + + + + + With VINS-Fusion : + + + true + + + + + + + With PDAL : + + + true + + + + + @@ -435,8 +321,28 @@ p, li { white-space: pre-wrap; } - - + + + + GPLv2 + + + true + + + + + + + GPLv3 + + + true + + + + + @@ -448,7 +354,17 @@ p, li { white-space: pre-wrap; } - + + + + With OKVIS : + + + true + + + + MIT @@ -458,18 +374,8 @@ p, li { white-space: pre-wrap; } - - - - MIT - - - true - - - - - + + @@ -481,60 +387,30 @@ p, li { white-space: pre-wrap; } - - + + - With OpenNI : + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter true - - + + - With Zed SDK : + BSD true - - - - Penn Software License - - - true - - - - - - - <html><head/><body><p><span style=" font-weight:600;">Third Party Libraries</span></p></body></html> - - - Qt::RichText - - - true - - - - - - - Apache v2 and/or GPLv2 - - - true - - - - + Apache v2 and/or GPLv2 @@ -544,66 +420,27 @@ p, li { white-space: pre-wrap; } - - + + - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + With Octomap : true - - + + - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + With Open3D : true - - - - With stereo dc1394 : - - - true - - - - - - - GPLv3 - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - + With Freenect : @@ -613,18 +450,18 @@ p, li { white-space: pre-wrap; } - - + + - With MRPT : + MIT true - - + + @@ -636,13 +473,40 @@ p, li { white-space: pre-wrap; } - - + + - + GPLv3 - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + true + + + + + + + With cvsba : + + + true + + + + + + + With SuperPoint Torch : + + + true + + + + + + + BSD true @@ -650,16 +514,92 @@ p, li { white-space: pre-wrap; } - + - With Viso2 : + With FOVIS : true - + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With AliceVision : + + + true + + + + + + + BSD + + + true + + + + + + + With DVO : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With OpenChisel : + + + true + + + + + + + With Freenect2 : + + + true + + + + @@ -672,8 +612,236 @@ p, li { white-space: pre-wrap; } + + + + MIT + + + true + + + + + + + Penn Software License + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With DepthAI : + + + true + + + + + + + With OpenNI : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + BSD + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + Apache-2 + + + true + + + + + + + BSD + + + true + + + + + + + With Ceres : + + + true + + + + + + + With XVisio SDK : + + + true + + + - + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With OpenNI2 : + + + true + + + + + + + With cuVSLAM : + + + true + + + + + + + With TORO : + + + true + + + + + + + With libLAS : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + MIT + + + true + + + + + + + <html><head/><body><p><span style=" font-weight:600;">License</span></p></body></html> + + + true + + + + + @@ -698,73 +866,27 @@ p, li { white-space: pre-wrap; } - - + + - With libpointmatcher : + GPLv3 true - - + + - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + GPLv2 true - - - - With stereo FlyCapture2 : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - Creative Commons [Attribution-NonCommercial-ShareAlike] - - - true - - - - - - - Apache-2 - - - true - - - - + BSD @@ -774,7 +896,73 @@ p, li { white-space: pre-wrap; } - + + + + With Orbbec SDK : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With MSCKF : + + + true + + + + + + + Open Source or Commercial + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + MPL2 + + + true + + + + With loam_velodyne : @@ -784,28 +972,8 @@ p, li { white-space: pre-wrap; } - - - - BSD - - - true - - - - - - - BSD - - - true - - - - + @@ -817,18 +985,163 @@ p, li { white-space: pre-wrap; } + + + + BSD + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With CPU-TSDF : + + + true + + + + + + + With GTSAM : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + Creative Commons [Attribution-NonCommercial-ShareAlike] + + + true + + + + + + + Apache-2 + + + true + + + + + + + With RealSense : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + BSD + + + true + + + + + + + With OpenVINS : + + + true + + + - + - With DVO : + With Viso2 : true - - + + + + <html><head/><body><p><span style=" font-weight:600;">Third Party Libraries</span></p></body></html> + + + Qt::RichText + + + true + + + + + @@ -840,27 +1153,40 @@ p, li { white-space: pre-wrap; } - - + + - With PDAL : + LGPL true - - + + - With CCCoreLib : + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter true - + + + + With ORB SLAM 2 : + + + true + + + + @@ -873,8 +1199,47 @@ p, li { white-space: pre-wrap; } - - + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + BSD @@ -883,6 +1248,249 @@ p, li { white-space: pre-wrap; } + + + + With ORB Octree : + + + true + + + + + + + Qt version : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + PSF + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With stereo FlyCapture2 : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With K4A : + + + true + + + + + + + Apache v2 + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With MRPT : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + GPLv3 + + + true + + + + + + + Apache v2 and/or GPLv2 + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + MIT + + + true + + + @@ -893,8 +1501,8 @@ p, li { white-space: pre-wrap; } - - + + @@ -906,319 +1514,8 @@ p, li { white-space: pre-wrap; } - - - - With MYNTEYE S : - - - true - - - - - - - Apache v2 - - - true - - - - - - - With FastCV : - - - true - - - - - - - With XVisio SDK : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With GTSAM : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - GPLv2 - - - true - - - - - - - BSD - - - true - - - - - - - GPLv3 - - - true - - - - - - - MPL2 - - - true - - - - - - - VTK version : - - - true - - - - - - - BSD - - - true - - - - - - - MIT - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With g2o : - - - true - - - - - - - With RealSense2 : - - - true - - - - - - - BSD - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With OpenChisel : - - - true - - - - - - - BSD - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With AliceVision : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - GPLv3 - - - true - - - - - - - With Python3 : - - - true - - - - - - - With OKVIS : - - - true - - - - - - - With TORO : - - - true - - - - - + + @@ -1243,262 +1540,28 @@ p, li { white-space: pre-wrap; } - - + + - With SuperPoint Torch : + VTK version : true - - + + - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + PCL version : true - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - BSD - - - true - - - - - - - With CPU-TSDF : - - - true - - - - - - - With CudaSift : - - - true - - - - - - - Apache v2 - - - true - - - - - - - With DepthAI : - - - true - - - - - - - With Ceres : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - BSD - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - <html><head/><body><p><span style=" font-weight:600;">License</span></p></body></html> - - - true - - - - - - - With libLAS : - - - true - - - - - - - NVIDIA ISAAC ROS SOFTWARE LICENSE - - - true - - - - - - - GPLv3 - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With GridMap : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With ORB Octree : - - - true - - - - - - - MIT - - - true - - - - - + + BSD @@ -1508,7 +1571,7 @@ p, li { white-space: pre-wrap; } - + @@ -1520,18 +1583,28 @@ p, li { white-space: pre-wrap; } - - + + - GPLv2 + With stereo dc1394 : true - - + + + + With CudaSift : + + + true + + + + + @@ -1543,57 +1616,7 @@ p, li { white-space: pre-wrap; } - - - - GPLv2 - - - true - - - - - - - Apache-2 - - - true - - - - - - - GPLv3 - - - true - - - - - - - With cvsba : - - - true - - - - - - - With VINS-Fusion : - - - true - - - - + With K4W2 : @@ -1603,127 +1626,18 @@ p, li { white-space: pre-wrap; } - - + + - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + With RealSense2 : true - - - - BSD - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - MIT - - - true - - - - - - - Qt version : - - - true - - - - - - - LGPL - - - true - - - - - - - With MSCKF : - - - true - - - - - - - Open Source or Commercial - - - true - - - - - - - With K4A : - - - true - - - - - - - Apache v2 - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - + + GPLv3 @@ -1732,8 +1646,8 @@ p, li { white-space: pre-wrap; } - - + + BSD @@ -1742,28 +1656,18 @@ p, li { white-space: pre-wrap; } - - + + - With Freenect2 : + With libpointmatcher : true - - - - BSD - - - true - - - - - + + @@ -1775,8 +1679,41 @@ p, li { white-space: pre-wrap; } - - + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + NVIDIA ISAAC ROS SOFTWARE LICENSE + + + true + + + + + + + GPLv2 + + + true + + + + + BSD @@ -1785,7 +1722,17 @@ p, li { white-space: pre-wrap; } - + + + + BSD + + + true + + + + @@ -1798,27 +1745,7 @@ p, li { white-space: pre-wrap; } - - - - With Orbbec SDK : - - - true - - - - - - - MIT - - - true - - - - + @@ -1831,6 +1758,100 @@ p, li { white-space: pre-wrap; } + + + + BSD + + + true + + + + + + + With Zed SDK : + + + true + + + + + + + GPLv3 + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + Apache v2 + + + true + + + + + + + With g2o : + + + true + + + + + + + BSD + + + true + + + + + + + With SuperPoint Rpautrat : + + + + + + + + + + + + + + MIT + + + diff --git a/guilib/src/ui/preferencesDialog.ui b/guilib/src/ui/preferencesDialog.ui index f355ed6c..aee9a8f0 100644 --- a/guilib/src/ui/preferencesDialog.ui +++ b/guilib/src/ui/preferencesDialog.ui @@ -11232,6 +11232,11 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag PyDetector + + + Superpoint Rpautrat + + @@ -23491,6 +23496,11 @@ Lower the ratio -> higher the precision. PyDetector + + + SuperPoint Rpautrat + + @@ -27861,6 +27871,183 @@ Lower the ratio -> higher the precision. + + + + + + SuperPoint Rpautrat + + + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">SuperPoint C++ implementation from the <a href="https://github.com/rpautrat/SuperPoint"><span style=" text-decoration: underline; color:#0000ff;">rpautrat/SuperPoint</span></a> project.</p> +<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A &quot;pt&quot; model file with same name than the &quot;pth&quot; file will be generated automatically in RTAB-Map's working directory so that the model can be called by c++ libtorch directly.</p> +<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">RTAB-Map must be built with libtorch and python to use this feature.<br /></p></body></html> + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + + + + + + + + + + + + + [Required] SuperPoint weights file (*.pth). + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Detector response threshold to accept keypoint. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Use Cuda device for Torch, otherwise CPU device is used by default. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + ... + + + + + + + 4 + + + 0.000100000000000 + + + 1.000000000000000 + + + 0.001000000000000 + + + 0.001000000000000 + + + + + + + If true, non-maximum suppression is applied to detected keypoints. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + + + + + Minimum distance (pixels) between keypoints (when non-maximum suppression is enabled). + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + [Required] SuperPoint python model file (superpoint_pytorch.py). + + + + + + + + + + ... + + + + + + + + + + + + Qt::Vertical + + + + 20 + 4561 + + + + + + From e9c6180d5629e6f3a2c3f8700ba3cce6f76f7a23 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sat, 8 Nov 2025 10:58:34 -0800 Subject: [PATCH 050/126] Prioritize neighbor links in Optimizer::getConnectedGraph() (#1610) * Prioritize neighbor links in Optimizer::getConnectedGraph() to avoid odometry jumps. * Fixed landmark order * report: added search for first valid id * Fixed non-neighbor comparison logic * RGBD/LocalizationPriorError can now be 0 (disabled) to avoid using priors to fix the graph (https://github.com/introlab/rtabmap_ros/issues/1371) * amend previous commit --- corelib/include/rtabmap/core/Parameters.h | 2 +- corelib/src/Optimizer.cpp | 135 ++++++++++++++-------- corelib/src/Rtabmap.cpp | 28 +++-- corelib/src/optimizer/OptimizerG2O.cpp | 4 +- guilib/src/ui/preferencesDialog.ui | 2 +- tools/Report/main.cpp | 9 ++ 6 files changed, 120 insertions(+), 60 deletions(-) diff --git a/corelib/include/rtabmap/core/Parameters.h b/corelib/include/rtabmap/core/Parameters.h index aba8a89d..672e1ff0 100644 --- a/corelib/include/rtabmap/core/Parameters.h +++ b/corelib/include/rtabmap/core/Parameters.h @@ -399,7 +399,7 @@ class RTABMAP_CORE_EXPORT Parameters RTABMAP_PARAM(RGBD, LoopCovLimited, bool, false, "Limit covariance of non-neighbor links to minimum covariance of neighbor links. In other words, if covariance of a loop closure link is smaller than the minimum covariance of odometry links, its covariance is set to minimum covariance of odometry links."); RTABMAP_PARAM(RGBD, MaxOdomCacheSize, int, 10, uFormat("Maximum odometry cache size. Used only in localization mode (when %s=false). This is used to get smoother localizations and to verify localization transforms (when %s!=0) to make sure we don't teleport to a location very similar to one we previously localized on. Set 0 to disable caching.", kMemIncrementalMemory().c_str(), kRGBDOptimizeMaxError().c_str())); RTABMAP_PARAM(RGBD, LocalizationSmoothing, bool, true, uFormat("Adjust localization constraints based on optimized odometry cache poses (when %s>0).", kRGBDMaxOdomCacheSize().c_str())); - RTABMAP_PARAM(RGBD, LocalizationPriorError, double, 0.001, uFormat("The corresponding variance (error x error) set to priors of the map's poses during localization (when %s>0).", kRGBDMaxOdomCacheSize().c_str())); + RTABMAP_PARAM(RGBD, LocalizationPriorError, double, 0.001, uFormat("The corresponding variance (error x error) set to priors of the map's poses during localization (when %s>0). Set to 0 to only fix the first map's pose linked in odometry cache (i.e., other map's poses will be allowed to move during the optimization).", kRGBDMaxOdomCacheSize().c_str())); RTABMAP_PARAM(RGBD, LocalizationSecondTryWithoutProximityLinks, bool, true, uFormat("When localization is rejected by graph optimization validation, try a second time without proximity links if landmark or loop closure links are also present in odometry cache (see %s). If it succeeds, the proximity links are removed. This assumes that global loop closure and landmark links are more accurate than proximity links.", kRGBDMaxOdomCacheSize().c_str())); // Local/Proximity loop closure detection diff --git a/corelib/src/Optimizer.cpp b/corelib/src/Optimizer.cpp index 96a070fa..4f2197b2 100644 --- a/corelib/src/Optimizer.cpp +++ b/corelib/src/Optimizer.cpp @@ -185,6 +185,48 @@ Optimizer * Optimizer::create(Optimizer::Type type, const ParametersMap & parame return optimizer; } +class LinkIdKey +{ +public: +LinkIdKey(int id, Link::Type type) : + id_(id), + type_(type) {} + bool operator<(const LinkIdKey & k) const + { + // landmark, sort by smallest to largest landmark id, after normal links + if(id_ < 0 && k.id_ < 0) + { + return id_ > k.id_; + } + else if(id_ < 0) { + return false; + } + else if(k.id_ < 0) { + return true; + } + + if(type_ == Link::kNeighbor && k.type_ != Link::kNeighbor) + { + return true; + } + else if(type_ != Link::kNeighbor && k.type_ == Link::kNeighbor) + { + return false; + } + else if(type_ == Link::kNeighborMerged && k.type_ != Link::kNeighbor && k.type_ != Link::kNeighborMerged) + { + return true; + } + else + { + // normal link, sort by smallest to largest id + return id_ < k.id_; + } + } + int id_; + Link::Type type_; +}; + void Optimizer::getConnectedGraph( int fromId, const std::map & posesIn, @@ -199,8 +241,8 @@ void Optimizer::getConnectedGraph( posesOut.clear(); linksOut.clear(); - std::set nextPoses; - nextPoses.insert(fromId); + std::map nextPoses; + nextPoses.insert(std::make_pair(LinkIdKey(fromId, Link::kUndef), posesIn.find(fromId)->second)); std::multimap > biLinks; for(std::multimap::const_iterator iter=linksIn.begin(); iter!=linksIn.end(); ++iter) { @@ -216,20 +258,25 @@ void Optimizer::getConnectedGraph( while(nextPoses.size()) { - int currentId = *nextPoses.rbegin(); // fill up all nodes before landmarks - nextPoses.erase(*nextPoses.rbegin()); + // Fill up all nodes before landmarks + // For nodes, fill up all neightbor nodes before loop closure ones + int currentId = nextPoses.begin()->first.id_; + Transform currentPose = nextPoses.begin()->second; + nextPoses.erase(nextPoses.begin()); - if(posesOut.empty()) + if(posesOut.find(currentId) != posesOut.end()) { + // Already added from priority list + continue; + } + + posesOut.insert(std::make_pair(currentId, currentPose)); + + // add prior links + for(std::multimap::const_iterator pter=linksIn.find(currentId); pter!=linksIn.end() && pter->first==currentId; ++pter) { - posesOut.insert(std::make_pair(currentId, posesIn.find(currentId)->second)); - - // add prior links - for(std::multimap::const_iterator pter=linksIn.find(currentId); pter!=linksIn.end() && pter->first==currentId; ++pter) + if(pter->second.from() == pter->second.to() && (!priorsIgnored() || pter->second.type() != Link::kPosePrior)) { - if(pter->second.from() == pter->second.to() && (!priorsIgnored() || pter->second.type() != Link::kPosePrior)) - { - linksOut.insert(*pter); - } + linksOut.insert(*pter); } } @@ -240,52 +287,42 @@ void Optimizer::getConnectedGraph( if(posesIn.find(toId) != posesIn.end() && (!landmarksIgnored() || toId>0)) { std::multimap::const_iterator kter = graph::findLink(linksIn, currentId, toId, true, type); - if(nextPoses.find(toId) == nextPoses.end()) + UASSERT(kter!=linksIn.end()); + if(!uContains(posesOut, toId)) { - if(!uContains(posesOut, toId)) + const Transform & poseToIn = posesIn.at(toId); + Transform t = kter->second.from()==currentId?kter->second.transform():kter->second.transform().inverse(); + Transform pose; + if(isSlam2d() && kter->second.type() == Link::kLandmark && toId>0 && (poseToIn.is3DoF() || poseToIn.is4DoF())) { - const Transform & poseToIn = posesIn.at(toId); - Transform t = kter->second.from()==currentId?kter->second.transform():kter->second.transform().inverse(); - if(isSlam2d() && kter->second.type() == Link::kLandmark && toId>0 && (poseToIn.is3DoF() || poseToIn.is4DoF())) + if(poseToIn.is3DoF()) { - if(poseToIn.is3DoF()) - { - posesOut.insert(std::make_pair(toId, (posesOut.at(currentId) * t).to3DoF())); - } - else - { - posesOut.insert(std::make_pair(toId, (posesOut.at(currentId) * t).to4DoF())); - } + pose = (posesOut.at(currentId) * t).to3DoF(); } else { - posesOut.insert(std::make_pair(toId, posesOut.at(currentId)* t)); + pose = (posesOut.at(currentId) * t).to4DoF(); } - - // add prior links - for(std::multimap::const_iterator pter=linksIn.find(toId); pter!=linksIn.end() && pter->first==toId; ++pter) - { - if(pter->second.from() == pter->second.to() && (!priorsIgnored() || pter->second.type() != Link::kPosePrior)) - { - linksOut.insert(*pter); - } - } - - nextPoses.insert(toId); + } + else + { + pose = posesOut.at(currentId)* t; } - // only add unique links - if(graph::findLink(linksOut, currentId, toId, true, kter->second.type()) == linksOut.end()) + nextPoses.insert(std::make_pair(LinkIdKey(toId, type), pose)); + } + + // only add unique links + if(graph::findLink(linksOut, currentId, toId, true, kter->second.type()) == linksOut.end()) + { + if(kter->second.to() < 0) { - if(kter->second.to() < 0) - { - // For landmarks, make sure fromId is the landmark - linksOut.insert(std::make_pair(kter->second.to(), kter->second.inverse())); - } - else - { - linksOut.insert(*kter); - } + // For landmarks, make sure fromId is the landmark + linksOut.insert(std::make_pair(kter->second.to(), kter->second.inverse())); + } + else + { + linksOut.insert(*kter); } } } diff --git a/corelib/src/Rtabmap.cpp b/corelib/src/Rtabmap.cpp index 5bf76e83..b7b13a4b 100644 --- a/corelib/src/Rtabmap.cpp +++ b/corelib/src/Rtabmap.cpp @@ -631,8 +631,8 @@ void Rtabmap::parseParameters(const ParametersMap & parameters) Parameters::parse(parameters, Parameters::kRGBDLocalizationSmoothing(), _localizationSmoothing); double localizationPriorError = Parameters::defaultRGBDLocalizationPriorError(); Parameters::parse(parameters, Parameters::kRGBDLocalizationPriorError(), localizationPriorError); - UASSERT(localizationPriorError>0.0); - _localizationPriorInf = 1.0/(localizationPriorError*localizationPriorError); + UASSERT(localizationPriorError>=0.0); + _localizationPriorInf = localizationPriorError>0?1.0/(localizationPriorError*localizationPriorError):0.0; Parameters::parse(parameters, Parameters::kRGBDLocalizationSecondTryWithoutProximityLinks(), _localizationSecondTryWithoutProximityLinks); Parameters::parse(parameters, Parameters::kRGBDProximityGlobalScanMap(), _createGlobalScanMap); @@ -3258,25 +3258,39 @@ bool Rtabmap::process( { constraints.insert(std::make_pair(iter->second.from(), iter->second)); } + cv::Mat priorInfMat = cv::Mat::eye(6,6, CV_64FC1)*_localizationPriorInf; + std::list addedPriors; for(std::multimap::iterator iter=constraints.begin(); iter!=constraints.end(); ++iter) { std::map::iterator iterPose = _optimizedPoses.find(iter->second.to()); if(iterPose != _optimizedPoses.end() && poses.find(iterPose->first) == poses.end()) { poses.insert(*iterPose); - // make the poses in the map fixed - constraints.insert(std::make_pair(iterPose->first, Link(iterPose->first, iterPose->first, Link::kPosePrior, iterPose->second, priorInfMat))); - UDEBUG("Constraint %d->%d: %s (type=%s, var=%f)", iterPose->first, iterPose->first, iterPose->second.prettyPrint().c_str(), Link::typeName(Link::kPosePrior).c_str(), 1./_localizationPriorInf); + if(_localizationPriorInf > 0) + { + // make the poses in the map fixed + constraints.insert(std::make_pair(iterPose->first, Link(iterPose->first, iterPose->first, Link::kPosePrior, iterPose->second, priorInfMat))); + UDEBUG("Constraint %d->%d: %s (type=%s, var=%f)", iterPose->first, iterPose->first, iterPose->second.prettyPrint().c_str(), Link::typeName(Link::kPosePrior).c_str(), 1./_localizationPriorInf); + addedPriors.push_back(iterPose->first); + } } UDEBUG("Constraint %d->%d: %s (type=%s, var = %f %f)", iter->second.from(), iter->second.to(), iter->second.transform().prettyPrint().c_str(), iter->second.typeName().c_str(), iter->second.transVariance(), iter->second.rotVariance()); } + if(addedPriors.size() == 1) { + // When there is only one map node, remove the prior to use fixed constraint in g2o (https://github.com/introlab/rtabmap_ros/issues/1371) + UDEBUG("Currently localizing on a single map node, removing prior on %d", addedPriors.front()); + constraints.erase(graph::findLink(constraints, addedPriors.front(), addedPriors.front(), false, Link::kPosePrior)); + } std::map posesOut; std::multimap edgeConstraintsOut; bool priorsIgnored = _graphOptimizer->priorsIgnored(); - UDEBUG("priorsIgnored was %s", priorsIgnored?"true":"false"); - _graphOptimizer->setPriorsIgnored(false); //temporary set false to use priors above to fix nodes of the map + if(_localizationPriorInf > 0) + { + UDEBUG("priorsIgnored was %s", priorsIgnored?"true":"false"); + _graphOptimizer->setPriorsIgnored(false); //temporary set false to use priors above to fix nodes of the map + } // If slam2d: get connected graph while keeping original roll,pitch,z values. _graphOptimizer->getConnectedGraph(signature->id(), poses, constraints, posesOut, edgeConstraintsOut); if(ULogger::level() == ULogger::kDebug) diff --git a/corelib/src/optimizer/OptimizerG2O.cpp b/corelib/src/optimizer/OptimizerG2O.cpp index 1fbb852d..3a6950a4 100644 --- a/corelib/src/optimizer/OptimizerG2O.cpp +++ b/corelib/src/optimizer/OptimizerG2O.cpp @@ -1004,8 +1004,8 @@ std::map OptimizerG2O::optimize( g2o::EdgeSE3 * e = new g2o::EdgeSE3(); g2o::VertexSE3* v1 = (g2o::VertexSE3*)optimizer.vertex(id1); g2o::VertexSE3* v2 = (g2o::VertexSE3*)optimizer.vertex(id2); - UASSERT(v1 != 0); - UASSERT(v2 != 0); + UASSERT_MSG(v1 != 0, uFormat("v1=%d v2=%d", id1, id2).c_str()); + UASSERT_MSG(v2 != 0, uFormat("v1=%d v2=%d", id1, id2).c_str()); e->setVertex(0, v1); e->setVertex(1, v2); e->setMeasurement(constraint); diff --git a/guilib/src/ui/preferencesDialog.ui b/guilib/src/ui/preferencesDialog.ui index aee9a8f0..887a35db 100644 --- a/guilib/src/ui/preferencesDialog.ui +++ b/guilib/src/ui/preferencesDialog.ui @@ -12888,7 +12888,7 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - Localization prior error. Used only in localization mode. The corresponding variance (error x error) set to priors of the map's poses during localization. + Localization prior error. Used only in localization mode. The corresponding variance (error x error) set to priors of the map's poses during localization. Set to 0 to only fix the first map's pose linked in odometry cache (i.e., other map's poses will be allowed to move during the optimization). true diff --git a/tools/Report/main.cpp b/tools/Report/main.cpp index 88eea111..51cbdab0 100644 --- a/tools/Report/main.cpp +++ b/tools/Report/main.cpp @@ -908,7 +908,16 @@ int main(int argc, char * argv[]) { std::map posesOut; std::multimap linksOut; + // Find first valid id int firstId = *ids.begin(); + for(std::multimap::iterator iter=links.begin(); iter!=links.end(); ++iter) + { + if(iter->second.type() == Link::kNeighbor || iter->second.type() == Link::kNeighborMerged) + { + firstId = iter->first; + break; + } + } rtabmap::Optimizer * optimizer = rtabmap::Optimizer::create(params); bool useOdomGravity = Parameters::defaultMemUseOdomGravity(); Parameters::parse(params, Parameters::kMemUseOdomGravity(), useOdomGravity); From e3a60c867210d1d8a7b44abe046c2f9229aafa94 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 9 Nov 2025 09:24:42 -0800 Subject: [PATCH 051/126] Set back min RGBD/LocalizationPriorError to 0.0001 (rtabmap_ros#1371) --- corelib/include/rtabmap/core/Parameters.h | 2 +- corelib/src/Rtabmap.cpp | 25 ++++++++++------------- guilib/src/ui/preferencesDialog.ui | 9 +++++--- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/corelib/include/rtabmap/core/Parameters.h b/corelib/include/rtabmap/core/Parameters.h index 672e1ff0..aba8a89d 100644 --- a/corelib/include/rtabmap/core/Parameters.h +++ b/corelib/include/rtabmap/core/Parameters.h @@ -399,7 +399,7 @@ class RTABMAP_CORE_EXPORT Parameters RTABMAP_PARAM(RGBD, LoopCovLimited, bool, false, "Limit covariance of non-neighbor links to minimum covariance of neighbor links. In other words, if covariance of a loop closure link is smaller than the minimum covariance of odometry links, its covariance is set to minimum covariance of odometry links."); RTABMAP_PARAM(RGBD, MaxOdomCacheSize, int, 10, uFormat("Maximum odometry cache size. Used only in localization mode (when %s=false). This is used to get smoother localizations and to verify localization transforms (when %s!=0) to make sure we don't teleport to a location very similar to one we previously localized on. Set 0 to disable caching.", kMemIncrementalMemory().c_str(), kRGBDOptimizeMaxError().c_str())); RTABMAP_PARAM(RGBD, LocalizationSmoothing, bool, true, uFormat("Adjust localization constraints based on optimized odometry cache poses (when %s>0).", kRGBDMaxOdomCacheSize().c_str())); - RTABMAP_PARAM(RGBD, LocalizationPriorError, double, 0.001, uFormat("The corresponding variance (error x error) set to priors of the map's poses during localization (when %s>0). Set to 0 to only fix the first map's pose linked in odometry cache (i.e., other map's poses will be allowed to move during the optimization).", kRGBDMaxOdomCacheSize().c_str())); + RTABMAP_PARAM(RGBD, LocalizationPriorError, double, 0.001, uFormat("The corresponding variance (error x error) set to priors of the map's poses during localization (when %s>0).", kRGBDMaxOdomCacheSize().c_str())); RTABMAP_PARAM(RGBD, LocalizationSecondTryWithoutProximityLinks, bool, true, uFormat("When localization is rejected by graph optimization validation, try a second time without proximity links if landmark or loop closure links are also present in odometry cache (see %s). If it succeeds, the proximity links are removed. This assumes that global loop closure and landmark links are more accurate than proximity links.", kRGBDMaxOdomCacheSize().c_str())); // Local/Proximity loop closure detection diff --git a/corelib/src/Rtabmap.cpp b/corelib/src/Rtabmap.cpp index b7b13a4b..5e0ef366 100644 --- a/corelib/src/Rtabmap.cpp +++ b/corelib/src/Rtabmap.cpp @@ -631,8 +631,8 @@ void Rtabmap::parseParameters(const ParametersMap & parameters) Parameters::parse(parameters, Parameters::kRGBDLocalizationSmoothing(), _localizationSmoothing); double localizationPriorError = Parameters::defaultRGBDLocalizationPriorError(); Parameters::parse(parameters, Parameters::kRGBDLocalizationPriorError(), localizationPriorError); - UASSERT(localizationPriorError>=0.0); - _localizationPriorInf = localizationPriorError>0?1.0/(localizationPriorError*localizationPriorError):0.0; + UASSERT(localizationPriorError>0.0); + _localizationPriorInf = 1.0/(localizationPriorError*localizationPriorError); Parameters::parse(parameters, Parameters::kRGBDLocalizationSecondTryWithoutProximityLinks(), _localizationSecondTryWithoutProximityLinks); Parameters::parse(parameters, Parameters::kRGBDProximityGlobalScanMap(), _createGlobalScanMap); @@ -3267,13 +3267,11 @@ bool Rtabmap::process( if(iterPose != _optimizedPoses.end() && poses.find(iterPose->first) == poses.end()) { poses.insert(*iterPose); - if(_localizationPriorInf > 0) - { - // make the poses in the map fixed - constraints.insert(std::make_pair(iterPose->first, Link(iterPose->first, iterPose->first, Link::kPosePrior, iterPose->second, priorInfMat))); - UDEBUG("Constraint %d->%d: %s (type=%s, var=%f)", iterPose->first, iterPose->first, iterPose->second.prettyPrint().c_str(), Link::typeName(Link::kPosePrior).c_str(), 1./_localizationPriorInf); - addedPriors.push_back(iterPose->first); - } + + // make the poses in the map fixed + constraints.insert(std::make_pair(iterPose->first, Link(iterPose->first, iterPose->first, Link::kPosePrior, iterPose->second, priorInfMat))); + UDEBUG("Constraint %d->%d: %s (type=%s, var=%f)", iterPose->first, iterPose->first, iterPose->second.prettyPrint().c_str(), Link::typeName(Link::kPosePrior).c_str(), 1./_localizationPriorInf); + addedPriors.push_back(iterPose->first); } UDEBUG("Constraint %d->%d: %s (type=%s, var = %f %f)", iter->second.from(), iter->second.to(), iter->second.transform().prettyPrint().c_str(), iter->second.typeName().c_str(), iter->second.transVariance(), iter->second.rotVariance()); } @@ -3285,12 +3283,11 @@ bool Rtabmap::process( std::map posesOut; std::multimap edgeConstraintsOut; + bool priorsIgnored = _graphOptimizer->priorsIgnored(); - if(_localizationPriorInf > 0) - { - UDEBUG("priorsIgnored was %s", priorsIgnored?"true":"false"); - _graphOptimizer->setPriorsIgnored(false); //temporary set false to use priors above to fix nodes of the map - } + UDEBUG("priorsIgnored was %s", priorsIgnored?"true":"false"); + _graphOptimizer->setPriorsIgnored(false); //temporary set false to use priors above to fix nodes of the map + // If slam2d: get connected graph while keeping original roll,pitch,z values. _graphOptimizer->getConnectedGraph(signature->id(), poses, constraints, posesOut, edgeConstraintsOut); if(ULogger::level() == ULogger::kDebug) diff --git a/guilib/src/ui/preferencesDialog.ui b/guilib/src/ui/preferencesDialog.ui index 887a35db..d71bf025 100644 --- a/guilib/src/ui/preferencesDialog.ui +++ b/guilib/src/ui/preferencesDialog.ui @@ -63,9 +63,9 @@ 0 - -2651 + -949 713 - 4779 + 4922 @@ -95,7 +95,7 @@ QFrame::Raised - 5 + 12 @@ -13326,6 +13326,9 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag 4 + + 0.000100000000000 + 1.000000000000000 From c97f0c10dd44d8cabdd751000b615d6f4e8e95e5 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Mon, 10 Nov 2025 09:09:39 -0800 Subject: [PATCH 052/126] Graph: reading pose format 10 or 12 from files saved with format 11 should work --- corelib/src/Graph.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/corelib/src/Graph.cpp b/corelib/src/Graph.cpp index 8bbcf660..5c2b5c10 100644 --- a/corelib/src/Graph.cpp +++ b/corelib/src/Graph.cpp @@ -451,9 +451,12 @@ bool importPoses( } double stamp = uStr2Double(strList.front()); strList.pop_front(); - if(format==11) + if(strList.size() == 8 && (format==10 || format==11 || format==12)) { - id = uStr2Int(strList.back()); + if(format==11) + { + id = uStr2Int(strList.back()); + } strList.pop_back(); } str = uJoin(strList, " "); From e612d103bfd3672b66d8e138adb185b9e8c2da24 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Tue, 11 Nov 2025 14:09:13 -0800 Subject: [PATCH 053/126] ORB-SLAM3 IMU support fixes (#1612) * Working IMU_RGBD and IMU_STEREO. Changing Inter IMU base frame is now possible. * Output error msg when vocabulary path is wrong. * Showing full local feature map --- cmake_modules/FindORB_SLAM.cmake | 7 ++- corelib/include/rtabmap/core/Camera.h | 5 +- corelib/include/rtabmap/core/Parameters.h | 4 +- corelib/src/Camera.cpp | 15 ++++- corelib/src/Parameters.cpp | 4 +- corelib/src/odometry/OdometryORBSLAM3.cpp | 75 ++++++++++++++++------- guilib/src/PreferencesDialog.cpp | 12 ++-- guilib/src/ui/preferencesDialog.ui | 51 ++++++++++----- 8 files changed, 120 insertions(+), 53 deletions(-) diff --git a/cmake_modules/FindORB_SLAM.cmake b/cmake_modules/FindORB_SLAM.cmake index f9ccdc0d..aa5e1124 100644 --- a/cmake_modules/FindORB_SLAM.cmake +++ b/cmake_modules/FindORB_SLAM.cmake @@ -11,6 +11,7 @@ find_path(ORB_SLAM_INCLUDE_DIR NAMES System.h PATHS $ENV{ORB_SLAM_ROOT_DIR}/include) find_library(ORB_SLAM2_LIBRARY NAMES ORB_SLAM2 PATHS $ENV{ORB_SLAM_ROOT_DIR}/lib) find_library(ORB_SLAM3_LIBRARY NAMES ORB_SLAM3 PATHS $ENV{ORB_SLAM_ROOT_DIR}/lib) +find_path(DBoW2_INCLUDE_DIR NAMES DBoW2/BowVector.h PATHS $ENV{ORB_SLAM_ROOT_DIR}/Thirdparty/DBoW2 NO_DEFAULT_PATH) find_path(g2o_INCLUDE_DIR NAMES g2o/core/sparse_optimizer.h PATHS $ENV{ORB_SLAM_ROOT_DIR}/Thirdparty/g2o NO_DEFAULT_PATH) find_path(sophus_INCLUDE_DIR NAMES sophus/se3.hpp PATHS $ENV{ORB_SLAM_ROOT_DIR}/Thirdparty/Sophus NO_DEFAULT_PATH) find_library(g2o_LIBRARY NAMES g2o PATHS $ENV{ORB_SLAM_ROOT_DIR}/Thirdparty/g2o/lib NO_DEFAULT_PATH) @@ -22,9 +23,9 @@ IF(ORB_SLAM2_LIBRARY) ELSEIF(ORB_SLAM3_LIBRARY) SET(ORB_SLAM_VERSION 3) SET(ORB_SLAM_LIBRARY ${ORB_SLAM3_LIBRARY}) - IF(g2o_INCLUDE_DIR AND sophus_INCLUDE_DIR) # ORB_SLAM3 v1 - SET(g2o_INCLUDE_DIR ${g2o_INCLUDE_DIR} ${sophus_INCLUDE_DIR}) - ENDIF(g2o_INCLUDE_DIR AND sophus_INCLUDE_DIR) + IF(g2o_INCLUDE_DIR AND sophus_INCLUDE_DIR AND DBoW2_INCLUDE_DIR) # ORB_SLAM3 v1 + SET(g2o_INCLUDE_DIR ${g2o_INCLUDE_DIR} ${sophus_INCLUDE_DIR} ${DBoW2_INCLUDE_DIR} $ENV{ORB_SLAM_ROOT_DIR}) + ENDIF(g2o_INCLUDE_DIR AND sophus_INCLUDE_DIR AND DBoW2_INCLUDE_DIR) ENDIF() IF (ORB_SLAM_INCLUDE_DIR AND ORB_SLAM_LIBRARY AND DBoW2_LIBRARY AND g2o_INCLUDE_DIR AND g2o_LIBRARY) diff --git a/corelib/include/rtabmap/core/Camera.h b/corelib/include/rtabmap/core/Camera.h index a267f140..a76ea3c9 100644 --- a/corelib/include/rtabmap/core/Camera.h +++ b/corelib/include/rtabmap/core/Camera.h @@ -38,7 +38,7 @@ class IMUFilter; /** * Class Camera - * + * */ class RTABMAP_CORE_EXPORT Camera : public SensorCapture { @@ -48,7 +48,7 @@ public: SensorData takeImage(SensorCaptureInfo * info = 0) {return takeData(info);} float getImageRate() const {return getFrameRate();} void setImageRate(float imageRate) {setFrameRate(imageRate);} - void setInterIMUPublishing(bool enabled, IMUFilter * filter = 0); // Take ownership of filter + void setInterIMUPublishing(bool enabled, IMUFilter * filter = 0, bool baseFrameConversion = false); // Take ownership of filter bool isInterIMUPublishing() const {return publishInterIMU_;} bool initFromFile(const std::string & calibrationPath); @@ -73,6 +73,7 @@ private: private: IMUFilter * imuFilter_; bool publishInterIMU_; + bool imuBaseFrameConversion_; }; diff --git a/corelib/include/rtabmap/core/Parameters.h b/corelib/include/rtabmap/core/Parameters.h index aba8a89d..a63d4b62 100644 --- a/corelib/include/rtabmap/core/Parameters.h +++ b/corelib/include/rtabmap/core/Parameters.h @@ -463,7 +463,7 @@ class RTABMAP_CORE_EXPORT Parameters RTABMAP_PARAM(GTSAM, IncRelinearizeSkip, int, 1, "Only relinearize any variables every X calls to ISAM2::update(). See GTSAM::ISAM2 doc for more info."); // Odometry - RTABMAP_PARAM(Odom, Strategy, int, 0, "0=Frame-to-Map (F2M) 1=Frame-to-Frame (F2F) 2=Fovis 3=viso2 4=DVO-SLAM 5=ORB_SLAM2 6=OKVIS 7=LOAM 8=MSCKF_VIO 9=VINS-Fusion 10=OpenVINS 11=FLOAM 12=Open3D 13=cuVSLAM"); + RTABMAP_PARAM(Odom, Strategy, int, 0, "0=Frame-to-Map (F2M) 1=Frame-to-Frame (F2F) 2=Fovis 3=viso2 4=DVO-SLAM 5=ORB_SLAM 6=OKVIS 7=LOAM 8=MSCKF_VIO 9=VINS-Fusion 10=OpenVINS 11=FLOAM 12=Open3D 13=cuVSLAM"); RTABMAP_PARAM(Odom, ResetCountdown, int, 0, "Automatically reset odometry after X consecutive images where odometry cannot be computed (a value of 0 disables auto-reset). When a reset occurs, odometry resumes from the last successfully computed pose with large covariance to trigger a new map. If external odometry is used, it will also be reset based on the motion estimated relative to the last computed pose but no large covariance will be received, so that a new map won't be triggered."); RTABMAP_PARAM(Odom, Holonomic, bool, true, "If the robot is holonomic (strafing commands can be issued). If not, y value will be estimated from x and yaw values (y=x*tan(yaw))."); RTABMAP_PARAM(Odom, FillInfoData, bool, true, "Fill info with data (inliers/outliers features)."); @@ -558,7 +558,7 @@ class RTABMAP_CORE_EXPORT Parameters RTABMAP_PARAM(OdomViso2, BucketWidth, double, 50, "Width of bucket."); RTABMAP_PARAM(OdomViso2, BucketHeight, double, 50, "Height of bucket."); - // Odometry ORB_SLAM2 + // Odometry ORB_SLAM RTABMAP_PARAM_STR(OdomORBSLAM, VocPath, "", "Path to ORB vocabulary (*.txt)."); RTABMAP_PARAM(OdomORBSLAM, Bf, double, 0.076, "Fake IR projector baseline (m) used only when stereo is not used."); RTABMAP_PARAM(OdomORBSLAM, ThDepth, double, 40.0, "Close/Far threshold. Baseline times."); diff --git a/corelib/src/Camera.cpp b/corelib/src/Camera.cpp index b9503c85..b95b0e93 100644 --- a/corelib/src/Camera.cpp +++ b/corelib/src/Camera.cpp @@ -39,7 +39,8 @@ namespace rtabmap Camera::Camera(float imageRate, const Transform & localTransform) : SensorCapture(imageRate, localTransform*CameraModel::opticalRotation()), imuFilter_(0), - publishInterIMU_(false) + publishInterIMU_(false), + imuBaseFrameConversion_(false) {} Camera::~Camera() @@ -52,15 +53,23 @@ bool Camera::initFromFile(const std::string & calibrationPath) return init(UDirectory::getDir(calibrationPath), uSplit(UFile::getName(calibrationPath), '.').front()); } -void Camera::setInterIMUPublishing(bool enabled, IMUFilter * filter) +void Camera::setInterIMUPublishing(bool enabled, IMUFilter * filter, bool baseFrameConversion) { publishInterIMU_ = enabled; delete imuFilter_; imuFilter_ = filter; + imuBaseFrameConversion_ = baseFrameConversion; } -void Camera::postInterIMU(const IMU & imu, double stamp) +void Camera::postInterIMU(const IMU & imu_in, double stamp) { + IMU imu = imu_in; + if(imuBaseFrameConversion_) + { + UASSERT(!imu.localTransform().isNull()); + imu.convertToBaseFrame(); + } + if(imuFilter_) { imuFilter_->update( diff --git a/corelib/src/Parameters.cpp b/corelib/src/Parameters.cpp index 0d4f8969..4eb501e6 100644 --- a/corelib/src/Parameters.cpp +++ b/corelib/src/Parameters.cpp @@ -1119,8 +1119,8 @@ ParametersMap Parameters::parseArguments(int argc, char * argv[], bool onlyParam ignore = true; } #endif -#ifndef RTABMAP_ORBSLAM2 - if(group.compare("OdomORBSLAM2") == 0) +#ifndef RTABMAP_ORB_SLAM + if(group.compare("OdomORBSLAM") == 0) { ignore = true; } diff --git a/corelib/src/odometry/OdometryORBSLAM3.cpp b/corelib/src/odometry/OdometryORBSLAM3.cpp index 7cde70a5..73d47e43 100644 --- a/corelib/src/odometry/OdometryORBSLAM3.cpp +++ b/corelib/src/odometry/OdometryORBSLAM3.cpp @@ -32,6 +32,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/utilite/UTimer.h" #include "rtabmap/utilite/UStl.h" #include "rtabmap/utilite/UDirectory.h" +#include "rtabmap/utilite/UFile.h" #include #include #include @@ -116,6 +117,13 @@ bool OdometryORBSLAM3::init(const rtabmap::CameraModel & model1, const rtabmap:: } //Load ORB Vocabulary vocabularyPath = uReplaceChar(vocabularyPath, '~', UDirectory::homeDir()); + if(!UFile::exists(vocabularyPath)) + { + UERROR("ORB_SLAM vocabulary path \"%s\" doesn't exist! (Parameter name=\"%s\")", + vocabularyPath.c_str(), + rtabmap::Parameters::kOdomORBSLAMVocPath().c_str()); + return false; + } UWARN("Loading ORB Vocabulary: \"%s\". This could take a while...", vocabularyPath.c_str()); // Create configuration file @@ -240,7 +248,7 @@ bool OdometryORBSLAM3::init(const rtabmap::CameraModel & model1, const rtabmap:: //# IMU Parameters TODO: hard-coded, not used //#-------------------------------------------------------------------------------------------- // Transformation from camera 0 to body-frame (imu) - rtabmap::Transform camImuT = model1.localTransform()*imuLocalTransform_; + rtabmap::Transform camImuT = imuLocalTransform_.inverse()*model1.localTransform(); ofs << "IMU.T_b_c1: !!opencv-matrix" << std::endl; ofs << " rows: 4" << std::endl; ofs << " cols: 4" << std::endl; @@ -340,14 +348,16 @@ bool OdometryORBSLAM3::init(const rtabmap::CameraModel & model1, const rtabmap:: ofs.close(); + ORB_SLAM3::System::eSensor sensor = + stereo?(withIMU?ORB_SLAM3::System::IMU_STEREO:ORB_SLAM3::System::STEREO): + (withIMU?ORB_SLAM3::System::IMU_RGBD:ORB_SLAM3::System::RGBD); + UINFO("Initializing ORB_SLAM3 system with sensor %d...", (int)sensor); orbslam_ = new ORB_SLAM3::System( vocabularyPath, configPath, - stereo && withIMU?ORB_SLAM3::System::IMU_STEREO: - stereo?ORB_SLAM3::System::STEREO: - withIMU?ORB_SLAM3::System::IMU_RGBD: - ORB_SLAM3::System::RGBD, + sensor, false); + UINFO("Initializing ORB_SLAM3 system with sensor %d... done!", (int)sensor); return true; #else UERROR("RTAB-Map is not built with ORB_SLAM support! Select another visual odometry approach."); @@ -373,6 +383,7 @@ Transform OdometryORBSLAM3::computeTransform( { if(lastImuStamp_ == 0.0 || lastImuStamp_ < data.stamp()) { + UDEBUG("Adding IMU %f", data.stamp()); orbslamImus_.push_back(ORB_SLAM3::IMU::Point( data.imu().linearAcceleration().val[0], data.imu().linearAcceleration().val[1], @@ -432,6 +443,7 @@ Transform OdometryORBSLAM3::computeTransform( if(lastImageStamp_ == 0.0) { lastImageStamp_ = data.stamp(); + UDEBUG("Waiting for another image to initialize..."); return t; } @@ -457,6 +469,7 @@ Transform OdometryORBSLAM3::computeTransform( rightMono = cv::Mat(); cv::cvtColor(data.imageRaw(), rightMono, CV_BGR2GRAY); } + UDEBUG("Adding Stereo Frame %f", data.stamp()); Tcw = orbslam_->TrackStereo(leftMono, rightMono, data.stamp(), orbslamImus_); orbslamImus_.clear(); } @@ -472,15 +485,22 @@ Transform OdometryORBSLAM3::computeTransform( { depth = util2d::cvtDepthToFloat(data.depthRaw()); } + UDEBUG("Adding RGBD Frame %f", data.stamp()); Tcw = orbslam_->TrackRGBD(data.imageRaw(), depth, data.stamp(), orbslamImus_); orbslamImus_.clear(); } Transform previousPoseInv = previousPose_.inverse(); - std::vector mapPoints = orbslam_->GetTrackedMapPoints(); - if(orbslam_->isLost() || mapPoints.empty()) + std::vector trackedMapPoints = orbslam_->GetTrackedMapPoints(); + if(orbslam_->isLost() || trackedMapPoints.empty()) { covariance = cv::Mat::eye(6,6,CV_64FC1)*9999.0f; + if(!imuLocalTransform_.isNull()) { + UWARN("ORBSLAM lost tracking! If it is on initialization, try moving the sensor in a circle for a couple of seconds."); + } + else { + UWARN("ORBSLAM lost tracking!"); + } } else { @@ -490,14 +510,16 @@ Transform OdometryORBSLAM3::computeTransform( if(!p.isNull()) { - if(!localTransform.isNull()) + if(!imuLocalTransform_.isNull()) { - if(originLocalTransform_.isNull()) - { - originLocalTransform_ = localTransform; - } - // transform in base frame - p = originLocalTransform_ * p.inverse() * localTransform.inverse(); + // Transform p from optical-imu system (x->left, y->back and z->up) to ros system, then remove camera local transform + p = Transform(0,0,0,0,0,-M_PI/2) * p.inverse() * localTransform.inverse(); + } + else + { + UASSERT(!localTransform.isNull()); + // Transform p from optical system (x->right, y->down and z->forward) to ros system, then remove camera local transform + p = CameraModel::opticalRotation() * p.inverse() * localTransform.inverse(); } t = previousPoseInv*p; } @@ -534,12 +556,14 @@ Transform OdometryORBSLAM3::computeTransform( } } + size_t mapPointsSize = 0; if(info) { info->lost = t.isNull(); info->type = (int)kTypeORBSLAM; info->reg.covariance = covariance; - info->localMapSize = mapPoints.size(); + std::vector mapPoints = orbslam_->GetAllMapPoints(); + info->localMapSize = mapPointsSize = mapPoints.size(); info->localKeyFrames = 0; if(this->isInfoDataFilled()) @@ -549,20 +573,20 @@ Transform OdometryORBSLAM3::computeTransform( info->reg.inliersIDs.resize(kpts.size()); int oi = 0; - UASSERT(mapPoints.size() == kpts.size()); + UASSERT(trackedMapPoints.size() == kpts.size()); for (unsigned int i = 0; i < kpts.size(); ++i) { int wordId; - if(mapPoints[i] != 0) + if(trackedMapPoints[i] != 0) { - wordId = mapPoints[i]->mnId; + wordId = trackedMapPoints[i]->mnId; } else { wordId = -(i+1); } info->words.insert(std::make_pair(wordId, kpts[i])); - if(mapPoints[i] != 0) + if(trackedMapPoints[i] != 0) { info->reg.matchesIDs[oi] = wordId; info->reg.inliersIDs[oi] = wordId; @@ -574,7 +598,15 @@ Transform OdometryORBSLAM3::computeTransform( info->reg.inliers = oi; info->reg.matches = oi; - Eigen::Affine3f fixRot = (this->getPose()*previousPoseInv*originLocalTransform_).toEigen3f(); + Eigen::Affine3f fixRot; + if(!imuLocalTransform_.isNull()) + { + fixRot = (this->getPose()*previousPoseInv*Transform(0,0,0,0,0,-M_PI/2)).toEigen3f(); + } + else + { + fixRot = (this->getPose()*previousPoseInv*CameraModel::opticalRotation()).toEigen3f(); + } for (unsigned int i = 0; i < mapPoints.size(); ++i) { if(mapPoints[i]) @@ -587,7 +619,8 @@ Transform OdometryORBSLAM3::computeTransform( } } - UINFO("Odom update time = %fs, map points=%ld, lost=%s", timer.elapsed(), mapPoints.size(), t.isNull()?"true":"false"); + UINFO("Odom update time = %fs, tracked points=%ld, map points=%ld, lost=%s", + timer.elapsed(), trackedMapPoints.size(), mapPointsSize, t.isNull()?"true":"false"); #else UERROR("RTAB-Map is not built with ORB_SLAM support! Select another visual odometry approach."); diff --git a/guilib/src/PreferencesDialog.cpp b/guilib/src/PreferencesDialog.cpp index 933b8a17..fabfd59a 100644 --- a/guilib/src/PreferencesDialog.cpp +++ b/guilib/src/PreferencesDialog.cpp @@ -6761,7 +6761,8 @@ Camera * PreferencesDialog::createCamera( camera->setInterIMUPublishing( _ui->checkbox_publishInterIMU->isChecked(), _ui->checkbox_publishInterIMU->isChecked() && getIMUFilteringStrategy()>0? - IMUFilter::create((IMUFilter::Type)(getIMUFilteringStrategy()-1), this->getAllParameters()):0); + IMUFilter::create((IMUFilter::Type)(getIMUFilteringStrategy()-1), this->getAllParameters()):0, + getIMUFilteringBaseFrameConversion()); } else if (driver == kSrcRealSense) { @@ -6803,7 +6804,8 @@ Camera * PreferencesDialog::createCamera( camera->setInterIMUPublishing( _ui->checkbox_publishInterIMU->isChecked(), _ui->checkbox_publishInterIMU->isChecked() && getIMUFilteringStrategy()>0? - IMUFilter::create((IMUFilter::Type)(getIMUFilteringStrategy()-1), this->getAllParameters()):0); + IMUFilter::create((IMUFilter::Type)(getIMUFilteringStrategy()-1), this->getAllParameters()):0, + getIMUFilteringBaseFrameConversion()); if(driver == kSrcStereoRealSense2) { ((CameraRealSense2*)camera)->setImagesRectified((_ui->checkBox_stereo_rectify->isEnabled() && _ui->checkBox_stereo_rectify->isChecked()) && !useRawImages); @@ -7021,7 +7023,8 @@ Camera * PreferencesDialog::createCamera( camera->setInterIMUPublishing( _ui->checkbox_publishInterIMU->isChecked(), _ui->checkbox_publishInterIMU->isChecked() && getIMUFilteringStrategy()>0? - IMUFilter::create((IMUFilter::Type)(getIMUFilteringStrategy()-1), this->getAllParameters()):0); + IMUFilter::create((IMUFilter::Type)(getIMUFilteringStrategy()-1), this->getAllParameters()):0, + getIMUFilteringBaseFrameConversion()); ((CameraStereoZed*)camera)->setRightGrayScale(_ui->checkBox_stereo_rightGrayScale->isChecked()); } else if (driver == kSrcStereoZedOC) @@ -7070,7 +7073,8 @@ Camera * PreferencesDialog::createCamera( camera->setInterIMUPublishing( _ui->checkbox_publishInterIMU->isChecked(), _ui->checkbox_publishInterIMU->isChecked() && getIMUFilteringStrategy()>0? - IMUFilter::create((IMUFilter::Type)(getIMUFilteringStrategy()-1), this->getAllParameters()):0); + IMUFilter::create((IMUFilter::Type)(getIMUFilteringStrategy()-1), this->getAllParameters()):0, + getIMUFilteringBaseFrameConversion()); } else if(driver == kSrcUsbDevice) { diff --git a/guilib/src/ui/preferencesDialog.ui b/guilib/src/ui/preferencesDialog.ui index d71bf025..b335ff59 100644 --- a/guilib/src/ui/preferencesDialog.ui +++ b/guilib/src/ui/preferencesDialog.ui @@ -6,7 +6,7 @@ 0 0 - 1009 + 980 925 @@ -63,9 +63,9 @@ 0 - -949 - 713 - 4922 + -1237 + 684 + 5110 @@ -95,7 +95,7 @@ QFrame::Raised - 12 + 19 @@ -8308,7 +8308,7 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - Publish inter IMU messages from the camera. IMU received between images will be published as separate topic. Orientation overridden (if any) if IMU filtering is used. + Publish inter IMU messages from the camera. IMU received between images will be published as separate topic. Orientation overridden (if any) if IMU filtering is used. true @@ -16349,7 +16349,7 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - 0 + 5 @@ -18580,23 +18580,39 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - Enable IMU. Only supported with ORB_SLAM3. + Enable IMU. true + + + + Only supported with ORB_SLAM3. Inter IMU publishing should be enabled (see Source panel under IMU Filtering section). + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + - 8 + 6 - 0.010000000000000 + 0.000001000000000 + 1.000000000000000 + + 0.010000000000000 @@ -18643,12 +18659,15 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - 8 + 6 - 0.100000000000000 + 0.000001000000000 + 1.000000000000000 + + 0.100000000000000 @@ -18682,10 +18701,10 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - 8 + 6 - 0.000000000000000 + 0.000001000000000 1.000000000000000 @@ -18701,10 +18720,10 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - 8 + 6 - 0.000000000000000 + 0.000001000000000 1.000000000000000 From d1b5d62d210d02de47b716a927895b3040367d39 Mon Sep 17 00:00:00 2001 From: Felix Toft Date: Sun, 16 Nov 2025 11:37:03 -0800 Subject: [PATCH 054/126] Limit Max Features (#1614) * limit max features internally inside pydetector and superpoint_rpautrat to limit keypoint/desc data size and improve performance * avoid full reset when max features is changed to work around the memory temprorary param update logic * remove leftover comment * try using image roi instead * revert and regenerate * unintended change * fixing issue with pydetector, adding mask filtering --------- Co-authored-by: Felix Toft --- corelib/src/Features2d.cpp | 12 +- corelib/src/python/PyDetector.cpp | 472 +++++++++--------- .../SuperpointRpautrat.cpp | 40 +- .../superpoint_rpautrat/SuperpointRpautrat.h | 13 +- .../superpoint_to_torchscript.py | 2 +- 5 files changed, 284 insertions(+), 255 deletions(-) diff --git a/corelib/src/Features2d.cpp b/corelib/src/Features2d.cpp index b18efb23..329e5a53 100644 --- a/corelib/src/Features2d.cpp +++ b/corelib/src/Features2d.cpp @@ -2661,14 +2661,14 @@ void SuperPointRpautrat::parseParameters(const ParametersMap & parameters) Parameters::parse(parameters, Parameters::kSuperPointRpautratNMSRadius(), minDistance_); Parameters::parse(parameters, Parameters::kSuperPointRpautratCuda(), cuda_); Parameters::parse(parameters, Parameters::kRtabmapWorkingDirectory(), outputDir_); - + // If working directory is not set, use the default if(outputDir_.empty()) { outputDir_ = Parameters::createDefaultWorkingDirectory(); } - // Delete the detector to force re-initialization on next frame if any parameter changed + // Reinitialize detector if model-affecting parameters changed if(superPoint_.get() == 0 || superpointWeightsPath_.compare(previousWeightsPath) != 0 || superpointModelPath_.compare(previousModelPath) != 0 || @@ -2677,7 +2677,13 @@ void SuperPointRpautrat::parseParameters(const ParametersMap & parameters) previousNms != nms_ || previousMinDistance != minDistance_) { - superPoint_ = cv::Ptr(new SPDetectorRpautrat(superpointWeightsPath_, superpointModelPath_, outputDir_, threshold_, nms_, minDistance_, cuda_)); + superPoint_ = cv::Ptr(new SPDetectorRpautrat(superpointWeightsPath_, superpointModelPath_, outputDir_, threshold_, nms_, minDistance_, cuda_, this->getMaxFeatures(), this->getSSC())); + } + else if(superPoint_.get() != 0) + { + // Update post-processing parameters without reinitializing + superPoint_->setMaxFeatures(this->getMaxFeatures()); + superPoint_->setSSC(this->getSSC()); } #else UWARN("RTAB-Map is not built with Torch support so SuperPoint Rpautrat feature cannot be used!"); diff --git a/corelib/src/python/PyDetector.cpp b/corelib/src/python/PyDetector.cpp index 46f9f256..8d3101a5 100644 --- a/corelib/src/python/PyDetector.cpp +++ b/corelib/src/python/PyDetector.cpp @@ -1,230 +1,242 @@ -/** - * Python interface for SuperGlue: https://github.com/magicleap/SuperGluePretrainedNetwork - */ - -#include "PyDetector.h" -#include -#include -#include -#include -#include -#include - -#include - -#define NPY_NO_DEPRECATED_API NPY_API_VERSION -#include - -namespace rtabmap -{ - -PyDetector::PyDetector(const ParametersMap & parameters) : - pModule_(0), - pFunc_(0), - path_(Parameters::defaultPyDetectorPath()), - cuda_(Parameters::defaultPyDetectorCuda()) -{ - this->parseParameters(parameters); - - UDEBUG("path = %s", path_.c_str()); - if(!UFile::exists(path_) || UFile::getExtension(path_).compare("py") != 0) - { - UERROR("Cannot initialize Python detector, the path is not valid: \"%s\"=\"%s\"", - Parameters::kPyDetectorPath().c_str(), path_.c_str()); - return; - } - - pybind11::gil_scoped_acquire acquire; - - std::string matcherPythonDir = UDirectory::getDir(path_); - if(!matcherPythonDir.empty()) - { - PyRun_SimpleString("import sys"); - PyRun_SimpleString(uFormat("sys.path.append(\"%s\")", matcherPythonDir.c_str()).c_str()); - } - - _import_array(); - - std::string scriptName = uSplit(UFile::getName(path_), '.').front(); - PyObject * pName = PyUnicode_FromString(scriptName.c_str()); - UDEBUG("PyImport_Import() beg"); - pModule_ = PyImport_Import(pName); - UDEBUG("PyImport_Import() end"); - - Py_DECREF(pName); - - if(!pModule_) - { - UERROR("Module \"%s\" could not be imported! (File=\"%s\")", scriptName.c_str(), path_.c_str()); - UERROR("%s", getPythonTraceback().c_str()); - } -} - -PyDetector::~PyDetector() -{ - pybind11::gil_scoped_acquire acquire; - - if(pFunc_) - { - Py_DECREF(pFunc_); - } - if(pModule_) - { - Py_DECREF(pModule_); - } -} - -void PyDetector::parseParameters(const ParametersMap & parameters) -{ - Feature2D::parseParameters(parameters); - - Parameters::parse(parameters, Parameters::kPyDetectorPath(), path_); - Parameters::parse(parameters, Parameters::kPyDetectorCuda(), cuda_); - - path_ = uReplaceChar(path_, '~', UDirectory::homeDir()); -} - -std::vector PyDetector::generateKeypointsImpl(const cv::Mat & image, const cv::Rect & roi, const cv::Mat & mask) -{ - UDEBUG(""); - descriptors_ = cv::Mat(); - UASSERT(!image.empty() && image.channels() == 1 && image.depth() == CV_8U); - std::vector keypoints; - cv::Mat imgRoi(image, roi); - - UTimer timer; - - if(!pModule_) - { - UERROR("Python detector module not loaded!"); - return keypoints; - } - - pybind11::gil_scoped_acquire acquire; - - if(!pFunc_) - { - PyObject * pFunc = PyObject_GetAttrString(pModule_, "init"); - if(pFunc) - { - if(PyCallable_Check(pFunc)) - { - PyObject * result = PyObject_CallFunction(pFunc, "i", cuda_?1:0); - - if(result == NULL) - { - UERROR("Call to \"init(...)\" in \"%s\" failed!", path_.c_str()); - UERROR("%s", getPythonTraceback().c_str()); - return keypoints; - } - Py_DECREF(result); - - pFunc_ = PyObject_GetAttrString(pModule_, "detect"); - if(pFunc_ && PyCallable_Check(pFunc_)) - { - // we are ready! - } - else - { - UERROR("Cannot find method \"detect(...)\" in %s", path_.c_str()); - UERROR("%s", getPythonTraceback().c_str()); - if(pFunc_) - { - Py_DECREF(pFunc_); - pFunc_ = 0; - } - return keypoints; - } - } - else - { - UERROR("Cannot call method \"init(...)\" in %s", path_.c_str()); - UERROR("%s", getPythonTraceback().c_str()); - return keypoints; - } - Py_DECREF(pFunc); - } - else - { - UERROR("Cannot find method \"init(...)\""); - UERROR("%s", getPythonTraceback().c_str()); - return keypoints; - } - UDEBUG("init time = %fs", timer.ticks()); - } - - if(pFunc_) - { - npy_intp dims[2] = {imgRoi.rows, imgRoi.cols}; - PyObject* pImageBuffer = PyArray_SimpleNewFromData(2, dims, NPY_UBYTE, (void*)imgRoi.data); - UASSERT(pImageBuffer); - - UDEBUG("Preparing data time = %fs", timer.ticks()); - - PyObject *pReturn = PyObject_CallFunctionObjArgs(pFunc_, pImageBuffer, NULL); - if(pReturn == NULL) - { - UERROR("Failed to call match() function!"); - UERROR("%s", getPythonTraceback().c_str()); - } - else - { - UDEBUG("Python detector time = %fs", timer.ticks()); - - if (PyTuple_Check(pReturn) && PyTuple_GET_SIZE(pReturn) == 2) - { - PyObject *kptsPtr = PyTuple_GET_ITEM(pReturn, 0); - PyObject *descPtr = PyTuple_GET_ITEM(pReturn, 1); - if(PyArray_Check(kptsPtr) && PyArray_Check(descPtr)) - { - PyArrayObject *arrayPtr = reinterpret_cast(kptsPtr); - int nKpts = PyArray_SHAPE(arrayPtr)[0]; - int kptSize = PyArray_SHAPE(arrayPtr)[1]; - int type = PyArray_TYPE(arrayPtr); - UDEBUG("Kpts array %dx%d (type=%d)", nKpts, kptSize, type); - UASSERT(kptSize == 3); - UASSERT_MSG(type == NPY_FLOAT, uFormat("Returned matches should type FLOAT=11, received type=%d", type).c_str()); - - float* c_out = reinterpret_cast(PyArray_DATA(arrayPtr)); - keypoints.reserve(nKpts); - for (int i = 0; i < nKpts*kptSize; i+=kptSize) - { - cv::KeyPoint kpt(c_out[i], c_out[i+1], 8, -1, c_out[i+2]); - keypoints.push_back(kpt); - } - - arrayPtr = reinterpret_cast(descPtr); - int nDesc = PyArray_SHAPE(arrayPtr)[0]; - UASSERT(nDesc = nKpts); - int dim = PyArray_SHAPE(arrayPtr)[1]; - type = PyArray_TYPE(arrayPtr); - UDEBUG("Desc array %dx%d (type=%d)", nDesc, dim, type); - UASSERT_MSG(type == NPY_FLOAT, uFormat("Returned matches should type FLOAT=11, received type=%d", type).c_str()); - - c_out = reinterpret_cast(PyArray_DATA(arrayPtr)); - for (int i = 0; i < nDesc*dim; i+=dim) - { - cv::Mat descriptor = cv::Mat(1, dim, CV_32FC1, &c_out[i]).clone(); - descriptors_.push_back(descriptor); - } - } - } - else - { - UWARN("Expected tuple (Kpts 3 x N, Descriptors dim x N), returning empty features."); - } - Py_DECREF(pReturn); - } - Py_DECREF(pImageBuffer); - } - - return keypoints; -} - -cv::Mat PyDetector::generateDescriptorsImpl(const cv::Mat & image, std::vector & keypoints) const -{ - UASSERT((int)keypoints.size() == descriptors_.rows); - return descriptors_; -} - -} +/** + * Python interface for SuperGlue: https://github.com/magicleap/SuperGluePretrainedNetwork + */ + +#include "PyDetector.h" +#include +#include +#include +#include +#include +#include + +#include + +#define NPY_NO_DEPRECATED_API NPY_API_VERSION +#include + +namespace rtabmap +{ + +PyDetector::PyDetector(const ParametersMap & parameters) : + pModule_(0), + pFunc_(0), + path_(Parameters::defaultPyDetectorPath()), + cuda_(Parameters::defaultPyDetectorCuda()) +{ + this->parseParameters(parameters); + + UDEBUG("path = %s", path_.c_str()); + if(!UFile::exists(path_) || UFile::getExtension(path_).compare("py") != 0) + { + UERROR("Cannot initialize Python detector, the path is not valid: \"%s\"=\"%s\"", + Parameters::kPyDetectorPath().c_str(), path_.c_str()); + return; + } + + pybind11::gil_scoped_acquire acquire; + + std::string matcherPythonDir = UDirectory::getDir(path_); + if(!matcherPythonDir.empty()) + { + PyRun_SimpleString("import sys"); + PyRun_SimpleString(uFormat("sys.path.append(\"%s\")", matcherPythonDir.c_str()).c_str()); + } + + _import_array(); + + std::string scriptName = uSplit(UFile::getName(path_), '.').front(); + PyObject * pName = PyUnicode_FromString(scriptName.c_str()); + UDEBUG("PyImport_Import() beg"); + pModule_ = PyImport_Import(pName); + UDEBUG("PyImport_Import() end"); + + Py_DECREF(pName); + + if(!pModule_) + { + UERROR("Module \"%s\" could not be imported! (File=\"%s\")", scriptName.c_str(), path_.c_str()); + UERROR("%s", getPythonTraceback().c_str()); + } +} + +PyDetector::~PyDetector() +{ + pybind11::gil_scoped_acquire acquire; + + if(pFunc_) + { + Py_DECREF(pFunc_); + } + if(pModule_) + { + Py_DECREF(pModule_); + } +} + +void PyDetector::parseParameters(const ParametersMap & parameters) +{ + Feature2D::parseParameters(parameters); + + Parameters::parse(parameters, Parameters::kPyDetectorPath(), path_); + Parameters::parse(parameters, Parameters::kPyDetectorCuda(), cuda_); + + path_ = uReplaceChar(path_, '~', UDirectory::homeDir()); +} + +std::vector PyDetector::generateKeypointsImpl(const cv::Mat & image, const cv::Rect & roi, const cv::Mat & mask) +{ + UDEBUG(""); + descriptors_ = cv::Mat(); + UASSERT(!image.empty() && image.channels() == 1 && image.depth() == CV_8U); + std::vector keypoints; + cv::Mat imgRoi(image, roi); + + UTimer timer; + + if(!pModule_) + { + UERROR("Python detector module not loaded!"); + return keypoints; + } + + pybind11::gil_scoped_acquire acquire; + + if(!pFunc_) + { + PyObject * pFunc = PyObject_GetAttrString(pModule_, "init"); + if(pFunc) + { + if(PyCallable_Check(pFunc)) + { + PyObject * result = PyObject_CallFunction(pFunc, "i", cuda_?1:0); + + if(result == NULL) + { + UERROR("Call to \"init(...)\" in \"%s\" failed!", path_.c_str()); + UERROR("%s", getPythonTraceback().c_str()); + return keypoints; + } + Py_DECREF(result); + + pFunc_ = PyObject_GetAttrString(pModule_, "detect"); + if(pFunc_ && PyCallable_Check(pFunc_)) + { + // we are ready! + } + else + { + UERROR("Cannot find method \"detect(...)\" in %s", path_.c_str()); + UERROR("%s", getPythonTraceback().c_str()); + if(pFunc_) + { + Py_DECREF(pFunc_); + pFunc_ = 0; + } + return keypoints; + } + } + else + { + UERROR("Cannot call method \"init(...)\" in %s", path_.c_str()); + UERROR("%s", getPythonTraceback().c_str()); + return keypoints; + } + Py_DECREF(pFunc); + } + else + { + UERROR("Cannot find method \"init(...)\""); + UERROR("%s", getPythonTraceback().c_str()); + return keypoints; + } + UDEBUG("init time = %fs", timer.ticks()); + } + + if(pFunc_) + { + npy_intp dims[2] = {imgRoi.rows, imgRoi.cols}; + PyObject * pImageBuffer = PyArray_SimpleNewFromData(2, dims, NPY_UBYTE, (void*)imgRoi.data); + UASSERT(pImageBuffer); + + UDEBUG("Preparing data time = %fs", timer.ticks()); + + PyObject * pReturn = PyObject_CallFunctionObjArgs(pFunc_, pImageBuffer, NULL); + if(pReturn == NULL) + { + UERROR("Failed to call match() function!"); + UERROR("%s", getPythonTraceback().c_str()); + } + else + { + UDEBUG("Python detector time = %fs", timer.ticks()); + + if (PyTuple_Check(pReturn) && PyTuple_GET_SIZE(pReturn) == 2) + { + PyObject * kptsPtr = PyTuple_GET_ITEM(pReturn, 0); + PyObject * descPtr = PyTuple_GET_ITEM(pReturn, 1); + if(PyArray_Check(kptsPtr) && PyArray_Check(descPtr)) + { + PyArrayObject *arrayPtr = reinterpret_cast(kptsPtr); + int nKpts = PyArray_SHAPE(arrayPtr)[0]; + int kptSize = PyArray_SHAPE(arrayPtr)[1]; + int type = PyArray_TYPE(arrayPtr); + UDEBUG("Kpts array %dx%d (type=%d)", nKpts, kptSize, type); + UASSERT(kptSize == 3); + UASSERT_MSG(type == NPY_FLOAT, uFormat("Returned matches should type FLOAT=11, received type=%d", type).c_str()); + + float* c_out = reinterpret_cast(PyArray_DATA(arrayPtr)); + std::vector keep_kpt(nKpts); + keypoints.reserve(nKpts); + for (int i = 0, kpt_idx = 0; i < nKpts*kptSize; i+=kptSize, kpt_idx++) + { + // x,y in full image coordinates. Mask is in full image coordinates too. + int full_x = (int)(c_out[i] + roi.x); + int full_y = (int)(c_out[i+1] + roi.y); + keep_kpt[kpt_idx] = mask.empty() || (full_x >= 0 && full_x < mask.cols && full_y >= 0 && full_y < mask.rows && mask.at(full_y, full_x) != 0); + if(keep_kpt[kpt_idx]) { + cv::KeyPoint kpt(c_out[i], c_out[i+1], 8, -1, c_out[i+2]); + keypoints.push_back(kpt); + } + } + + arrayPtr = reinterpret_cast(descPtr); + int nDesc = PyArray_SHAPE(arrayPtr)[0]; + UASSERT(nDesc = nKpts); + int dim = PyArray_SHAPE(arrayPtr)[1]; + type = PyArray_TYPE(arrayPtr); + UDEBUG("Desc array %dx%d (type=%d)", nDesc, dim, type); + UASSERT_MSG(type == NPY_FLOAT, uFormat("Returned matches should type FLOAT=11, received type=%d", type).c_str()); + + c_out = reinterpret_cast(PyArray_DATA(arrayPtr)); + for (int i = 0, kpt_idx = 0; i < nDesc*dim; i+=dim, kpt_idx++) + { + if(keep_kpt[kpt_idx]) { + cv::Mat descriptor = cv::Mat(1, dim, CV_32FC1, &c_out[i]).clone(); + descriptors_.push_back(descriptor); + } + } + } + } + else + { + UWARN("Expected tuple (Kpts 3 x N, Descriptors dim x N), returning empty features."); + } + Py_DECREF(pReturn); + } + Py_DECREF(pImageBuffer); + } + + // Apply limitKeypoints to enforce maxFeatures and SSC + this->limitKeypoints(keypoints, descriptors_, this->getMaxFeatures(), cv::Size(roi.width, roi.height), this->getSSC()); + + return keypoints; +} + +cv::Mat PyDetector::generateDescriptorsImpl(const cv::Mat & image, std::vector & keypoints) const +{ + UASSERT((int)keypoints.size() == descriptors_.rows); + return descriptors_; +} + +} diff --git a/corelib/src/superpoint_rpautrat/SuperpointRpautrat.cpp b/corelib/src/superpoint_rpautrat/SuperpointRpautrat.cpp index b3391103..ffdbd760 100644 --- a/corelib/src/superpoint_rpautrat/SuperpointRpautrat.cpp +++ b/corelib/src/superpoint_rpautrat/SuperpointRpautrat.cpp @@ -4,6 +4,7 @@ */ #include "SuperpointRpautrat.h" +#include #include #include #include @@ -95,7 +96,7 @@ static std::string exportSuperPointTorchScript( return output; } -SPDetectorRpautrat::SPDetectorRpautrat(std::string superpointWeightsPath, std::string superpointModelPath, std::string outputDir, float threshold, bool nms, int minDistance, bool cuda) : +SPDetectorRpautrat::SPDetectorRpautrat(std::string superpointWeightsPath, std::string superpointModelPath, std::string outputDir, float threshold, bool nms, int minDistance, bool cuda, int maxFeatures, bool ssc) : device_(torch::kCPU), superpointWeightsPath_(superpointWeightsPath), superpointModelPath_(superpointModelPath), @@ -103,6 +104,8 @@ SPDetectorRpautrat::SPDetectorRpautrat(std::string superpointWeightsPath, std::s threshold_(threshold), nms_(nms), minDistance_(minDistance), + maxFeatures_(maxFeatures), + ssc_(ssc), detected_(false) { if(cuda && !torch::cuda::is_available()) @@ -136,12 +139,9 @@ cv::Mat SPDetectorRpautrat::compute(const std::vector &keypoints) } // These should have the same size - UASSERT(static_cast(desc_.size(0)) == keypoints.size()); + UASSERT(static_cast(desc_.rows) == keypoints.size()); - // Move to CPU and return descriptors computed in the forward pass - torch::Tensor desc_cpu = desc_.to(torch::kCPU); - cv::Mat desc_mat(cv::Size(desc_cpu.size(1), desc_cpu.size(0)), CV_32FC1, desc_cpu.data_ptr()); - return desc_mat.clone(); + return desc_; } std::vector SPDetectorRpautrat::detect(const cv::Mat &img, const cv::Mat & mask) @@ -189,12 +189,12 @@ std::vector SPDetectorRpautrat::detect(const cv::Mat &img, const c x = x.set_requires_grad(false).to(device_); auto outputs = model_.forward({x}).toTuple(); - keypoints_tensor_ = outputs->elements()[0].toTensor(); // [N, 2] keypoint coordinates - auto scores_tensor = outputs->elements()[1].toTensor(); // [N] keypoint scores - desc_ = outputs->elements()[2].toTensor(); // [N, 256] descriptors + auto kpts_tensor = outputs->elements()[0].toTensor(); // [N, 2] keypoint coordinates + auto scores_tensor = outputs->elements()[1].toTensor(); // [N] keypoint scores + torch::Tensor desc_tensor = outputs->elements()[2].toTensor(); // [N, 256] descriptors // Convert to CPU for processing - auto keypoints_cpu = keypoints_tensor_.to(torch::kCPU); + auto keypoints_cpu = kpts_tensor.to(torch::kCPU); auto scores_cpu = scores_tensor.to(torch::kCPU); std::vector filtered_keypoints; @@ -213,16 +213,20 @@ std::vector SPDetectorRpautrat::detect(const cv::Mat &img, const c } } - // Update the stored tensors to maintain correspondence - // This way if keypoints are re-ordered, we can still match kpts->descs in the compute step + // Filter descriptors based on mask auto keep_indices = torch::from_blob(keep_indices_vec.data(), {(long int)keep_indices_vec.size()}, torch::kLong); - keep_indices = keep_indices.to(keypoints_tensor_.device()); - auto filtered_keypoints_tensor = keypoints_tensor_.index_select(0, keep_indices); - auto filtered_descriptors = desc_.index_select(0, keep_indices); + keep_indices = keep_indices.to(desc_tensor.device()); + auto filtered_descriptors = desc_tensor.index_select(0, keep_indices); - keypoints_tensor_ = filtered_keypoints_tensor; - desc_ = filtered_descriptors; - + // Convert descriptors to cv::Mat + auto filtered_descriptors_cpu = filtered_descriptors.to(torch::kCPU); + cv::Mat descriptors_mat(filtered_descriptors_cpu.size(0), filtered_descriptors_cpu.size(1), CV_32FC1, filtered_descriptors_cpu.data_ptr()); + cv::Mat descriptors_clone = descriptors_mat.clone(); // Clone to own the memory + + // Apply limitKeypoints to enforce maxFeatures and SSC + Feature2D::limitKeypoints(filtered_keypoints, descriptors_clone, maxFeatures_, cv::Size(img.cols, img.rows), ssc_); + + desc_ = descriptors_clone; detected_ = true; return filtered_keypoints; } diff --git a/corelib/src/superpoint_rpautrat/SuperpointRpautrat.h b/corelib/src/superpoint_rpautrat/SuperpointRpautrat.h index 9adb982f..0eefaa57 100644 --- a/corelib/src/superpoint_rpautrat/SuperpointRpautrat.h +++ b/corelib/src/superpoint_rpautrat/SuperpointRpautrat.h @@ -23,17 +23,22 @@ class SPDetectorRpautrat { float threshold = 0.005f, bool nms = true, int nmsRadius = 4, - bool cuda = false + bool cuda = false, + int maxFeatures = 1000, + bool ssc = false ); virtual ~SPDetectorRpautrat(); std::vector detect(const cv::Mat &img, const cv::Mat & mask = cv::Mat()); cv::Mat compute(const std::vector &keypoints); + + // Setters for post-processing parameters that don't require model reinitialization + void setMaxFeatures(int maxFeatures) { maxFeatures_ = maxFeatures; } + void setSSC(bool ssc) { ssc_ = ssc; } private: torch::jit::script::Module model_; torch::Device device_; - torch::Tensor desc_; - torch::Tensor keypoints_tensor_; + cv::Mat desc_; std::string superpointWeightsPath_; std::string superpointModelPath_; @@ -42,6 +47,8 @@ class SPDetectorRpautrat { bool nms_; int minDistance_; bool cuda_; + int maxFeatures_; + bool ssc_; bool detected_; }; diff --git a/corelib/src/superpoint_rpautrat/superpoint_to_torchscript.py b/corelib/src/superpoint_rpautrat/superpoint_to_torchscript.py index da342c7e..804ddc85 100644 --- a/corelib/src/superpoint_rpautrat/superpoint_to_torchscript.py +++ b/corelib/src/superpoint_rpautrat/superpoint_to_torchscript.py @@ -59,7 +59,7 @@ def generate_model( # Load SuperPoint model and weights model = SuperPoint( nms_radius=nms_radius, - threshold=threshold, + detection_threshold=threshold, ).eval().to(device) # Load weights without forcing CPU location to allow CUDA usage From 82fb7ff5d2bd95a16f0c876fafb783c1bcc20cd7 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 16 Nov 2025 11:37:47 -0800 Subject: [PATCH 055/126] DbViewer: coloring graph based on env sensor value (e.g., wifi signal strength) (#1613) --- guilib/include/rtabmap/gui/DatabaseViewer.h | 1 + guilib/include/rtabmap/gui/GraphViewer.h | 1 + guilib/src/DatabaseViewer.cpp | 45 +- guilib/src/GraphViewer.cpp | 60 ++ guilib/src/ui/DatabaseViewer.ui | 793 +++++++++++--------- 5 files changed, 542 insertions(+), 358 deletions(-) diff --git a/guilib/include/rtabmap/gui/DatabaseViewer.h b/guilib/include/rtabmap/gui/DatabaseViewer.h index a11b4c93..07f94943 100644 --- a/guilib/include/rtabmap/gui/DatabaseViewer.h +++ b/guilib/include/rtabmap/gui/DatabaseViewer.h @@ -220,6 +220,7 @@ private: std::map mapIds_; std::map weights_; std::map > wmStates_; + std::map envSensors_; QMap idToIndex_; QList neighborLinks_; QList loopLinks_; diff --git a/guilib/include/rtabmap/gui/GraphViewer.h b/guilib/include/rtabmap/gui/GraphViewer.h index 8df08885..d80023e5 100644 --- a/guilib/include/rtabmap/gui/GraphViewer.h +++ b/guilib/include/rtabmap/gui/GraphViewer.h @@ -77,6 +77,7 @@ public: // Use updateNodeColorByValue() instead with valueName="Posterior". RTABMAP_DEPRECATED void updatePosterior(const std::map & posterior, float fixedMax = 0.0f, int zValueOffset = 0); void updateNodeColorByValue(const std::string & valueName, const std::map & values, float fixedMax = 0.0f, bool invertedColorScale = false, int zValueOffset = 0); + void updateNodeColorByValue(const std::string & valueName, const std::map & values, float fixedMin, float fixedMax, bool invertedColorScale = false, unsigned short hueMin=0, unsigned short hueMax=180, int zValueOffset = 0); void updateLocalPath(const std::vector & localPath); void setGlobalPath(const std::vector > & globalPath); void setCurrentGoalID(int id, const Transform & pose = Transform()); diff --git a/guilib/src/DatabaseViewer.cpp b/guilib/src/DatabaseViewer.cpp index 33fa3452..d2e2e8a7 100644 --- a/guilib/src/DatabaseViewer.cpp +++ b/guilib/src/DatabaseViewer.cpp @@ -454,6 +454,7 @@ DatabaseViewer::DatabaseViewer(const QString & ini, QWidget * parent) : connect(ui_->checkBox_alignScansCloudsWithGroundTruth, SIGNAL(stateChanged(int)), this, SLOT(updateGraphView())); connect(ui_->checkBox_ignoreIntermediateNodes, SIGNAL(stateChanged(int)), this, SLOT(configModified())); connect(ui_->checkBox_ignoreIntermediateNodes, SIGNAL(stateChanged(int)), this, SLOT(updateGraphView())); + connect(ui_->comboBox_env_sensor_graph_colormap, SIGNAL(currentIndexChanged(int)), this, SLOT(updateGraphView())); connect(ui_->checkBox_timeStats, SIGNAL(stateChanged(int)), this, SLOT(configModified())); connect(ui_->checkBox_timeStats, SIGNAL(stateChanged(int)), this, SLOT(updateStatistics())); // Graph view @@ -1129,6 +1130,7 @@ bool DatabaseViewer::closeDatabase() lastWmIds_.clear(); mapIds_.clear(); weights_.clear(); + envSensors_.clear(); wmStates_.clear(); links_.clear(); linksAdded_.clear(); @@ -1806,6 +1808,7 @@ void DatabaseViewer::updateIds() idToIndex_.clear(); mapIds_.clear(); weights_.clear(); + envSensors_.clear(); wmStates_.clear(); odomPoses_.clear(); groundTruthPoses_.clear(); @@ -1912,6 +1915,7 @@ void DatabaseViewer::updateIds() dbDriver_->getNodeInfo(ids_[i], p, mapId, w, l, s, g, v, gps, sensors); mapIds_.insert(std::make_pair(ids_[i], mapId)); weights_.insert(std::make_pair(ids_[i], w)); + envSensors_.insert(std::make_pair(ids_[i], sensors)); if(w>=0) { for(std::multimap::iterator iter=links.find(ids_[i]); iter!=links.end() && iter->first==ids_[i]; ++iter) @@ -7242,7 +7246,46 @@ void DatabaseViewer::sliderIterationsValueChanged(int value) ui_->graphViewer->updateGTGraph(groundTruthPoses_); ui_->graphViewer->updateGPSGraph(gpsPoses_, gpsValues_); ui_->graphViewer->updateGraph(graph, graphLinks_, mapIds_, weights_); - if(ui_->checkBox_wmState->isEnabled() && + if(ui_->comboBox_env_sensor_graph_colormap->currentIndex() != 0) + { + std::map colors; + EnvSensor::Type curentType = (EnvSensor::Type)ui_->comboBox_env_sensor_graph_colormap->currentIndex(); + for(std::map::iterator iter=graph.begin(); iter!=graph.end(); ++iter) + { + auto jter = envSensors_.find(iter->first); + if(jter != envSensors_.end() && jter->second.find(curentType) != jter->second.end()) + { + colors.insert(std::make_pair(iter->first, jter->second.at(curentType).value())); + } + } + std::string legend; + bool invertedColor = false; + unsigned char hueMax = 240; // blue + switch(curentType) + { + case EnvSensor::kWifiSignalStrength: + legend = "Wifi Signal Strength (dBm)"; + invertedColor = true; + hueMax = 120; // green + break; + case EnvSensor::kAmbientTemperature: + legend = "Ambient Temperature (Celcius)"; + break; + case EnvSensor::kAmbientAirPressure: + legend = "Ambient Air Pressure (hPa)"; + break; + case EnvSensor::kAmbientLight: + legend = "Ambient Light / Illuminance (lx)"; + break; + case EnvSensor::kAmbientRelativeHumidity: + legend = "Ambient Relative Humidity (%)"; + break; + default: + break; + } + ui_->graphViewer->updateNodeColorByValue(legend, colors, 0.0f, 0.0f, invertedColor, 0, hueMax, 1); + } + else if(ui_->checkBox_wmState->isEnabled() && ui_->checkBox_wmState->isChecked() && !lastWmIds_.empty()) { diff --git a/guilib/src/GraphViewer.cpp b/guilib/src/GraphViewer.cpp index 8f1f64b9..10c76f99 100644 --- a/guilib/src/GraphViewer.cpp +++ b/guilib/src/GraphViewer.cpp @@ -1134,6 +1134,66 @@ void GraphViewer::updateNodeColorByValue(const std::string & valueName, const st } } +void GraphViewer::updateNodeColorByValue( + const std::string & valueName, + const std::map & values, + float min, + float max, + bool invertedColorScale, + unsigned short hueMin, + unsigned short hueMax, + int zValueOffset) +{ + hueMax = hueMax > 360 ? 360 : hueMax; + + //find min/max + if(min >= max) + { + bool firstValueSet = false; + for(std::map::const_iterator iter = values.begin(); iter!=values.end(); ++iter) + { + if(iter->first > 0) + { + if(!firstValueSet) { + min = max = iter->second; + firstValueSet = true; + } + else if(iter->second>max) + { + max = iter->second; + } + else if(iter->secondsecond; + } + } + } + } + if(min < max && hueMin < hueMax) + { + float range = max - min; + float hueRange = float(hueMax - hueMin); + for(QMap::iterator iter = _nodeItems.begin(); iter!=_nodeItems.end(); ++iter) + { + std::map::const_iterator jter = values.find(iter.key()); + if(jter != values.end()) + { + float v = jter->second; + v = std::min(v, max); + v = std::max(v, min); + iter.value()->setColor(QColor::fromHsvF(( invertedColorScale ? (v-min)/range : 1-(v-min)/range )*hueRange/360.0f + hueMin/360.0f, 1, 1, 1), valueName.c_str(), jter->second); + iter.value()->setZValue(iter.value()->zValue()+zValueOffset); + } + } + } + else if(min < max) { + UWARN("min (%f) is not less than max (%f), cannot change color of the graph.", min, max); + } + else if(hueMin < hueMax) { + UWARN("Hue min (%d) is not less than hue max (%d), cannot change color of the graph. The hue values should be set between 0 (red) and 360(pink).", (int)hueMin, (int)hueMax); + } +} + void GraphViewer::setGlobalPath(const std::vector > & globalPath) { UDEBUG("Set global path size=%d", (int)globalPath.size()); diff --git a/guilib/src/ui/DatabaseViewer.ui b/guilib/src/ui/DatabaseViewer.ui index 48717272..798c9f75 100644 --- a/guilib/src/ui/DatabaseViewer.ui +++ b/guilib/src/ui/DatabaseViewer.ui @@ -17,7 +17,7 @@ Qt::LeftToRight - + 0 @@ -44,7 +44,7 @@ - + 0 @@ -776,71 +776,90 @@ - - - - 12 - - - 12 - - - 12 - - - 12 - - - - - - - Index : - + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 12 + + + 12 + + + 12 + + + 12 + + + + + + + Index : + + + + + + + Id : + + + + + + + + + + + false + + + + + + + idB + + + + + + + + + Qt::ClickFocus + + + Qt::Horizontal + + + QSlider::TicksAbove + - - - - - Id : - - - + - - - - - - - false - - - - - - - idB - - - - - - - - - Qt::ClickFocus - - - Qt::Horizontal - - - QSlider::TicksAbove - - - + - + @@ -1252,322 +1271,382 @@ - - + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + - + - - - 0.0 deg - - - - - - - Qt::ClickFocus - - - -1799 - - - 1800 - - - 0 - - - Qt::Horizontal - - - QSlider::TicksAbove - - - 100 - - - - - - - <html><head/><body><p>The rotation will be applied temporary to optimized global graph. To save it to database, do File-&gt;&quot;Regenerate optimized 2D map...&quot;.</p></body></html> - - - Apply Rotation - - - - - - - - - - - # - - - - - - - Qt::ClickFocus - - - Qt::Horizontal - - - QSlider::TicksAbove - - - - - - - QComboBox::AdjustToContents - + + - Global Iterative + 0.0 deg + - - Global Full + + + Qt::ClickFocus + + -1799 + + + 1800 + + + 0 + + + Qt::Horizontal + + + QSlider::TicksAbove + + + 100 + + - - Local Optimized + + + <html><head/><body><p>The rotation will be applied temporary to optimized global graph. To save it to database, do File-&gt;&quot;Regenerate optimized 2D map...&quot;.</p></body></html> + + Apply Rotation + + - + - - - - - - - - Align poses with ground truth - - - true - - - - - - - - - - false - - - - - + + - + - Root + # - + - + + + Qt::ClickFocus + + + Qt::Horizontal + + + QSlider::TicksAbove + + + + + + + QComboBox::AdjustToContents + + + + Global Iterative + + + + + Global Full + + + + + Local Optimized + + + + + + + + + + - Span to all maps + Align poses with GPS + + + true + + + + + + + Time grid (s) + + + + + + + Time optimization (s) + + + + + + + + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + - true + true - + - - + + - WM + - + + true + + - + + + + + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Align poses with ground truth + + + true + + + + + + + RMSE (m) + + + + + + + + + Root + + + + + + + Span to all maps + + + true + + + + + + + WM + + + + + + + + + + + + + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Ignore intermediate nodes + + + true + + + + + + + + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Poses + + + + + + + Path length (m) + + + + + + + Align scans/clouds with ground truth + + + true + + + + + + + <html><head/><body><p>N: Neighbor</p><p>NM: Neighbor Merged</p><p>G: Global</p><p>LS: Local by Space (Proximity)</p><p>LT: Local by Time (Proximity)</p><p>U: User</p><p>P: Prior</p><p>LM: Landmark</p><p>GR: Gravity</p></body></html> + + + Links (N, NM, G, LS, LT, U, P, LM, GR) + + + + + + + + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + false + + + + + + + + + + true + + + + + + + Env Sensor Colormap + + + + + + + + Disabled + + + + + Wifi + + + + + Temperature + + + + + Air Pressure + + + + + Light + + + + + Relative Humidity + + + + + - - - - Time grid (s) - - - - - - - RMSE (m) - - - - - - - - - - true - - - - - - - - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - Align scans/clouds with ground truth - - - true - - - - - - - - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - <html><head/><body><p>N: Neighbor</p><p>NM: Neighbor Merged</p><p>G: Global</p><p>LS: Local by Space (Proximity)</p><p>LT: Local by Time (Proximity)</p><p>U: User</p><p>P: Prior</p><p>LM: Landmark</p><p>GR: Gravity</p></body></html> - - - Links (N, NM, G, LS, LT, U, P, LM, GR) - - - - - - - Path length (m) - - - - - - - Poses - - - - - - - - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - - - - - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - Ignore intermediate nodes - - - true - - - - - - - Time optimization (s) - - - - - - - - - - true - - - - - - - Align poses with GPS - - - true - - - - - - - - - - true - - - - + - + @@ -1805,7 +1884,7 @@ 0 - -316 + 0 518 1071 @@ -1815,7 +1894,7 @@ - + @@ -2526,8 +2605,8 @@ 0 0 - 296 - 272 + 222 + 306 @@ -2700,8 +2779,8 @@ 0 0 - 428 - 196 + 181 + 485 From e75baec73eedbace3e6f5f2d886c2c8bbce3ce61 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 16 Nov 2025 18:56:55 -0800 Subject: [PATCH 056/126] Graph viewer optimizations (#1616) * DbViewer: coloring graph based on env sensor value (e.g., wifi signal strength) * GraphViewer optimizations * Fixed link memory allocation * refactor * more optimizations * mainwindow keypoints optimization * MainWindow optimizations * Shrink graphics scene on clearAll --- corelib/src/GlobalMap.cpp | 15 +- guilib/include/rtabmap/gui/GraphViewer.h | 10 +- guilib/src/DatabaseViewer.cpp | 11 +- guilib/src/GraphViewer.cpp | 452 +++++++++--------- guilib/src/MainWindow.cpp | 581 ++++++++++++----------- 5 files changed, 579 insertions(+), 490 deletions(-) diff --git a/corelib/src/GlobalMap.cpp b/corelib/src/GlobalMap.cpp index 2b40d7b4..5ee319f6 100644 --- a/corelib/src/GlobalMap.cpp +++ b/corelib/src/GlobalMap.cpp @@ -139,14 +139,25 @@ bool GlobalMap::update(const std::map & poses) std::list > orderedPoses; // add old poses that were not in the current map (they were just retrieved from LTM) + int nodesNotAssembled = 0; + int nodesNotInCache = 0; for(std::map::const_iterator iter=poses.lower_bound(1); iter!=poses.end(); ++iter) { if(!isNodeAssembled(iter->first)) { - UDEBUG("Pose %d not found in current added poses, it will be added to map", iter->first); - orderedPoses.push_back(*iter); + if(uContains(cache(), iter->first)) + { + ++nodesNotAssembled; + //UDEBUG("Pose %d not found in current added poses, it will be added to map", iter->first); + orderedPoses.push_back(*iter); + } + else + { + ++nodesNotInCache; + } } } + UDEBUG("%d nodes will be assembled in the map and %d nodes won't (no local grids in cache for them)", nodesNotAssembled, nodesNotInCache); // insert zero after if(poses.find(0) != poses.end()) diff --git a/guilib/include/rtabmap/gui/GraphViewer.h b/guilib/include/rtabmap/gui/GraphViewer.h index d80023e5..8cf9d68a 100644 --- a/guilib/include/rtabmap/gui/GraphViewer.h +++ b/guilib/include/rtabmap/gui/GraphViewer.h @@ -119,8 +119,9 @@ public: bool isReferentialVisible() const; bool isLocalRadiusVisible() const; float getLoopClosureOutlierThr() const {return _loopClosureOutlierThr;} - float getMaxLinkLength() const {return _maxLinkLength;} + float getMinLinkLength() const {return _minLinkLength;} bool isGraphVisible() const; + bool isNodeVisible() const; bool isGlobalPathVisible() const; bool isLocalPathVisible() const; bool isGtGraphVisible() const; @@ -159,7 +160,7 @@ public: void setReferentialVisible(bool visible); void setLocalRadiusVisible(bool visible); void setLoopClosureOutlierThr(float value); - void setMaxLinkLength(float value); + void setMinLinkLength(float value); void setGraphVisible(bool visible); void setGlobalPathVisible(bool visible); void setLocalPathVisible(bool visible); @@ -185,6 +186,9 @@ protected: virtual void mousePressEvent(QMouseEvent * event); virtual void contextMenuEvent(QContextMenuEvent * event); +private: + void setupGraphicsScene(); + private: QString _workingDirectory; QColor _nodeColor; @@ -238,7 +242,7 @@ private: QGraphicsEllipseItem * _localRadius; QGraphicsRectItem * _odomCacheOverlay; float _loopClosureOutlierThr; - float _maxLinkLength; + float _minLinkLength; bool _orientationENU; bool _mouseTracking; ViewPlane _viewPlane; diff --git a/guilib/src/DatabaseViewer.cpp b/guilib/src/DatabaseViewer.cpp index d2e2e8a7..0b85b5ae 100644 --- a/guilib/src/DatabaseViewer.cpp +++ b/guilib/src/DatabaseViewer.cpp @@ -394,11 +394,10 @@ DatabaseViewer::DatabaseViewer(const QString & ini, QWidget * parent) : connect(ui_->toolButton_constraint, SIGNAL(clicked(bool)), this, SLOT(editConstraint())); connect(ui_->checkBox_enableForAll, SIGNAL(stateChanged(int)), this, SLOT(updateConstraintButtons())); - ui_->horizontalSlider_iterations->setTracking(false); + ui_->horizontalSlider_iterations->setTracking(true); ui_->horizontalSlider_iterations->setEnabled(false); ui_->spinBox_optimizationsFrom->setEnabled(false); connect(ui_->horizontalSlider_iterations, SIGNAL(valueChanged(int)), this, SLOT(sliderIterationsValueChanged(int))); - connect(ui_->horizontalSlider_iterations, SIGNAL(sliderMoved(int)), this, SLOT(sliderIterationsValueChanged(int))); connect(ui_->spinBox_optimizationsFrom, SIGNAL(editingFinished()), this, SLOT(updateGraphView())); connect(ui_->comboBox_optimizationFlavor, SIGNAL(activated(int)), this, SLOT(updateGraphView())); connect(ui_->checkBox_spanAllMaps, SIGNAL(stateChanged(int)), this, SLOT(updateGraphView())); @@ -7104,6 +7103,7 @@ void DatabaseViewer::updateConstraintButtons() void DatabaseViewer::sliderIterationsValueChanged(int value) { + UDEBUG("sender=%s value=%d currentValue = %d", sender()?sender()->objectName().toStdString().c_str():"NA", value, ui_->horizontalSlider_iterations->value()); if(dbDriver_ && value >=0 && value < (int)graphes_.size()) { std::map graph = uValueAt(graphes_, value); @@ -7284,6 +7284,7 @@ void DatabaseViewer::sliderIterationsValueChanged(int value) break; } ui_->graphViewer->updateNodeColorByValue(legend, colors, 0.0f, 0.0f, invertedColor, 0, hueMax, 1); + UDEBUG("Updated node color based on env sensor %d", (int)curentType); } else if(ui_->checkBox_wmState->isEnabled() && ui_->checkBox_wmState->isChecked() && @@ -7306,6 +7307,7 @@ void DatabaseViewer::sliderIterationsValueChanged(int value) { ui_->graphViewer->updateNodeColorByValue("In WM", colors, 1, false, 1); } + UDEBUG("Updated node color based working memory state"); } QGraphicsRectItem * rectScaleItem = 0; ui_->graphViewer->clearMap(); @@ -7509,13 +7511,16 @@ void DatabaseViewer::sliderIterationsValueChanged(int value) } #endif } - ui_->graphViewer->fitInView(ui_->graphViewer->scene()->itemsBoundingRect(), Qt::KeepAspectRatio); + if(rectScaleItem != 0) { ui_->graphViewer->fitInView(rectScaleItem, Qt::KeepAspectRatio); ui_->graphViewer->scene()->removeItem(rectScaleItem); delete rectScaleItem; } + else { + ui_->graphViewer->fitInView(ui_->graphViewer->sceneRect(), Qt::KeepAspectRatio); + } ui_->graphViewer->update(); ui_->label_iterations->setNum(value); diff --git a/guilib/src/GraphViewer.cpp b/guilib/src/GraphViewer.cpp index 10c76f99..07144a4e 100644 --- a/guilib/src/GraphViewer.cpp +++ b/guilib/src/GraphViewer.cpp @@ -337,17 +337,37 @@ GraphViewer::GraphViewer(QWidget * parent) : _gridCellSize(0.0f), _localRadius(0), _loopClosureOutlierThr(0), - _maxLinkLength(0.02f), + _minLinkLength(0.02f), _orientationENU(false), _mouseTracking(false), _viewPlane(XY), _ensureFrameVisible(true) { - this->setScene(new QGraphicsScene(this)); this->setDragMode(QGraphicsView::ScrollHandDrag); _workingDirectory = QDir::homePath(); - this->scene()->clear(); + setupGraphicsScene(); + + // Match by default scan colors from DatabaseViewer + _highlightedNodes.push_back(QPair(Qt::yellow, nullptr)); + _highlightedNodes.push_back(QPair(Qt::magenta, nullptr)); + + this->restoreDefaults(); + + this->fitInView(this->sceneRect(), Qt::KeepAspectRatio); +} + +GraphViewer::~GraphViewer() +{ +} + +void GraphViewer::setupGraphicsScene() +{ + if(this->scene()) + { + delete this->scene(); + } + this->setScene(new QGraphicsScene(this)); _world = (QGraphicsItem *)this->scene()->addEllipse(QRectF(-0.0001,-0.0001,0.0001,0.0001)); _root = (QGraphicsItem *)this->scene()->addEllipse(QRectF(-0.0001,-0.0001,0.0001,0.0001)); _root->setParentItem(_world); @@ -485,17 +505,6 @@ GraphViewer::GraphViewer(QWidget * parent) : _odomCacheOverlay->setBrush(QBrush(QColor(255, 255, 255, 150))); _odomCacheOverlay->setPen(QPen(Qt::NoPen)); - // Match by default scan colors from DatabaseViewer - _highlightedNodes.push_back(QPair(Qt::yellow, nullptr)); - _highlightedNodes.push_back(QPair(Qt::magenta, nullptr)); - - this->restoreDefaults(); - - this->fitInView(this->sceneRect(), Qt::KeepAspectRatio); -} - -GraphViewer::~GraphViewer() -{ } void GraphViewer::setWorldMapRotation(const float & theta) @@ -510,18 +519,48 @@ void GraphViewer::updateGraph(const std::map & poses, const std::map & weights, const std::set & odomCacheIds) { - UTimer timer; - bool wasVisible = _graphRoot->isVisible(); - _graphRoot->show(); - - bool wasEmpty = _nodeItems.size() == 0 && _linkItems.size() == 0; - UDEBUG("poses=%d constraints=%d", (int)poses.size(), (int)constraints.size()); - //Hide nodes and links - for(QMap::iterator iter = _nodeItems.begin(); iter!=_nodeItems.end(); ++iter) + if(!_graphRoot->isVisible()) { + UDEBUG("Ignoring updating graph, the graph root is not visible."); + return; + } + + UTimer timer; + + bool wasEmpty = _nodeItems.size() == 0 && _linkItems.size() == 0 && _gridMap->pixmap().isNull(); + UDEBUG("poses=%ld constraints=%ld mapIds=%ld weights=%ld", poses.size(), constraints.size(), mapIds.size(), weights.size()); + for(QMultiMap::iterator iter = _linkItems.begin(); iter!=_linkItems.end(); ++iter) + { + iter.value()->hide(); + } + UDEBUG("hidden %d links", _linkItems.size()); + + int created = 0; + int reused = 0; + int removed = 0; + QMap::iterator nter = _nodeItems.begin(); + std::map::const_iterator iter=_nodeVisible?poses.begin():poses.end(); + while(nter!=_nodeItems.end() || iter!=poses.end()) + { + if(nter!=_nodeItems.end() && (iter==poses.end() || nter.key() < iter->first || iter->second.isNull())) + { + // NodeItem is not in poses anymore, increase only _nodeItems iterator + for(int i=0; i<_highlightedNodes.size(); ++i) + { + if(_highlightedNodes[i].second && _highlightedNodes[i].second == nter.value()) + { + _highlightedNodes[i].second = nullptr; + } + } + delete nter.value(); + nter = _nodeItems.erase(nter); + ++removed; + continue; + } + QColor color = _nodeColor; - bool isOdomCache = odomCacheIds.find(iter.key()) != odomCacheIds.end(); - if(iter.key()<0) + bool isOdomCache = odomCacheIds.find(iter->first) != odomCacheIds.end(); + if(iter->first<0) { color = QColor(255-color.red(), 255-color.green(), 255-color.blue()); } @@ -529,51 +568,50 @@ void GraphViewer::updateGraph(const std::map & poses, { color = _nodeOdomCacheColor; } - iter.value()->hide(); - iter.value()->setColor(color); // reset color - iter.value()->setToolTipInfo(QString()); - iter.value()->setZValue(iter.key()<0?21:20); - } - for(QMultiMap::iterator iter = _linkItems.begin(); iter!=_linkItems.end(); ++iter) - { - iter.value()->hide(); - } - for(std::map::const_iterator iter=poses.begin(); iter!=poses.end(); ++iter) - { - if(!iter->second.isNull()) + UASSERT(iter!=poses.end()); + + if(nter == _nodeItems.end() || nter.key() > iter->first) { - QMap::iterator itemIter = _nodeItems.find(iter->first); - if(itemIter != _nodeItems.end()) - { - itemIter.value()->setPose(iter->second, _viewPlane); - itemIter.value()->show(); - } - else - { - // create node item - QColor color = _nodeColor; - bool isOdomCache = odomCacheIds.find(iter->first) != odomCacheIds.end(); - if(iter->first<0) - { - color = QColor(255-color.red(), 255-color.green(), 255-color.blue()); - } - else if(isOdomCache) - { - color = _nodeOdomCacheColor; - } - const Transform & pose = iter->second; - NodeItem * item = new NodeItem(iter->first, uContains(mapIds, iter->first)?mapIds.at(iter->first):-1, pose, _nodeRadius, uContains(weights, iter->first)?weights.at(iter->first):-1, _viewPlane, _linkWidth); - this->scene()->addItem(item); - item->setZValue(iter->first<0?21:20); - item->setColor(color); - item->setParentItem(_graphRoot); - item->show(); - _nodeItems.insert(iter->first, item); - } + // NodeItem is not in poses, create a new one and increase poses iterator + const Transform & pose = iter->second; + NodeItem * item = new NodeItem( + iter->first, + uValue(mapIds, iter->first, -1), + pose, + _nodeRadius, + uValue(weights, iter->first, -1), + _viewPlane, + _linkWidth); + this->scene()->addItem(item); + item->setZValue(iter->first<0?21:20); + item->setColor(color); + item->setParentItem(_graphRoot); + item->show(); + _nodeItems.insert(iter->first, item); + ++iter; + ++created; + } + else + { + // NodeItem exists for the pose, copy data and increase both iterators + UASSERT(iter->first == nter.key()); + nter.value()->setColor(color); // reset color + nter.value()->setToolTipInfo(QString()); + nter.value()->setZValue(iter->first<0?21:20); + nter.value()->setPose(iter->second, _viewPlane); + nter.value()->show(); + ++nter; + ++iter; + ++reused; } } - + UDEBUG("Nodes created=%d, reused=%d removed=%d", created, reused, removed); + created = 0; + reused = 0; + removed = 0; + int removedSmallLinks = 0; + int ignoredSmallLinks = 0; for(std::multimap::const_iterator iter=constraints.begin(); iter!=constraints.end(); ++iter) { // make the first id the smallest one @@ -587,30 +625,28 @@ void GraphViewer::updateGraph(const std::map & poses, std::map::const_iterator jterA = poses.find(idFrom); std::map::const_iterator jterB = poses.find(idTo); LinkItem * linkItem = 0; - if(jterA != poses.end() && jterB != poses.end() && - _nodeItems.contains(idFrom) && _nodeItems.contains(idTo)) + if(jterA != poses.end() && jterB != poses.end()) { const Transform & poseA = jterA->second; const Transform & poseB = jterB->second; - QMultiMap::iterator itemIter = _linkItems.end(); - - if(_linkItems.contains(idFrom)) + QMultiMap::iterator itemIter = _linkItems.find(idFrom); + bool alreadyAdded = false; + while(itemIter != _linkItems.end() && itemIter.key() == idFrom) { - itemIter = _linkItems.find(idFrom); - bool alreadyAdded = false; - while(itemIter != _linkItems.end() && itemIter.key() == idFrom) + if(itemIter.value()->to() == idTo) { - if(itemIter.value()->to() == idTo && itemIter.value()->isVisible()) - { + if(itemIter.value()->isVisible()) { alreadyAdded = true; - break; + } else { + linkItem = itemIter.value(); } - ++itemIter; - } - if(alreadyAdded){ - continue; + break; } + ++itemIter; + } + if(alreadyAdded){ + continue; } bool interSessionClosure = false; @@ -625,11 +661,15 @@ void GraphViewer::updateGraph(const std::map & poses, if(isLinkedToOdomCachePoses) { - _nodeItems.value(idFrom)->setZValue(odomCacheIds.find(idFrom)!=odomCacheIds.end()?24:23); - _nodeItems.value(idTo)->setZValue(odomCacheIds.find(idTo)!=odomCacheIds.end()?24:23); + if(_nodeItems.contains(idFrom)) { + _nodeItems.value(idFrom)->setZValue(odomCacheIds.find(idFrom)!=odomCacheIds.end()?24:23); + } + if(_nodeItems.contains(idTo)) { + _nodeItems.value(idTo)->setZValue(odomCacheIds.find(idTo)!=odomCacheIds.end()?24:23); + } } - if(poseA.getDistance(poseB) > _maxLinkLength) + if(poseA.getDistance(poseB) > _minLinkLength) { if(linkItem == 0) { @@ -642,14 +682,24 @@ void GraphViewer::updateGraph(const std::map & poses, this->scene()->addItem(linkItem); linkItem->setParentItem(_graphRoot); _linkItems.insert(idFrom, linkItem); + ++created; + } + else { + linkItem->setPoses(poseA, poseB, _viewPlane); + linkItem->show(); + ++reused; } } - else if(linkItem && itemIter != _linkItems.end()) + else if(linkItem) { // erase small links _linkItems.erase(itemIter); delete linkItem; linkItem = 0; + ++removedSmallLinks; + } + else { + ++ignoredSmallLinks; } if(linkItem) @@ -730,71 +780,48 @@ void GraphViewer::updateGraph(const std::map & poses, } } } - + UDEBUG("Links created=%d, reused=%d, small links: removed=%d ignored=%d", created, reused, removedSmallLinks, ignoredSmallLinks); //remove not used nodes and links - for(QMap::iterator iter = _nodeItems.begin(); iter!=_nodeItems.end();) - { - if(!iter.value()->isVisible()) - { - for(int i=0; i<_highlightedNodes.size(); ++i) - { - if(_highlightedNodes[i].second && _highlightedNodes[i].second == iter.value()) - { - _highlightedNodes[i].second = nullptr; - } - } - - delete iter.value(); - iter = _nodeItems.erase(iter); - } - else - { - iter.value()->setVisible(_nodeVisible); - ++iter; - } - } + removed = 0; + int visible = 0; for(QMultiMap::iterator iter = _linkItems.begin(); iter!=_linkItems.end();) { if(!iter.value()->isVisible()) { delete iter.value(); iter = _linkItems.erase(iter); + ++removed; } else { ++iter; + ++visible; } } - + UDEBUG("Links removed=%d, visible=%d", removed, visible); if(_nodeItems.size()) { (--_nodeItems.end()).value()->setColor(_nodeOdomCacheColor); } - - this->scene()->setSceneRect(this->scene()->itemsBoundingRect()); // Re-shrink the scene to it's bounding contents - - if(!odomCacheIds.empty()) - _odomCacheOverlay->setRect(this->scene()->itemsBoundingRect()); - else - _odomCacheOverlay->setRect(0, 0, 0, 0); - + if(wasEmpty) { - QRectF rect = this->scene()->itemsBoundingRect(); + QRectF rect = this->scene()->sceneRect(); this->fitInView(rect.adjusted(-rect.width()/2.0f, -rect.height()/2.0f, rect.width()/2.0f, rect.height()/2.0f), Qt::KeepAspectRatio); } - _graphRoot->setVisible(wasVisible); - UDEBUG("_nodeItems=%d, _linkItems=%d, timer=%fs", _nodeItems.size(), _linkItems.size(), timer.ticks()); } void GraphViewer::updateGTGraph(const std::map & poses) { + if(!_gtGraphRoot->isVisible()) + { + UDEBUG("Ignoring updating gt graph, the graph root is not visible."); + return; + } + UTimer timer; - bool wasVisible = _gtGraphRoot->isVisible(); - _gtGraphRoot->show(); - bool wasEmpty = _gtNodeItems.size() == 0 && _gtLinkItems.size() == 0; UDEBUG("poses=%d", (int)poses.size()); //Hide nodes and links for(QMap::iterator iter = _gtNodeItems.begin(); iter!=_gtNodeItems.end(); ++iter) @@ -811,23 +838,26 @@ void GraphViewer::updateGTGraph(const std::map & poses) { if(!iter->second.isNull()) { - QMap::iterator itemIter = _gtNodeItems.find(iter->first); - if(itemIter != _gtNodeItems.end()) + if(_nodeVisible) { - itemIter.value()->setPose(iter->second, _viewPlane); - itemIter.value()->show(); - } - else - { - // create node item - const Transform & pose = iter->second; - NodeItem * item = new NodeItem(iter->first, -1, pose, _nodeRadius, -1, _viewPlane, _linkWidth); - this->scene()->addItem(item); - item->setZValue(20); - item->setColor(_gtPathColor); - item->setParentItem(_gtGraphRoot); - item->setVisible(_nodeVisible); - _gtNodeItems.insert(iter->first, item); + QMap::iterator itemIter = _gtNodeItems.find(iter->first); + if(itemIter != _gtNodeItems.end()) + { + itemIter.value()->setPose(iter->second, _viewPlane); + itemIter.value()->show(); + } + else + { + // create node item + const Transform & pose = iter->second; + NodeItem * item = new NodeItem(iter->first, -1, pose, _nodeRadius, -1, _viewPlane, _linkWidth); + this->scene()->addItem(item); + item->setZValue(20); + item->setColor(_gtPathColor); + item->setParentItem(_gtGraphRoot); + item->setVisible(_nodeVisible); + _gtNodeItems.insert(iter->first, item); + } } if(iter!=poses.begin()) @@ -913,20 +943,6 @@ void GraphViewer::updateGTGraph(const std::map & poses) ++iter; } } - - if(_gtNodeItems.size() || _gtLinkItems.size()) - { - this->scene()->setSceneRect(this->scene()->itemsBoundingRect()); // Re-shrink the scene to it's bounding contents - - if(wasEmpty) - { - QRectF rect = this->scene()->itemsBoundingRect(); - this->fitInView(rect.adjusted(-rect.width()/2.0f, -rect.height()/2.0f, rect.width()/2.0f, rect.height()/2.0f), Qt::KeepAspectRatio); - } - } - - _gtGraphRoot->setVisible(wasVisible); - UDEBUG("_gtNodeItems=%d, _gtLinkItems=%d timer=%fs", _gtNodeItems.size(), _gtLinkItems.size(), timer.ticks()); } @@ -934,10 +950,11 @@ void GraphViewer::updateGPSGraph( const std::map & poses, const std::map & gpsValues) { + if(!_gpsGraphRoot->isVisible()) { + UDEBUG("Ignoring updating gps graph, the graph root is not visible."); + return; + } UTimer timer; - bool wasVisible = _gpsGraphRoot->isVisible(); - _gpsGraphRoot->show(); - bool wasEmpty = _gpsNodeItems.size() == 0 && _gpsNodeItems.size() == 0; UDEBUG("poses=%d", (int)poses.size()); //Hide nodes and links for(QMap::iterator iter = _gpsNodeItems.begin(); iter!=_gpsNodeItems.end(); ++iter) @@ -954,24 +971,27 @@ void GraphViewer::updateGPSGraph( { if(!iter->second.isNull()) { - QMap::iterator itemIter = _gpsNodeItems.find(iter->first); - if(itemIter != _gpsNodeItems.end()) + if(_nodeVisible) { - itemIter.value()->setPose(iter->second, _viewPlane); - itemIter.value()->show(); - } - else - { - // create node item - const Transform & pose = iter->second; - UASSERT(gpsValues.find(iter->first) != gpsValues.end()); - NodeItem * item = new NodeGPSItem(iter->first, -1, pose, _nodeRadius, gpsValues.at(iter->first), _viewPlane, _linkWidth); - this->scene()->addItem(item); - item->setZValue(20); - item->setColor(_gpsPathColor); - item->setParentItem(_gpsGraphRoot); - item->setVisible(_nodeVisible); - _gpsNodeItems.insert(iter->first, item); + QMap::iterator itemIter = _gpsNodeItems.find(iter->first); + if(itemIter != _gpsNodeItems.end()) + { + itemIter.value()->setPose(iter->second, _viewPlane); + itemIter.value()->show(); + } + else + { + // create node item + const Transform & pose = iter->second; + UASSERT(gpsValues.find(iter->first) != gpsValues.end()); + NodeItem * item = new NodeGPSItem(iter->first, -1, pose, _nodeRadius, gpsValues.at(iter->first), _viewPlane, _linkWidth); + this->scene()->addItem(item); + item->setZValue(20); + item->setColor(_gpsPathColor); + item->setParentItem(_gpsGraphRoot); + item->setVisible(_nodeVisible); + _gpsNodeItems.insert(iter->first, item); + } } if(iter!=poses.begin()) @@ -1043,20 +1063,6 @@ void GraphViewer::updateGPSGraph( ++iter; } } - - if(_gpsNodeItems.size() || _gpsLinkItems.size()) - { - this->scene()->setSceneRect(this->scene()->itemsBoundingRect()); // Re-shrink the scene to it's bounding contents - - if(wasEmpty) - { - QRectF rect = this->scene()->itemsBoundingRect(); - this->fitInView(rect.adjusted(-rect.width()/2.0f, -rect.height()/2.0f, rect.width()/2.0f, rect.height()/2.0f), Qt::KeepAspectRatio); - } - } - - _gpsGraphRoot->setVisible(wasVisible); - UDEBUG("_gpsNodeItems=%d, _gpsLinkItems=%d timer=%fs", _gpsNodeItems.size(), _gpsLinkItems.size(), timer.ticks()); } @@ -1085,6 +1091,7 @@ void GraphViewer::updateMap(const cv::Mat & map8U, float resolution, float xMin, UASSERT(map8U.empty() || (!map8U.empty() && resolution > 0.0f)); if(!map8U.empty()) { + bool wasEmpty = _nodeItems.size() <= 1 && _linkItems.size() == 0 && _gridMap->pixmap().isNull(); _gridCellSize = resolution; QImage image = uCvMat2QImage(map8U, false); _gridMap->resetTransform(); @@ -1092,8 +1099,11 @@ void GraphViewer::updateMap(const cv::Mat & map8U, float resolution, float xMin, _gridMap->setRotation(90); _gridMap->setPixmap(QPixmap::fromImage(image)); _gridMap->setPos(-yMin*100.0f, -xMin*100.0f); - // Re-shrink the scene to it's bounding contents - this->scene()->setSceneRect(this->scene()->itemsBoundingRect()); + + if(wasEmpty) + { + this->fitInView(this->scene()->sceneRect(), Qt::KeepAspectRatio); + } } else { @@ -1186,10 +1196,10 @@ void GraphViewer::updateNodeColorByValue( } } } - else if(min < max) { + else if(min >= max) { UWARN("min (%f) is not less than max (%f), cannot change color of the graph.", min, max); } - else if(hueMin < hueMax) { + else if(hueMin >= hueMax) { UWARN("Hue min (%d) is not less than hue max (%d), cannot change color of the graph. The hue values should be set between 0 (red) and 360(pink).", (int)hueMin, (int)hueMax); } } @@ -1260,6 +1270,11 @@ void GraphViewer::setNodeInfo(int id, const QString & info) void GraphViewer::setLocalRadius(float radius) { _localRadius->setRect(-radius*100, -radius*100, radius*200, radius*200); + if(_nodeItems.empty() && _linkItems.empty() && _gridMap->pixmap().isNull()) + { + QRectF rect = this->scene()->sceneRect(); + this->fitInView(rect.adjusted(-rect.width()/2.0f, -rect.height()/2.0f, rect.width()/2.0f, rect.height()/2.0f), Qt::KeepAspectRatio); + } } void GraphViewer::updateLocalPath(const std::vector & localPath) @@ -1384,14 +1399,17 @@ void GraphViewer::clearGraph() _worldMapRotation = 0.0f; _referential->resetTransform(); _localRadius->resetTransform(); - this->scene()->setSceneRect(this->scene()->itemsBoundingRect()); // Re-shrink the scene to it's bounding contents } void GraphViewer::clearMap() { - _gridMap->setPixmap(QPixmap()); _gridCellSize = 0.0f; - this->scene()->setSceneRect(this->scene()->itemsBoundingRect()); // Re-shrink the scene to it's bounding contents + if(_gridMap->pixmap().isNull()) + { + // there is no grid map, just return + return; + } + _gridMap->setPixmap(QPixmap()); } void GraphViewer::clearPosterior() @@ -1411,6 +1429,14 @@ void GraphViewer::clearAll() { clearMap(); clearGraph(); + + // The only way to re-shrink the dynamic scene rect is to re-create the QGraphicsScene. + QSettings tmp; + saveSettings(tmp); + QRectF localRadiusRectF = _localRadius->rect(); + setupGraphicsScene(); + loadSettings(tmp); // restore previous state + _localRadius->setRect(localRadiusRectF); } void GraphViewer::saveSettings(QSettings & settings, const QString & group) const @@ -1447,8 +1473,9 @@ void GraphViewer::saveSettings(QSettings & settings, const QString & group) cons settings.setValue("referential_visible", this->isReferentialVisible()); settings.setValue("local_radius_visible", this->isLocalRadiusVisible()); settings.setValue("loop_closure_outlier_thr", this->getLoopClosureOutlierThr()); - settings.setValue("max_link_length", this->getMaxLinkLength()); + settings.setValue("min_link_length", this->getMinLinkLength()); settings.setValue("graph_visible", this->isGraphVisible()); + settings.setValue("node_visible", this->isNodeVisible()); settings.setValue("global_path_visible", this->isGlobalPathVisible()); settings.setValue("local_path_visible", this->isLocalPathVisible()); settings.setValue("gt_graph_visible", this->isGtGraphVisible()); @@ -1497,8 +1524,9 @@ void GraphViewer::loadSettings(QSettings & settings, const QString & group) this->setLocalRadiusVisible(settings.value("local_radius_visible", this->isLocalRadiusVisible()).toBool()); this->setIntraInterSessionColorsEnabled(settings.value("intra_inter_session_colors_enabled", this->isIntraInterSessionColorsEnabled()).toBool()); this->setLoopClosureOutlierThr(settings.value("loop_closure_outlier_thr", this->getLoopClosureOutlierThr()).toDouble()); - this->setMaxLinkLength(settings.value("max_link_length", this->getMaxLinkLength()).toDouble()); + this->setMinLinkLength(settings.value("min_link_length", this->getMinLinkLength()).toDouble()); this->setGraphVisible(settings.value("graph_visible", this->isGraphVisible()).toBool()); + this->setNodeVisible(settings.value("node_visible", this->isNodeVisible()).toBool()); this->setGlobalPathVisible(settings.value("global_path_visible", this->isGlobalPathVisible()).toBool()); this->setLocalPathVisible(settings.value("local_path_visible", this->isLocalPathVisible()).toBool()); this->setGtGraphVisible(settings.value("gt_graph_visible", this->isGtGraphVisible()).toBool()); @@ -1533,6 +1561,10 @@ bool GraphViewer::isGraphVisible() const { return _graphRoot->isVisible(); } +bool GraphViewer::isNodeVisible() const +{ + return _nodeVisible; +} bool GraphViewer::isGlobalPathVisible() const { return _globalPathRoot->isVisible(); @@ -1855,9 +1887,9 @@ void GraphViewer::setLoopClosureOutlierThr(float value) { _loopClosureOutlierThr = value; } -void GraphViewer::setMaxLinkLength(float value) +void GraphViewer::setMinLinkLength(float value) { - _maxLinkLength = value; + _minLinkLength = value; } void GraphViewer::setGraphVisible(bool visible) { @@ -1898,10 +1930,6 @@ void GraphViewer::setOrientationENU(bool enabled) QTransform t; t.rotateRadians(_worldMapRotation); _root->setTransform(t); - if(_nodeItems.size() || _linkItems.size()) - { - this->scene()->setSceneRect(this->scene()->itemsBoundingRect()); // Re-shrink the scene to it's bounding contents - } } void GraphViewer::setViewPlane(ViewPlane plane) @@ -1928,11 +1956,6 @@ void GraphViewer::setViewPlane(ViewPlane plane) _referentialXY->setVisible(plane==XY); _referentialXZ->setVisible(plane==XZ); _referentialYZ->setVisible(plane==YZ); - - if(_nodeItems.size() || _linkItems.size()) - { - this->scene()->setSceneRect(this->scene()->itemsBoundingRect()); // Re-shrink the scene to it's bounding contents - } } void GraphViewer::setEnsureFrameVisible(bool visible) { @@ -2123,7 +2146,7 @@ void GraphViewer::contextMenuEvent(QContextMenuEvent * event) menu.addSeparator(); QAction * aSetNodeSize = menu.addAction(tr("Set node radius...")); QAction * aSetLinkSize = menu.addAction(tr("Set link width...")); - QAction * aChangeMaxLinkLength = menu.addAction(tr("Set maximum link length...")); + QAction * aChangeMinLinkLength = menu.addAction(tr("Set minimum link length...")); menu.addSeparator(); QAction * aEnsureFrameVisible; QAction * aShowHideGridMap; @@ -2241,12 +2264,12 @@ void GraphViewer::contextMenuEvent(QContextMenuEvent * event) aMouseTracking->setCheckable(true); aMouseTracking->setChecked(_mouseTracking); aMouseTracking->setEnabled(_viewPlane == XY); - aShowHideGraph->setEnabled(_nodeItems.size() && _viewPlane == XY); - aShowHideGraphNodes->setEnabled(_nodeItems.size() && _graphRoot->isVisible()); + aShowHideGraph->setEnabled(_viewPlane == XY); + aShowHideGraphNodes->setEnabled(_graphRoot->isVisible()); aShowHideGlobalPath->setEnabled(_globalPathLinkItems.size()); aShowHideLocalPath->setEnabled(_localPathLinkItems.size()); - aShowHideGtGraph->setEnabled(_gtNodeItems.size()); - aShowHideGPSGraph->setEnabled(_gpsNodeItems.size()); + aShowHideGtGraph->setEnabled(_gtGraphRoot->isVisible()); + aShowHideGPSGraph->setEnabled(_gpsGraphRoot->isVisible()); aShowHideOdomCacheOverlay->setEnabled(_odomCacheOverlay->rect().width()>0); QMenu * viewPlaneMenu = menu.addMenu("View Plane..."); @@ -2363,8 +2386,7 @@ void GraphViewer::contextMenuEvent(QContextMenuEvent * event) //reset scale _root->setScale(1.0f); - this->scene()->setSceneRect(this->scene()->itemsBoundingRect()); // Re-shrink the scene to it's bounding contents - + this->scene()->setSceneRect(QRectF()); QDesktopServices::openUrl(QUrl::fromLocalFile(filePath)); } @@ -2426,13 +2448,13 @@ void GraphViewer::contextMenuEvent(QContextMenuEvent * event) setLoopClosureOutlierThr(value); } } - else if(r == aChangeMaxLinkLength) + else if(r == aChangeMinLinkLength) { bool ok; - double value = QInputDialog::getDouble(this, tr("Maximum link length to be shown"), tr("Value (m)"), _maxLinkLength, 0.0, 1000.0, 3, &ok); + double value = QInputDialog::getDouble(this, tr("Minimum link length to be shown"), tr("Value (m)"), _minLinkLength, 0.0, 1000.0, 3, &ok); if(ok) { - setMaxLinkLength(value); + setMinLinkLength(value); } } else if(r == aChangeNodeColor || diff --git a/guilib/src/MainWindow.cpp b/guilib/src/MainWindow.cpp index df953724..9770c78b 100644 --- a/guilib/src/MainWindow.cpp +++ b/guilib/src/MainWindow.cpp @@ -1588,63 +1588,61 @@ void MainWindow::processOdometry(const rtabmap::OdometryEvent & odom, bool dataI { _odometryReceived = true; // update camera position - if(data->cameraModels().size() && data->cameraModels()[0].isValidForProjection()) + if(_cloudViewer->isVisible()) { - _cloudViewer->updateCameraFrustums(_odometryCorrection*odom.pose(), data->cameraModels()); - } - else if(data->stereoCameraModels().size() && data->stereoCameraModels()[0].isValidForProjection()) - { - _cloudViewer->updateCameraFrustums(_odometryCorrection*odom.pose(), data->stereoCameraModels()); - } - else if(!data->laserScanRaw().isEmpty() || - !data->laserScanCompressed().isEmpty()) - { - Transform scanLocalTransform; - if(!data->laserScanRaw().isEmpty()) + if(data->cameraModels().size() && data->cameraModels()[0].isValidForProjection()) { - scanLocalTransform = data->laserScanRaw().localTransform(); + _cloudViewer->updateCameraFrustums(_odometryCorrection*odom.pose(), data->cameraModels()); + } + else if(data->stereoCameraModels().size() && data->stereoCameraModels()[0].isValidForProjection()) + { + _cloudViewer->updateCameraFrustums(_odometryCorrection*odom.pose(), data->stereoCameraModels()); + } + else if(!data->laserScanRaw().isEmpty() || + !data->laserScanCompressed().isEmpty()) + { + Transform scanLocalTransform; + if(!data->laserScanRaw().isEmpty()) + { + scanLocalTransform = data->laserScanRaw().localTransform(); + } + else + { + scanLocalTransform = data->laserScanCompressed().localTransform(); + } + //fake frustum + CameraModel model( + 2, + 2, + 2, + 1.5, + scanLocalTransform*CameraModel::opticalRotation(), + 0, + cv::Size(4,3)); + _cloudViewer->updateCameraFrustum(_odometryCorrection*odom.pose(), model); + + } +#if PCL_VERSION_COMPARE(>=, 1, 7, 2) + if(_preferencesDialog->isFramesShown()) + { + _cloudViewer->addOrUpdateLine("odom_to_base_link", _odometryCorrection, _odometryCorrection*odom.pose(), qRgb(255, 128, 0), true, false); } else { - scanLocalTransform = data->laserScanCompressed().localTransform(); + _cloudViewer->removeLine("odom_to_base_link"); } - //fake frustum - CameraModel model( - 2, - 2, - 2, - 1.5, - scanLocalTransform*CameraModel::opticalRotation(), - 0, - cv::Size(4,3)); - _cloudViewer->updateCameraFrustum(_odometryCorrection*odom.pose(), model); - - } -#if PCL_VERSION_COMPARE(>=, 1, 7, 2) - if(_preferencesDialog->isFramesShown()) - { - _cloudViewer->addOrUpdateLine("odom_to_base_link", _odometryCorrection, _odometryCorrection*odom.pose(), qRgb(255, 128, 0), true, false); - } - else - { - _cloudViewer->removeLine("odom_to_base_link"); - } #endif - _cloudViewer->updateCameraTargetPosition(_odometryCorrection*odom.pose()); - UDEBUG("Time Update Pose: %fs", time.ticks()); - } - - _cloudViewer->refreshView(); - - if(_ui->graphicsView_graphView->isVisible()) - { - if(!pose.isNull() && !odom.pose().isNull()) + _cloudViewer->updateCameraTargetPosition(_odometryCorrection*odom.pose()); + UDEBUG("Time Update Pose: %fs", time.ticks()); + _cloudViewer->refreshView(); + } + if(_ui->graphicsView_graphView->isVisible()) { _ui->graphicsView_graphView->updateReferentialPosition(_odometryCorrection*odom.pose()); _ui->graphicsView_graphView->update(); UDEBUG("Time Update graphview: %fs", time.ticks()); } - } + } if(_ui->dockWidget_odometry->isVisible() && !data->imageRaw().empty()) @@ -2031,13 +2029,14 @@ void MainWindow::processStats(const rtabmap::Statistics & stat) { // make sure data are uncompressed // We don't need to uncompress images if we don't show them - bool uncompressImages = !signature.sensorData().imageCompressed().empty() && ( - _ui->imageView_source->isVisible() || - (_loopClosureViewer->isVisible() && - !signature.sensorData().depthOrRightCompressed().empty()) || - (_cloudViewer->isVisible() && - _preferencesDialog->isCloudsShown(0) && - !signature.sensorData().depthOrRightCompressed().empty())); + bool uncompressImages = (!signature.sensorData().imageCompressed().empty() && + ((_ui->imageView_source->isVisible() && _ui->imageView_source->isImageShown()) || + _loopClosureViewer->isVisible())) + || + (!signature.sensorData().depthOrRightCompressed().empty() && + ((_ui->imageView_loopClosure->isVisible() && _ui->imageView_loopClosure->isImageShown()) || + (_cloudViewer->isVisible() && _preferencesDialog->isCloudsShown(0)))); + bool uncompressScan = !signature.sensorData().laserScanCompressed().isEmpty() && ( _loopClosureViewer->isVisible() || (_cloudViewer->isVisible() && _preferencesDialog->isScansShown(0))); @@ -2116,11 +2115,19 @@ void MainWindow::processStats(const rtabmap::Statistics & stat) _ui->imageView_source->clear(); _ui->imageView_loopClosure->clear(); - if(signature.sensorData().imageRaw().empty() && signature.getWords().empty()) + // To see colors + QRect rect(0,0,640,480); // default + if(signature.sensorData().cameraModels().size() && signature.sensorData().cameraModels().at(0).imageSize()!=cv::Size()) { - // To see colors - _ui->imageView_source->setSceneRect(QRect(0,0,640,480)); + rect.setWidth(signature.sensorData().cameraModels().at(0).imageWidth()*signature.sensorData().cameraModels().size()); + rect.setHeight(signature.sensorData().cameraModels().at(0).imageHeight()); } + else if(signature.sensorData().stereoCameraModels().size() && signature.sensorData().stereoCameraModels().at(0).left().imageSize()!=cv::Size()) + { + rect.setWidth(signature.sensorData().stereoCameraModels().at(0).left().imageWidth()*signature.sensorData().stereoCameraModels().size()); + rect.setHeight(signature.sensorData().stereoCameraModels().at(0).left().imageHeight()); + } + _ui->imageView_source->setSceneRect(rect); _ui->imageView_source->setBackgroundColor(_ui->imageView_source->getDefaultBackgroundColor()); _ui->imageView_loopClosure->setBackgroundColor(_ui->imageView_loopClosure->getDefaultBackgroundColor()); @@ -2261,22 +2268,27 @@ void MainWindow::processStats(const rtabmap::Statistics & stat) QMap::iterator iter = _cachedSignatures.find(shownLoopId); if(iter != _cachedSignatures.end()) { - // uncompress after copy to avoid keeping uncompressed data in memory loopSignature = iter.value(); - bool uncompressImages = !loopSignature.sensorData().imageCompressed().empty() && ( - _ui->imageView_source->isVisible() || - (_loopClosureViewer->isVisible() && - !loopSignature.sensorData().depthOrRightCompressed().empty())); - bool uncompressScan = _loopClosureViewer->isVisible() && - !loopSignature.sensorData().laserScanCompressed().isEmpty(); - if(uncompressImages || uncompressScan) + + if((_ui->imageView_loopClosure->isVisible() && (_ui->imageView_loopClosure->isImageShown() || _ui->imageView_loopClosure->isImageDepthShown())) || + _loopClosureViewer->isVisible()) { - cv::Mat tmpRGB, tmpDepth; - LaserScan tmpScan; - loopSignature.sensorData().uncompressData( - uncompressImages?&tmpRGB:0, - uncompressImages?&tmpDepth:0, - uncompressScan?&tmpScan:0); + // uncompress after copy to avoid keeping uncompressed data in memory + bool uncompressImages = !loopSignature.sensorData().imageCompressed().empty() && ( + (_ui->imageView_loopClosure->isVisible() && (_ui->imageView_loopClosure->isImageShown() || _ui->imageView_loopClosure->isImageDepthShown())) || + (_loopClosureViewer->isVisible() && + !loopSignature.sensorData().depthOrRightCompressed().empty())); + bool uncompressScan = _loopClosureViewer->isVisible() && + !loopSignature.sensorData().laserScanCompressed().isEmpty(); + if(uncompressImages || uncompressScan) + { + cv::Mat tmpRGB, tmpDepth; + LaserScan tmpScan; + loopSignature.sensorData().uncompressData( + uncompressImages?&tmpRGB:0, + uncompressImages?&tmpDepth:0, + uncompressScan?&tmpScan:0); + } } } } @@ -2294,14 +2306,14 @@ void MainWindow::processStats(const rtabmap::Statistics & stat) !loopSignature.sensorData().imageRaw().empty() || signature.getWords().size()) { - cv::Mat refImage = signature.sensorData().imageRaw(); - cv::Mat loopImage = loopSignature.sensorData().imageRaw(); + cv::Mat refImage = _ui->imageView_source->isImageShown()?signature.sensorData().imageRaw():cv::Mat(); + cv::Mat loopImage = _ui->imageView_loopClosure->isImageShown()?loopSignature.sensorData().imageRaw():cv::Mat(); if( _preferencesDialog->isMarkerDetection() && _preferencesDialog->isLandmarksShown()) { //draw markers - if(!signature.getLandmarks().empty()) + if(!signature.getLandmarks().empty() && !refImage.empty()) { if(refImage.channels() == 1) { @@ -2315,7 +2327,7 @@ void MainWindow::processStats(const rtabmap::Statistics & stat) } drawLandmarks(refImage, signature); } - if(!loopSignature.getLandmarks().empty()) + if(!loopSignature.getLandmarks().empty() && !loopImage.empty()) { if(loopImage.channels() == 1) { @@ -2345,42 +2357,37 @@ void MainWindow::processStats(const rtabmap::Statistics & stat) { _ui->imageView_source->setImage(img); } - if(!signature.sensorData().depthOrRightRaw().empty()) + if(!signature.sensorData().depthOrRightRaw().empty() && _ui->imageView_source->isImageDepthShown()) { _ui->imageView_source->setImageDepth(signature.sensorData().depthOrRightRaw(), signature.sensorData().depthConfidenceRaw()); } - if(img.isNull() && signature.sensorData().depthOrRightRaw().empty()) - { - QRect sceneRect; - if(signature.sensorData().cameraModels().size()) - { - for(unsigned int i=0; iimageView_source->setSceneRect(sceneRect); - } - } if(!lcImg.isNull()) { _ui->imageView_loopClosure->setImage(lcImg); } - if(!loopSignature.sensorData().depthOrRightRaw().empty()) + if(!loopSignature.sensorData().depthOrRightRaw().empty() && _ui->imageView_loopClosure->isImageDepthShown()) { _ui->imageView_loopClosure->setImageDepth(loopSignature.sensorData().depthOrRightRaw(), loopSignature.sensorData().depthConfidenceRaw()); } + + if(lcImg.isNull()) + { + QRect sceneRect; + if(loopSignature.sensorData().cameraModels().size() && loopSignature.sensorData().cameraModels().at(0).imageSize()!=cv::Size()) + { + rect.setWidth(loopSignature.sensorData().cameraModels().at(0).imageWidth()*loopSignature.sensorData().cameraModels().size()); + rect.setHeight(loopSignature.sensorData().cameraModels().at(0).imageHeight()); + } + else if(loopSignature.sensorData().stereoCameraModels().size() && loopSignature.sensorData().stereoCameraModels().at(0).left().imageSize()!=cv::Size()) + { + rect.setWidth(loopSignature.sensorData().stereoCameraModels().at(0).left().imageWidth()*loopSignature.sensorData().stereoCameraModels().size()); + rect.setHeight(loopSignature.sensorData().stereoCameraModels().at(0).left().imageHeight()); + } + if(sceneRect.isValid()) + { + _ui->imageView_loopClosure->setSceneRect(sceneRect); + } + } if(_ui->imageView_loopClosure->sceneRect().isNull()) { _ui->imageView_loopClosure->setSceneRect(_ui->imageView_source->sceneRect()); @@ -2394,24 +2401,37 @@ void MainWindow::processStats(const rtabmap::Statistics & stat) UDEBUG("time= %d ms (update detection imageviews)", time.restart()); - // do it after scaling - std::multimap wordsA; - std::multimap wordsB; - if(signature.getWords().size() == signature.getWordsKpts().size()) + if(_ui->imageView_source->isFeaturesShown() || _ui->imageView_loopClosure->isFeaturesShown() || + (_ui->imageView_source->isLinesShown() && _ui->imageView_loopClosure->isLinesShown())) { - for(std::map::const_iterator iter=signature.getWords().begin(); iter!=signature.getWords().end(); ++iter) + // do it after scaling + std::multimap wordsA; + std::multimap wordsB; + if(signature.getWords().size() == signature.getWordsKpts().size() && + (_ui->imageView_source->isFeaturesShown() || (_ui->imageView_source->isLinesShown() && _ui->imageView_loopClosure->isLinesShown()))) { - wordsA.insert(wordsA.end(), std::make_pair(iter->first, signature.getWordsKpts()[iter->second])); + for(std::map::const_iterator iter=signature.getWords().begin(); iter!=signature.getWords().end(); ++iter) + { + wordsA.insert(wordsA.end(), std::make_pair(iter->first, signature.getWordsKpts()[iter->second])); + } } - } - if(loopSignature.getWords().size() == loopSignature.getWordsKpts().size()) - { - for(std::map::const_iterator iter=loopSignature.getWords().begin(); iter!=loopSignature.getWords().end(); ++iter) + if(loopSignature.getWords().size() == loopSignature.getWordsKpts().size() && + (_ui->imageView_loopClosure->isFeaturesShown() || (_ui->imageView_source->isLinesShown() && _ui->imageView_loopClosure->isLinesShown()))) { - wordsB.insert(wordsB.end(), std::make_pair(iter->first, loopSignature.getWordsKpts()[iter->second])); + for(std::map::const_iterator iter=loopSignature.getWords().begin(); iter!=loopSignature.getWords().end(); ++iter) + { + wordsB.insert(wordsB.end(), std::make_pair(iter->first, loopSignature.getWordsKpts()[iter->second])); + } } + this->drawKeypoints(wordsA, wordsB); + } + else { + _ui->imageView_source->clearFeatures(); + _ui->imageView_loopClosure->clearFeatures(); + _ui->imageView_source->clearLines(); + _ui->imageView_loopClosure->clearLines(); + _lastIds.clear(); } - this->drawKeypoints(wordsA, wordsB); UDEBUG("time= %d ms (draw keypoints)", time.restart()); @@ -2520,43 +2540,45 @@ void MainWindow::processStats(const rtabmap::Statistics & stat) UDEBUG("%d %d %d", poses.size(), poses.size()?poses.rbegin()->first:0, stat.refImageId()); if(!_odometryReceived && poses.size() && poses.rbegin()->first == stat.refImageId()) { - if(poses.rbegin()->first == stat.getLastSignatureData().id()) + if(_cloudViewer->isVisible()) { - if(stat.getLastSignatureData().sensorData().cameraModels().size() && stat.getLastSignatureData().sensorData().cameraModels()[0].isValidForProjection()) + if(poses.rbegin()->first == stat.getLastSignatureData().id()) { - _cloudViewer->updateCameraFrustums(poses.rbegin()->second, stat.getLastSignatureData().sensorData().cameraModels()); - } - else if(stat.getLastSignatureData().sensorData().stereoCameraModels().size() && stat.getLastSignatureData().sensorData().stereoCameraModels()[0].isValidForProjection()) - { - _cloudViewer->updateCameraFrustums(poses.rbegin()->second, stat.getLastSignatureData().sensorData().stereoCameraModels()); - } - else if(!stat.getLastSignatureData().sensorData().laserScanRaw().isEmpty() || - !stat.getLastSignatureData().sensorData().laserScanCompressed().isEmpty()) - { - Transform scanLocalTransform; - if(!stat.getLastSignatureData().sensorData().laserScanRaw().isEmpty()) + if(stat.getLastSignatureData().sensorData().cameraModels().size() && stat.getLastSignatureData().sensorData().cameraModels()[0].isValidForProjection()) { - scanLocalTransform = stat.getLastSignatureData().sensorData().laserScanRaw().localTransform(); + _cloudViewer->updateCameraFrustums(poses.rbegin()->second, stat.getLastSignatureData().sensorData().cameraModels()); } - else + else if(stat.getLastSignatureData().sensorData().stereoCameraModels().size() && stat.getLastSignatureData().sensorData().stereoCameraModels()[0].isValidForProjection()) { - scanLocalTransform = stat.getLastSignatureData().sensorData().laserScanCompressed().localTransform(); + _cloudViewer->updateCameraFrustums(poses.rbegin()->second, stat.getLastSignatureData().sensorData().stereoCameraModels()); + } + else if(!stat.getLastSignatureData().sensorData().laserScanRaw().isEmpty() || + !stat.getLastSignatureData().sensorData().laserScanCompressed().isEmpty()) + { + Transform scanLocalTransform; + if(!stat.getLastSignatureData().sensorData().laserScanRaw().isEmpty()) + { + scanLocalTransform = stat.getLastSignatureData().sensorData().laserScanRaw().localTransform(); + } + else + { + scanLocalTransform = stat.getLastSignatureData().sensorData().laserScanCompressed().localTransform(); + } + //fake frustum + CameraModel model( + 2, + 2, + 2, + 1.5, + scanLocalTransform*CameraModel::opticalRotation(), + 0, + cv::Size(4,3)); + _cloudViewer->updateCameraFrustum(poses.rbegin()->second, model); } - //fake frustum - CameraModel model( - 2, - 2, - 2, - 1.5, - scanLocalTransform*CameraModel::opticalRotation(), - 0, - cv::Size(4,3)); - _cloudViewer->updateCameraFrustum(poses.rbegin()->second, model); } + _cloudViewer->updateCameraTargetPosition(poses.rbegin()->second); } - _cloudViewer->updateCameraTargetPosition(poses.rbegin()->second); - if(_ui->graphicsView_graphView->isVisible()) { _ui->graphicsView_graphView->updateReferentialPosition(poses.rbegin()->second); @@ -2851,6 +2873,7 @@ void MainWindow::updateMapCloud( _progressDialog->appendText(tr("Map update: %1 nodes shown of %2 (cloud filtering is on)").arg(poses.size()).arg(nodePoses.size())); QApplication::processEvents(); } + UDEBUG("Filtered poses"); } else { @@ -2858,27 +2881,30 @@ void MainWindow::updateMapCloud( mapIds = mapIdsIn; } - std::map posesMask; - for(std::map::const_iterator iter = nodePoses.begin(); iter!=nodePoses.end(); ++iter) + if(_ui->widget_mapVisibility->isVisible()) { - posesMask.insert(posesMask.end(), std::make_pair(iter->first, poses.find(iter->first) != poses.end())); + std::map posesMask; + for(std::map::const_iterator iter = nodePoses.begin(); iter!=nodePoses.end(); ++iter) + { + posesMask.insert(posesMask.end(), std::make_pair(iter->first, poses.find(iter->first) != poses.end())); + } + _ui->widget_mapVisibility->setMap(nodePoses, posesMask); + UDEBUG("Updated map visibility with %ld poses", nodePoses.size()); } - _ui->widget_mapVisibility->setMap(nodePoses, posesMask); if(groundTruths.size() && _ui->actionAnchor_clouds_to_ground_truth->isChecked()) { + int anchored = 0; for(std::map::iterator iter = poses.begin(); iter!=poses.end(); ++iter) { std::map::const_iterator gtIter = groundTruths.find(iter->first); if(gtIter!=groundTruths.end()) { iter->second = gtIter->second; - } - else - { - UWARN("Not found ground truth pose for node %d", iter->first); + ++anchored; } } + UDEBUG("Anchored %d/%ld poses to ground truth", anchored, poses.size()); } else if(_currentGTPosesMap.size() == 0) { @@ -3050,7 +3076,9 @@ void MainWindow::updateMapCloud( cv::Mat obstacles; cv::Mat empty; + UTimer decompressionTime; jter->sensorData().uncompressDataConst(0, 0, 0, 0, &ground, &obstacles, &empty); + UDEBUG("Uncompressed local occupancy grid of node %d (%f s)", jter->id(), decompressionTime.ticks()); double resolution = jter->sensorData().gridCellSize(); if(_preferencesDialog->getGridUIResolution() > jter->sensorData().gridCellSize()) @@ -3219,6 +3247,7 @@ void MainWindow::updateMapCloud( if(_preferencesDialog->isGroundTruthAligned() && _currentGTPosesMap.size()) { mapToGt = alignPosesToGroundTruth(_currentPosesMap, _currentGTPosesMap).inverse(); + UDEBUG("Aligned poses to ground truth (%ld poses %ld gt poses)", _currentPosesMap.size(), _currentGTPosesMap.size()); } std::map posesWithOdomCache; @@ -3233,7 +3262,9 @@ void MainWindow::updateMapCloud( } } - if((_preferencesDialog->isGraphsShown() || _preferencesDialog->isFrustumsShown(0)) && _currentPosesMap.size()) + if( _cloudViewer->isVisible() && + (_preferencesDialog->isGraphsShown() || _preferencesDialog->isFrustumsShown(0)) && + _currentPosesMap.size()) { UTimer timerGraph; // Find all graphs @@ -3336,7 +3367,7 @@ void MainWindow::updateMapCloud( } } - UDEBUG("timerGraph=%fs", timerGraph.ticks()); + UDEBUG("timerGraph (CloudViewer)=%fs", timerGraph.ticks()); } UDEBUG("labels.size()=%d", (int)labels.size()); @@ -5021,96 +5052,113 @@ void MainWindow::drawKeypoints(const std::multimap & refWords timer.start(); ULOGGER_DEBUG("refWords.size() = %d", refWords.size()); - if(refWords.size()) + _ui->imageView_source->clearFeatures(); + if(_ui->imageView_source->isFeaturesShown()) { - _ui->imageView_source->clearFeatures(); + for(std::multimap::const_iterator iter = refWords.begin(); iter != refWords.end(); ++iter ) + { + int id = iter->first; + QColor color; + if(id<0) + { + // GRAY = NOT QUANTIZED + color = Qt::gray; + } + else if(uContains(loopWords, id)) + { + // PINK = FOUND IN LOOP SIGNATURE + color = Qt::magenta; + } + else if(_lastIds.contains(id)) + { + // BLUE = FOUND IN LAST SIGNATURE + color = Qt::blue; + } + else if(id<=_lastId) + { + // RED = ALREADY EXISTS + color = Qt::red; + } + else if(refWords.count(id) > 1) + { + // YELLOW = NEW and multiple times + color = Qt::yellow; + } + else + { + // GREEN = NEW + color = Qt::green; + } + _ui->imageView_source->addFeature(iter->first, iter->second, 0, color); + } } - for(std::multimap::const_iterator iter = refWords.begin(); iter != refWords.end(); ++iter ) - { - int id = iter->first; - QColor color; - if(id<0) - { - // GRAY = NOT QUANTIZED - color = Qt::gray; - } - else if(uContains(loopWords, id)) - { - // PINK = FOUND IN LOOP SIGNATURE - color = Qt::magenta; - } - else if(_lastIds.contains(id)) - { - // BLUE = FOUND IN LAST SIGNATURE - color = Qt::blue; - } - else if(id<=_lastId) - { - // RED = ALREADY EXISTS - color = Qt::red; - } - else if(refWords.count(id) > 1) - { - // YELLOW = NEW and multiple times - color = Qt::yellow; - } - else - { - // GREEN = NEW - color = Qt::green; - } - _ui->imageView_source->addFeature(iter->first, iter->second, 0, color); - } - ULOGGER_DEBUG("source time = %f s", timer.ticks()); + ULOGGER_DEBUG("source time (shown=%d) = %f s", _ui->imageView_source->isFeaturesShown()?1:0, timer.ticks()); timer.start(); ULOGGER_DEBUG("loopWords.size() = %d", loopWords.size()); QList > uniqueCorrespondences; - if(loopWords.size()) + _ui->imageView_loopClosure->clearFeatures(); + if(_ui->imageView_loopClosure->isFeaturesShown()) { - _ui->imageView_loopClosure->clearFeatures(); - } - for(std::multimap::const_iterator iter = loopWords.begin(); iter != loopWords.end(); ++iter ) - { - int id = iter->first; - QColor color; - if(id<0) + for(std::multimap::const_iterator iter = loopWords.begin(); iter != loopWords.end(); ++iter ) { - // GRAY = NOT QUANTIZED - color = Qt::gray; - } - else if(uContains(refWords, id)) - { - // PINK = FOUND IN LOOP SIGNATURE - color = Qt::magenta; - //To draw lines... get only unique correspondences - if(uValues(refWords, id).size() == 1 && uValues(loopWords, id).size() == 1) + int id = iter->first; + QColor color; + if(id<0) { - const cv::KeyPoint & a = refWords.find(id)->second; - const cv::KeyPoint & b = iter->second; - uniqueCorrespondences.push_back(QPair(a.pt, b.pt)); + // GRAY = NOT QUANTIZED + color = Qt::gray; + } + else if(uContains(refWords, id)) + { + // PINK = FOUND IN LOOP SIGNATURE + color = Qt::magenta; + //To draw lines... get only unique correspondences + if(uValues(refWords, id).size() == 1 && uValues(loopWords, id).size() == 1) + { + const cv::KeyPoint & a = refWords.find(id)->second; + const cv::KeyPoint & b = iter->second; + uniqueCorrespondences.push_back(QPair(a.pt, b.pt)); + } + } + else if(id<=_lastId) + { + // RED = ALREADY EXISTS + color = Qt::red; + } + else if(refWords.count(id) > 1) + { + // YELLOW = NEW and multiple times + color = Qt::yellow; + } + else + { + // GREEN = NEW + color = Qt::green; + } + _ui->imageView_loopClosure->addFeature(iter->first, iter->second, 0, color); + } + } + else if(_ui->imageView_source->isLinesShown() && _ui->imageView_loopClosure->isLinesShown()) + { + for(std::multimap::const_iterator iter = loopWords.begin(); iter != loopWords.end(); ++iter ) + { + int id = iter->first; + if(id>=0 && uContains(refWords, id)) + { + //To draw lines... get only unique correspondences + if(uValues(refWords, id).size() == 1 && uValues(loopWords, id).size() == 1) + { + const cv::KeyPoint & a = refWords.find(id)->second; + const cv::KeyPoint & b = iter->second; + uniqueCorrespondences.push_back(QPair(a.pt, b.pt)); + } } } - else if(id<=_lastId) - { - // RED = ALREADY EXISTS - color = Qt::red; - } - else if(refWords.count(id) > 1) - { - // YELLOW = NEW and multiple times - color = Qt::yellow; - } - else - { - // GREEN = NEW - color = Qt::green; - } - _ui->imageView_loopClosure->addFeature(iter->first, iter->second, 0, color); } + ULOGGER_DEBUG("loop closure time (shown=%d) = %f s", _ui->imageView_loopClosure->isFeaturesShown()?1:0, timer.ticks()); - ULOGGER_DEBUG("loop closure time = %f s", timer.ticks()); - + _lastIds.clear(); if(refWords.size()>0) { if((*refWords.rbegin()).first > _lastId) @@ -5125,52 +5173,51 @@ void MainWindow::drawKeypoints(const std::multimap & refWords #endif } - // Draw lines between corresponding features... - float scaleSource = _ui->imageView_source->viewScale(); - float scaleLoop = _ui->imageView_loopClosure->viewScale(); - UDEBUG("scale source=%f loop=%f", scaleSource, scaleLoop); - // Delta in actual window pixels - float sourceMarginX = (_ui->imageView_source->width() - _ui->imageView_source->sceneRect().width()*scaleSource)/2.0f; - float sourceMarginY = (_ui->imageView_source->height() - _ui->imageView_source->sceneRect().height()*scaleSource)/2.0f; - float loopMarginX = (_ui->imageView_loopClosure->width() - _ui->imageView_loopClosure->sceneRect().width()*scaleLoop)/2.0f; - float loopMarginY = (_ui->imageView_loopClosure->height() - _ui->imageView_loopClosure->sceneRect().height()*scaleLoop)/2.0f; - - float deltaX = 0; - float deltaY = 0; - - if(_preferencesDialog->isVerticalLayoutUsed()) + _ui->imageView_source->clearLines(); + _ui->imageView_loopClosure->clearLines(); + if(_ui->imageView_source->isLinesShown() && _ui->imageView_loopClosure->isLinesShown()) { - deltaY = _ui->label_matchId->height() + _ui->imageView_source->height(); - } - else - { - deltaX = _ui->imageView_source->width(); - } + // Draw lines between corresponding features... + float scaleSource = _ui->imageView_source->viewScale(); + float scaleLoop = _ui->imageView_loopClosure->viewScale(); + UDEBUG("scale source=%f loop=%f", scaleSource, scaleLoop); + // Delta in actual window pixels + float sourceMarginX = (_ui->imageView_source->width() - _ui->imageView_source->sceneRect().width()*scaleSource)/2.0f; + float sourceMarginY = (_ui->imageView_source->height() - _ui->imageView_source->sceneRect().height()*scaleSource)/2.0f; + float loopMarginX = (_ui->imageView_loopClosure->width() - _ui->imageView_loopClosure->sceneRect().width()*scaleLoop)/2.0f; + float loopMarginY = (_ui->imageView_loopClosure->height() - _ui->imageView_loopClosure->sceneRect().height()*scaleLoop)/2.0f; - if(refWords.size() && loopWords.size()) - { - _ui->imageView_source->clearLines(); - _ui->imageView_loopClosure->clearLines(); - } + float deltaX = 0; + float deltaY = 0; - for(QList >::iterator iter = uniqueCorrespondences.begin(); - iter!=uniqueCorrespondences.end(); - ++iter) - { + if(_preferencesDialog->isVerticalLayoutUsed()) + { + deltaY = _ui->label_matchId->height() + _ui->imageView_source->height(); + } + else + { + deltaX = _ui->imageView_source->width(); + } - _ui->imageView_source->addLine( - iter->first.x, - iter->first.y, - (iter->second.x*scaleLoop+loopMarginX+deltaX-sourceMarginX)/scaleSource, - (iter->second.y*scaleLoop+loopMarginY+deltaY-sourceMarginY)/scaleSource, - _ui->imageView_source->getDefaultMatchingLineColor()); + for(QList >::iterator iter = uniqueCorrespondences.begin(); + iter!=uniqueCorrespondences.end(); + ++iter) + { - _ui->imageView_loopClosure->addLine( - (iter->first.x*scaleSource+sourceMarginX-deltaX-loopMarginX)/scaleLoop, - (iter->first.y*scaleSource+sourceMarginY-deltaY-loopMarginY)/scaleLoop, - iter->second.x, - iter->second.y, - _ui->imageView_loopClosure->getDefaultMatchingLineColor()); + _ui->imageView_source->addLine( + iter->first.x, + iter->first.y, + (iter->second.x*scaleLoop+loopMarginX+deltaX-sourceMarginX)/scaleSource, + (iter->second.y*scaleLoop+loopMarginY+deltaY-sourceMarginY)/scaleSource, + _ui->imageView_source->getDefaultMatchingLineColor()); + + _ui->imageView_loopClosure->addLine( + (iter->first.x*scaleSource+sourceMarginX-deltaX-loopMarginX)/scaleLoop, + (iter->first.y*scaleSource+sourceMarginY-deltaY-loopMarginY)/scaleLoop, + iter->second.x, + iter->second.y, + _ui->imageView_loopClosure->getDefaultMatchingLineColor()); + } } _ui->imageView_source->update(); _ui->imageView_loopClosure->update(); From 0bf216093901823ad167da21fd9d668652007a36 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 23 Nov 2025 11:33:26 -0800 Subject: [PATCH 057/126] Fixed odom cache overlay not visible --- guilib/src/GraphViewer.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/guilib/src/GraphViewer.cpp b/guilib/src/GraphViewer.cpp index 07144a4e..a09fa461 100644 --- a/guilib/src/GraphViewer.cpp +++ b/guilib/src/GraphViewer.cpp @@ -803,10 +803,15 @@ void GraphViewer::updateGraph(const std::map & poses, { (--_nodeItems.end()).value()->setColor(_nodeOdomCacheColor); } + + QRectF rect = this->scene()->sceneRect(); + if(!odomCacheIds.empty()) + _odomCacheOverlay->setRect(rect); + else + _odomCacheOverlay->setRect(0, 0, 0, 0); if(wasEmpty) { - QRectF rect = this->scene()->sceneRect(); this->fitInView(rect.adjusted(-rect.width()/2.0f, -rect.height()/2.0f, rect.width()/2.0f, rect.height()/2.0f), Qt::KeepAspectRatio); } From 1edb6a864710b33895b7f6a16b9077ac67d9f02f Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 23 Nov 2025 12:03:17 -0800 Subject: [PATCH 058/126] log msg typo --- corelib/src/optimizer/OptimizerG2O.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/corelib/src/optimizer/OptimizerG2O.cpp b/corelib/src/optimizer/OptimizerG2O.cpp index 3a6950a4..e8d605ed 100644 --- a/corelib/src/optimizer/OptimizerG2O.cpp +++ b/corelib/src/optimizer/OptimizerG2O.cpp @@ -1172,7 +1172,7 @@ std::map OptimizerG2O::optimize( if(i>0 && optimizer.activeRobustChi2() > 1000000000000.0) { - UERROR("g2o: Large optimimzation error detected (%f), aborting optimization!"); + UERROR("g2o: Large optimization error detected (%f), aborting optimization!"); return optimizedPoses; } @@ -1221,7 +1221,7 @@ std::map OptimizerG2O::optimize( if(optimizer.activeRobustChi2() > 1000000000000.0) { - UERROR("g2o: Large optimimzation error detected (%f), aborting optimization!"); + UERROR("g2o: Large optimization error detected (%f), aborting optimization!"); return optimizedPoses; } From 3093df8e71a6b3dc16b1ffe4b76681101459c1e9 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 23 Nov 2025 13:47:15 -0800 Subject: [PATCH 059/126] Localization: updated how poses with fixed priors are fed to optimization for faster convergence (see https://github.com/introlab/rtabmap_ros/issues/1371) --- corelib/src/Rtabmap.cpp | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/corelib/src/Rtabmap.cpp b/corelib/src/Rtabmap.cpp index 5e0ef366..c4accd94 100644 --- a/corelib/src/Rtabmap.cpp +++ b/corelib/src/Rtabmap.cpp @@ -3260,7 +3260,7 @@ bool Rtabmap::process( } cv::Mat priorInfMat = cv::Mat::eye(6,6, CV_64FC1)*_localizationPriorInf; - std::list addedPriors; + std::map addedPriors; for(std::multimap::iterator iter=constraints.begin(); iter!=constraints.end(); ++iter) { std::map::iterator iterPose = _optimizedPoses.find(iter->second.to()); @@ -3271,15 +3271,10 @@ bool Rtabmap::process( // make the poses in the map fixed constraints.insert(std::make_pair(iterPose->first, Link(iterPose->first, iterPose->first, Link::kPosePrior, iterPose->second, priorInfMat))); UDEBUG("Constraint %d->%d: %s (type=%s, var=%f)", iterPose->first, iterPose->first, iterPose->second.prettyPrint().c_str(), Link::typeName(Link::kPosePrior).c_str(), 1./_localizationPriorInf); - addedPriors.push_back(iterPose->first); + addedPriors.insert(*iterPose); } UDEBUG("Constraint %d->%d: %s (type=%s, var = %f %f)", iter->second.from(), iter->second.to(), iter->second.transform().prettyPrint().c_str(), iter->second.typeName().c_str(), iter->second.transVariance(), iter->second.rotVariance()); } - if(addedPriors.size() == 1) { - // When there is only one map node, remove the prior to use fixed constraint in g2o (https://github.com/introlab/rtabmap_ros/issues/1371) - UDEBUG("Currently localizing on a single map node, removing prior on %d", addedPriors.front()); - constraints.erase(graph::findLink(constraints, addedPriors.front(), addedPriors.front(), false, Link::kPosePrior)); - } std::map posesOut; std::multimap edgeConstraintsOut; @@ -3290,6 +3285,9 @@ bool Rtabmap::process( // If slam2d: get connected graph while keeping original roll,pitch,z values. _graphOptimizer->getConnectedGraph(signature->id(), poses, constraints, posesOut, edgeConstraintsOut); + + // Set back map's poses corresponding to priors so that g2o can converge correctly (https://github.com/introlab/rtabmap_ros/issues/1371) + uInsert(posesOut, addedPriors); if(ULogger::level() == ULogger::kDebug) { for(std::map::iterator iter=posesOut.begin(); iter!=posesOut.end(); ++iter) From 3268707c00ca588d31e5b7b87f1f4cda796c0895 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 23 Nov 2025 14:07:01 -0800 Subject: [PATCH 060/126] g2o: update initial pose estimate when prior is provided (https://github.com/introlab/rtabmap_ros/issues/1371) --- corelib/src/Rtabmap.cpp | 4 ---- corelib/src/optimizer/OptimizerG2O.cpp | 5 ++++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/corelib/src/Rtabmap.cpp b/corelib/src/Rtabmap.cpp index c4accd94..98b5432b 100644 --- a/corelib/src/Rtabmap.cpp +++ b/corelib/src/Rtabmap.cpp @@ -3260,7 +3260,6 @@ bool Rtabmap::process( } cv::Mat priorInfMat = cv::Mat::eye(6,6, CV_64FC1)*_localizationPriorInf; - std::map addedPriors; for(std::multimap::iterator iter=constraints.begin(); iter!=constraints.end(); ++iter) { std::map::iterator iterPose = _optimizedPoses.find(iter->second.to()); @@ -3271,7 +3270,6 @@ bool Rtabmap::process( // make the poses in the map fixed constraints.insert(std::make_pair(iterPose->first, Link(iterPose->first, iterPose->first, Link::kPosePrior, iterPose->second, priorInfMat))); UDEBUG("Constraint %d->%d: %s (type=%s, var=%f)", iterPose->first, iterPose->first, iterPose->second.prettyPrint().c_str(), Link::typeName(Link::kPosePrior).c_str(), 1./_localizationPriorInf); - addedPriors.insert(*iterPose); } UDEBUG("Constraint %d->%d: %s (type=%s, var = %f %f)", iter->second.from(), iter->second.to(), iter->second.transform().prettyPrint().c_str(), iter->second.typeName().c_str(), iter->second.transVariance(), iter->second.rotVariance()); } @@ -3286,8 +3284,6 @@ bool Rtabmap::process( // If slam2d: get connected graph while keeping original roll,pitch,z values. _graphOptimizer->getConnectedGraph(signature->id(), poses, constraints, posesOut, edgeConstraintsOut); - // Set back map's poses corresponding to priors so that g2o can converge correctly (https://github.com/introlab/rtabmap_ros/issues/1371) - uInsert(posesOut, addedPriors); if(ULogger::level() == ULogger::kDebug) { for(std::map::iterator iter=posesOut.begin(); iter!=posesOut.end(); ++iter) diff --git a/corelib/src/optimizer/OptimizerG2O.cpp b/corelib/src/optimizer/OptimizerG2O.cpp index e8d605ed..fa410232 100644 --- a/corelib/src/optimizer/OptimizerG2O.cpp +++ b/corelib/src/optimizer/OptimizerG2O.cpp @@ -593,7 +593,9 @@ std::map OptimizerG2O::optimize( g2o::EdgeSE2Prior * priorEdge = new g2o::EdgeSE2Prior(); g2o::VertexSE2* v1 = (g2o::VertexSE2*)optimizer.vertex(id1); priorEdge->setVertex(0, v1); - priorEdge->setMeasurement(g2o::SE2(iter->second.transform().x(), iter->second.transform().y(), iter->second.transform().theta())); + auto pose = g2o::SE2(iter->second.transform().x(), iter->second.transform().y(), iter->second.transform().theta()); + v1->setEstimate(pose); // This will help g2o to converge faster (https://github.com/introlab/rtabmap_ros/issues/1371) + priorEdge->setMeasurement(pose); priorEdge->setParameterId(0, PARAM_OFFSET); Eigen::Matrix information = Eigen::Matrix::Identity(); if(!isCovarianceIgnored()) @@ -674,6 +676,7 @@ std::map OptimizerG2O::optimize( Eigen::Isometry3d pose; pose = a.linear(); pose.translation() = a.translation(); + v1->setEstimate(pose); // This will help g2o to converge faster (https://github.com/introlab/rtabmap_ros/issues/1371) priorEdge->setMeasurement(pose); priorEdge->setParameterId(0, PARAM_OFFSET); Eigen::Matrix information = Eigen::Matrix::Identity(); From 90d7cc3045c638522fff7d2e13d920a71a71f763 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Fri, 28 Nov 2025 14:41:39 -0800 Subject: [PATCH 061/126] Fixed cropBoxImpl() asserting when voxel filtering large 2D clouds (where min z == max z) --- corelib/src/util3d_filtering.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/corelib/src/util3d_filtering.cpp b/corelib/src/util3d_filtering.cpp index 5fb1cad6..96e107d3 100644 --- a/corelib/src/util3d_filtering.cpp +++ b/corelib/src/util3d_filtering.cpp @@ -954,7 +954,8 @@ pcl::IndicesPtr cropBoxImpl( const Transform & transform, bool negative) { - UASSERT(min[0] < max[0] && min[1] < max[1] && min[2] < max[2]); + UASSERT_MSG(min[0] < max[0] && min[1] < max[1] && min[2] <= max[2], // z can be equal in 2D case + uFormat("x=%f->%f y=%f->%f z=%f->%f", min[0], max[0], min[1], max[1], min[2], max[2]).c_str()); pcl::IndicesPtr output(new std::vector); pcl::CropBox filter; @@ -973,7 +974,8 @@ pcl::IndicesPtr cropBoxImpl( pcl::IndicesPtr cropBox(const pcl::PCLPointCloud2::Ptr & cloud, const pcl::IndicesPtr & indices, const Eigen::Vector4f & min, const Eigen::Vector4f & max, const Transform & transform, bool negative) { - UASSERT(min[0] < max[0] && min[1] < max[1] && min[2] < max[2]); + UASSERT_MSG(min[0] < max[0] && min[1] < max[1] && min[2] <= max[2], // z can be equal in 2D case + uFormat("x=%f->%f y=%f->%f z=%f->%f", min[0], max[0], min[1], max[1], min[2], max[2]).c_str()); pcl::IndicesPtr output(new std::vector); pcl::CropBox filter; From 16c0d2fec5484aa49215bb66cff65b6325e1f1d9 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sat, 29 Nov 2025 17:28:27 -0800 Subject: [PATCH 062/126] Removed gcc 4 constraint on unix (#1624) --- CMakeLists.txt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8ed56bcf..dfcc6228 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -83,14 +83,6 @@ IF(MINGW) SET(CMAKE_SHARED_LINKER_FLAGS "-Wl,--enable-auto-import") ENDIF(MINGW) -# GCC 4 required -IF(UNIX OR MINGW) - EXEC_PROGRAM( gcc ARGS "-dumpversion" OUTPUT_VARIABLE GCC_VERSION ) - IF(GCC_VERSION VERSION_LESS "4.0.0") - MESSAGE(FATAL_ERROR "GCC ${GCC_VERSION} found, but version 4.x.x minimum is required") - ENDIF(GCC_VERSION VERSION_LESS "4.0.0") -ENDIF(UNIX OR MINGW) - #The CDT Error Parser cannot handle error messages that span #more than one line, which is the default gcc behavior. #In order to force gcc to generate single line error messages with no line wrapping From c7da53ac3dfff27f06c8329b59c0c14f2ef5f403 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Mon, 1 Dec 2025 19:42:27 -0800 Subject: [PATCH 063/126] Fixed regression when MapVisibility is not shown, poses are missing when exporting clouds #1625 --- guilib/include/rtabmap/gui/MapVisibilityWidget.h | 1 + guilib/src/MainWindow.cpp | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/guilib/include/rtabmap/gui/MapVisibilityWidget.h b/guilib/include/rtabmap/gui/MapVisibilityWidget.h index e4675a35..55abc90f 100644 --- a/guilib/include/rtabmap/gui/MapVisibilityWidget.h +++ b/guilib/include/rtabmap/gui/MapVisibilityWidget.h @@ -44,6 +44,7 @@ public: void setMap(const std::map & poses, const std::map & mask); std::map getVisiblePoses() const; + bool isEmpty() const {return _poses.empty();} void clear(); diff --git a/guilib/src/MainWindow.cpp b/guilib/src/MainWindow.cpp index 9770c78b..27ec51ac 100644 --- a/guilib/src/MainWindow.cpp +++ b/guilib/src/MainWindow.cpp @@ -755,7 +755,7 @@ void MainWindow::setupMainLayout(bool vertical) std::map MainWindow::currentVisiblePosesMap() const { - return _ui->widget_mapVisibility->getVisiblePoses(); + return !_ui->widget_mapVisibility->isEmpty()?_ui->widget_mapVisibility->getVisiblePoses():_currentPosesMap; } void MainWindow::setCloudViewer(rtabmap::CloudViewer * cloudViewer) @@ -2891,6 +2891,9 @@ void MainWindow::updateMapCloud( _ui->widget_mapVisibility->setMap(nodePoses, posesMask); UDEBUG("Updated map visibility with %ld poses", nodePoses.size()); } + else { + _ui->widget_mapVisibility->clear(); + } if(groundTruths.size() && _ui->actionAnchor_clouds_to_ground_truth->isChecked()) { @@ -8043,7 +8046,7 @@ void MainWindow::exportClouds() return; } - std::map poses = _ui->widget_mapVisibility->getVisiblePoses(); + std::map poses = !_ui->widget_mapVisibility->isEmpty()?_ui->widget_mapVisibility->getVisiblePoses():_currentPosesMap; // Use ground truth poses if current clouds are using them if(_currentGTPosesMap.size() && _ui->actionAnchor_clouds_to_ground_truth->isChecked()) @@ -8080,7 +8083,7 @@ void MainWindow::viewClouds() return; } - std::map poses = _ui->widget_mapVisibility->getVisiblePoses(); + std::map poses = !_ui->widget_mapVisibility->isEmpty()?_ui->widget_mapVisibility->getVisiblePoses():_currentPosesMap; // Use ground truth poses if current clouds are using them if(_currentGTPosesMap.size() && _ui->actionAnchor_clouds_to_ground_truth->isChecked()) @@ -8156,7 +8159,7 @@ void MainWindow::exportImages() QMessageBox::warning(this, tr("Export images..."), tr("Cannot export images, the cache is empty!")); return; } - std::map poses = _ui->widget_mapVisibility->getVisiblePoses(); + std::map poses = !_ui->widget_mapVisibility->isEmpty()?_ui->widget_mapVisibility->getVisiblePoses():_currentPosesMap; if(poses.empty()) { @@ -8373,7 +8376,7 @@ void MainWindow::exportBundlerFormat() return; } - std::map posesIn = _ui->widget_mapVisibility->getVisiblePoses(); + std::map posesIn = !_ui->widget_mapVisibility->isEmpty()?_ui->widget_mapVisibility->getVisiblePoses():_currentPosesMap; // Use ground truth poses if current clouds are using them if(_currentGTPosesMap.size() && _ui->actionAnchor_clouds_to_ground_truth->isChecked()) From 65867c47aea4ed4ebf3c79c99513231e533a9bd0 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Wed, 3 Dec 2025 21:52:47 -0800 Subject: [PATCH 064/126] RegVis: updated error msg on 2D-2D estimation when there are not enough features (https://github.com/introlab/rtabmap_ros/issues/1381) --- corelib/src/RegistrationVis.cpp | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/corelib/src/RegistrationVis.cpp b/corelib/src/RegistrationVis.cpp index 26118830..e49d3142 100644 --- a/corelib/src/RegistrationVis.cpp +++ b/corelib/src/RegistrationVis.cpp @@ -1670,14 +1670,10 @@ Transform RegistrationVis::computeTransformationImpl( UINFO(msg.c_str()); } } - else if(fromSignature.getWords().size() == 0) + else { - msg = uFormat("No enough features (%d)", (int)fromSignature.getWords().size()); - UWARN(msg.c_str()); - } - else - { - msg = uFormat("No camera model"); + msg = uFormat("No enough features < %s=%d (from=%d to=%d)", + Parameters::kVisMinInliers().c_str(), _minInliers, (int)fromSignature.getWords().size(), (int)toSignature.getWords().size()); UWARN(msg.c_str()); } } From 1e7ad82af683f0820b519d8cbe658d01086c46fe Mon Sep 17 00:00:00 2001 From: v4hn Date: Sat, 6 Dec 2025 01:46:08 +0100 Subject: [PATCH 065/126] support gtsam 4.3 header migration as currently in their development branch --- corelib/src/optimizer/gtsam/GravityFactor.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/corelib/src/optimizer/gtsam/GravityFactor.h b/corelib/src/optimizer/gtsam/GravityFactor.h index fb2b3ee3..062a0461 100644 --- a/corelib/src/optimizer/gtsam/GravityFactor.h +++ b/corelib/src/optimizer/gtsam/GravityFactor.h @@ -25,6 +25,9 @@ #pragma once #include +#if GTSAM_VERSION_NUMERIC >= 40300 +#include +#endif #include #include From 106874845f453edb4e12659550605fdfb6407a16 Mon Sep 17 00:00:00 2001 From: Felix Toft Date: Mon, 15 Dec 2025 14:21:32 -0800 Subject: [PATCH 066/126] CuVSLAM 14 Update (#1618) * update cuvslam api call to adhere to new v4.0 changes * update comment * parse cuvslam major version from the header and throw an error if theversion doesn't match the expected * moving average filter + extensive debug logging * lots more logs * improvement by turning off use_motion_model in cuvslam config. lots of debug logs. * slight improvements to velocity guard logic. lots of debug logs * removing debug logs. working. * let find_package_handle_standard_args handle the version comparison logic. Custom logic to handle newer then requested major version * proper warning message if the major version doesn't match * add back debug logs. Handle no guess transform case. * adding warnings when no guess transform is provided * clean up comments * lowering average window to 5 * moderate odometry mode, 5 value averaging window * adding comment * add reset logs to investigate reset issue * try returning guess or identity on init to avoid reset cascade * disable motion model again * big log when tracking * remove some debug logs * flag to use original covariance from cuVSLAM * use raw covariance * data collection for revised lost detection * compare guess to estimated transform for lost detection * re-enable motion model, it appears to reduce false-positives for the new lost detection * run pose estimation afer init on first frame * don't initialize if we don't have enough features * dont warm up GPU everytime we reset * low estimate velocity guard. Passes all challenges. Debug comments still everywhere. * Remove debug logs. Add thresholds to header in a config section. * Cleanup logs * exposing multi-cam mode as a rtabmap param * remove max frame delta * change to use assertion instead of conditional * replaced some UERROR by UWARN --------- Co-authored-by: Felix Toft Co-authored-by: matlabbe --- CMakeLists.txt | 2 +- cmake_modules/FindCuVSLAM.cmake | 48 ++- corelib/include/rtabmap/core/Parameters.h | 3 + .../rtabmap/core/odometry/OdometryCuVSLAM.h | 15 + corelib/src/Parameters.cpp | 1 + corelib/src/odometry/OdometryCuVSLAM.cpp | 334 +++++++++++------- 6 files changed, 262 insertions(+), 141 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index dfcc6228..418cad37 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -854,7 +854,7 @@ IF(WITH_ORB_SLAM AND NOT G2O_FOUND) ENDIF(WITH_ORB_SLAM AND NOT G2O_FOUND) IF(WITH_CUVSLAM) - FIND_PACKAGE(CuVSLAM) + FIND_PACKAGE(CuVSLAM 14.0.0) IF(CUVSLAM_FOUND) MESSAGE(STATUS "Found cuVSLAM: ${CUVSLAM_INCLUDE_DIRS}") ENDIF() diff --git a/cmake_modules/FindCuVSLAM.cmake b/cmake_modules/FindCuVSLAM.cmake index 8a17530b..ec204b24 100644 --- a/cmake_modules/FindCuVSLAM.cmake +++ b/cmake_modules/FindCuVSLAM.cmake @@ -4,6 +4,7 @@ # # It sets the following variables: # CUVSLAM_FOUND - Set to false, or undefined, if cuVSLAM isn't found. +# CUVSLAM_VERSION - The version of cuVSLAM found (e.g., "14.0.0"). # CUVSLAM_INCLUDE_DIRS - The cuVSLAM include directory. # CUVSLAM_LIBRARIES - The cuVSLAM library to link against. @@ -33,7 +34,18 @@ find_library(CUVSLAM_LIBRARY ) if(CUVSLAM_INCLUDE_DIRS AND CUVSLAM_LIBRARY) - set(CUVSLAM_FOUND TRUE) + # Extract version from cuvslam.h header + file(STRINGS "${CUVSLAM_INCLUDE_DIRS}/cuvslam.h" CUVSLAM_VERSION_MAJOR_LINE + REGEX "^#define CUVSLAM_API_VERSION_MAJOR") + file(STRINGS "${CUVSLAM_INCLUDE_DIRS}/cuvslam.h" CUVSLAM_VERSION_MINOR_LINE + REGEX "^#define CUVSLAM_API_VERSION_MINOR") + + if(CUVSLAM_VERSION_MAJOR_LINE AND CUVSLAM_VERSION_MINOR_LINE) + string(REGEX MATCH "[0-9]+" CUVSLAM_VERSION_MAJOR "${CUVSLAM_VERSION_MAJOR_LINE}") + string(REGEX MATCH "[0-9]+" CUVSLAM_VERSION_MINOR "${CUVSLAM_VERSION_MINOR_LINE}") + set(CUVSLAM_VERSION "${CUVSLAM_VERSION_MAJOR}.${CUVSLAM_VERSION_MINOR}.0") + endif() + set(CUVSLAM_LIBRARIES ${CUVSLAM_LIBRARY} ${CUDA_LIBRARIES} @@ -46,11 +58,35 @@ if(CUVSLAM_INCLUDE_DIRS AND CUVSLAM_LIBRARY) ) endif() +# Version compatibility check - cuVSLAM only guarantees API compatibility within the same major version +set(CUVSLAM_VERSION_MISMATCH_REASON "") +if(CuVSLAM_FIND_VERSION AND CUVSLAM_VERSION) + string(REGEX MATCH "^[0-9]+" REQUESTED_MAJOR_VERSION "${CuVSLAM_FIND_VERSION}") + if(NOT CUVSLAM_VERSION_MAJOR EQUAL REQUESTED_MAJOR_VERSION) + set(CUVSLAM_VERSION_MISMATCH_REASON "Major version mismatch: found ${CUVSLAM_VERSION_MAJOR}.x but requested ${REQUESTED_MAJOR_VERSION}.x.\ncuVSLAM only guarantees API compatibility within the same major version.\nPlease install cuVSLAM ${REQUESTED_MAJOR_VERSION}.x or update CMakeLists.txt to request version ${CUVSLAM_VERSION_MAJOR}.0.0") + + if(CuVSLAM_FIND_REQUIRED) + message(FATAL_ERROR + "cuVSLAM major version mismatch: found version ${CUVSLAM_VERSION} but version ${CuVSLAM_FIND_VERSION} is required.\n" + "cuVSLAM only guarantees API compatibility within the same major version.\n" + "Found major version ${CUVSLAM_VERSION_MAJOR} is not compatible with requested major version ${REQUESTED_MAJOR_VERSION}.\n" + "Please install cuVSLAM ${REQUESTED_MAJOR_VERSION}.x or update CMakeLists.txt to request version ${CUVSLAM_VERSION_MAJOR}.x." + ) + else() + # Clear the found variables to indicate incompatibility + unset(CUVSLAM_LIBRARIES) + unset(CUVSLAM_INCLUDE_DIRS) + endif() + endif() +endif() + # Handle the QUIET and REQUIRED arguments include(FindPackageHandleStandardArgs) find_package_handle_standard_args(CuVSLAM FOUND_VAR CUVSLAM_FOUND REQUIRED_VARS CUVSLAM_LIBRARIES CUVSLAM_INCLUDE_DIRS + VERSION_VAR CUVSLAM_VERSION + REASON_FAILURE_MESSAGE "${CUVSLAM_VERSION_MISMATCH_REASON}" HANDLE_COMPONENTS ) @@ -64,16 +100,6 @@ if(CUVSLAM_FOUND) INTERFACE_LINK_LIBRARIES "${CUVSLAM_LIBRARIES};Eigen3::Eigen" ) endif() - - # Show which cuVSLAM was found only if not quiet - if(NOT CUVSLAM_FIND_QUIETLY) - message(STATUS "Found cuVSLAM: ${CUVSLAM_LIBRARIES}") - endif() -else() - # Fatal error if cuVSLAM is required but not found - if(CUVSLAM_FIND_REQUIRED) - message(FATAL_ERROR "Could not find cuVSLAM library") - endif() endif() mark_as_advanced(CUVSLAM_INCLUDE_DIRS CUVSLAM_LIBRARY) diff --git a/corelib/include/rtabmap/core/Parameters.h b/corelib/include/rtabmap/core/Parameters.h index a63d4b62..729d61eb 100644 --- a/corelib/include/rtabmap/core/Parameters.h +++ b/corelib/include/rtabmap/core/Parameters.h @@ -682,6 +682,9 @@ class RTABMAP_CORE_EXPORT Parameters RTABMAP_PARAM(OdomOpen3D, MaxDepth, float, 3.0, "Maximum depth."); RTABMAP_PARAM(OdomOpen3D, Method, int, 0, "Registration method: 0=PointToPlane, 1=Intensity, 2=Hybrid."); + // Odometry cuVSLAM + RTABMAP_PARAM(OdomCuVSLAM, MulticamMode, int, 0, "cuVSLAM multicam_mode setting: 0=moderate, 1=performance, 2=precision."); + // Common registration parameters RTABMAP_PARAM(Reg, RepeatOnce, bool, true, "Do a second registration with the output of the first registration as guess. Only done if no guess was provided for the first registration (like on loop closure). It can be useful if the registration approach used can use a guess to get better matches."); RTABMAP_PARAM(Reg, Strategy, int, 0, "0=Vis, 1=Icp, 2=VisIcp"); diff --git a/corelib/include/rtabmap/core/odometry/OdometryCuVSLAM.h b/corelib/include/rtabmap/core/odometry/OdometryCuVSLAM.h index 92b76849..bfe552a3 100644 --- a/corelib/include/rtabmap/core/odometry/OdometryCuVSLAM.h +++ b/corelib/include/rtabmap/core/odometry/OdometryCuVSLAM.h @@ -31,6 +31,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include +#include +#include #ifdef RTABMAP_CUVSLAM #include @@ -51,6 +53,7 @@ public: private: virtual Transform computeTransform(SensorData & image, const Transform & guess = Transform(), OdometryInfo * info = 0); + virtual void cleanupCuVSLAMResources(); private: #ifdef RTABMAP_CUVSLAM @@ -65,9 +68,21 @@ private: bool lost_; bool tracking_; bool planar_constraints_; + int multicam_mode_; Transform previous_pose_; double last_timestamp_; + // Configuration Thresholds + double velocity_ratio_threshold_high_ = 1.5; // The maximum velocity ratio of guess / estimated velocity needed to detect lost state. + double velocity_ratio_threshold_low_ = 0.5; // The minimum velocity ratio of guess / estimated velocity needed to detect lost state. + double velocity_difference_threshold_ = 0.1; // The maximum velocity difference between the guess and the estimated velocity needed to detect lost state. + double zero_estimated_velocity_threshold_ = 0.00001; // The minimum cuVSLAM estimated velocity needed to detect lost state. + double min_landmarks_threshold_ = 30; // The minimum number of landmarks needed to start tracking after an initialization. + + // Forward cuVLSAM covariance directly to RTAB-Map. + // When true this disables covariance based lost detection. + bool use_raw_covariance_ = false; + //visualization std::vector observations_; std::vector landmarks_; diff --git a/corelib/src/Parameters.cpp b/corelib/src/Parameters.cpp index 4eb501e6..463478a8 100644 --- a/corelib/src/Parameters.cpp +++ b/corelib/src/Parameters.cpp @@ -183,6 +183,7 @@ rtabmap::ParametersMap Parameters::getDefaultOdometryParameters(bool stereo, boo (stereo && group.compare("Stereo") == 0) || (icp && group.compare("Icp") == 0) || (vis && Parameters::isFeatureParameter(iter->first)) || + group.compare("OdomCuVSLAM") == 0 || group.compare("Reg") == 0 || group.compare("Optimizer") == 0 || group.compare("g2o") == 0 || diff --git a/corelib/src/odometry/OdometryCuVSLAM.cpp b/corelib/src/odometry/OdometryCuVSLAM.cpp index c5ba56da..14b9384f 100644 --- a/corelib/src/odometry/OdometryCuVSLAM.cpp +++ b/corelib/src/odometry/OdometryCuVSLAM.cpp @@ -30,6 +30,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/core/OdometryInfo.h" #include "rtabmap/utilite/ULogger.h" #include "rtabmap/utilite/UTimer.h" +#include #ifdef RTABMAP_CUVSLAM #include "rtabmap/core/CameraModel.h" @@ -95,6 +96,7 @@ bool initializeCuVSLAM(const SensorData & data, CUVSLAM_TrackerHandle & cuvslam_handle, CUVSLAM_GroundConstraintHandle & ground_constraint_handle, bool planar_constraints, + int multicam_mode, std::vector & gpu_left_image_data, std::vector & gpu_right_image_data, std::vector & gpu_left_image_sizes, @@ -103,7 +105,7 @@ bool initializeCuVSLAM(const SensorData & data, std::vector> & intrinsics, cudaStream_t & cuda_stream); -CUVSLAM_Configuration CreateConfiguration(const SensorData & data); +CUVSLAM_Configuration CreateConfiguration(const SensorData & data, int multicam_mode); bool prepareImages(const SensorData & data, std::vector & cuvslam_images, @@ -113,10 +115,7 @@ bool prepareImages(const SensorData & data, std::vector & gpu_right_image_sizes, cudaStream_t & cuda_stream); -cv::Mat convertCuVSLAMCovariance(const float * cuvslam_covariance); - -void printCovarianceMatrix(const cv::Mat & cov, const std::string & label); -void printRawCuvslamCovariance(const float * cuvslam_covariance, const std::string & label); +cv::Mat convertCuVSLAMCovariance(const float * cuvslam_covariance, bool use_raw_covariance); // ============================================================================ @@ -161,24 +160,6 @@ Transform FromcuVSLAMPose(const CUVSLAM_Pose & cuvslam_pose) return rtabmap_transform; } -void PrintConfiguration(const CUVSLAM_Configuration & cfg) -{ - UINFO("Use use_gpu: %s", cfg.use_gpu ? "true" : "false"); - UINFO("Enable IMU Fusion: %s", cfg.enable_imu_fusion ? "true" : "false"); - if (cfg.enable_imu_fusion) { - UINFO("gyroscope_noise_density: %f", - cfg.imu_calibration.gyroscope_noise_density); - UINFO("gyroscope_random_walk: %f", - cfg.imu_calibration.gyroscope_random_walk); - UINFO("accelerometer_noise_density: %f", - cfg.imu_calibration.accelerometer_noise_density); - UINFO("accelerometer_random_walk: %f", - cfg.imu_calibration.accelerometer_random_walk); - UINFO("frequency: %f", - cfg.imu_calibration.frequency); - } -} - } // namespace rtabmap #endif @@ -199,6 +180,7 @@ OdometryCuVSLAM::OdometryCuVSLAM(const ParametersMap & parameters) : lost_(false), tracking_(false), planar_constraints_(false), + multicam_mode_(0), previous_pose_(Transform::getIdentity()), last_timestamp_(-1.0), observations_(5000), @@ -212,6 +194,12 @@ OdometryCuVSLAM::OdometryCuVSLAM(const ParametersMap & parameters) : { #ifdef RTABMAP_CUVSLAM Parameters::parse(parameters, Parameters::kRegForce3DoF(), planar_constraints_); + Parameters::parse(parameters, Parameters::kOdomCuVSLAMMulticamMode(), multicam_mode_); + UASSERT(multicam_mode_ >= 0 && multicam_mode_ <= 2); + UINFO("%s=%d", Parameters::kOdomCuVSLAMMulticamMode().c_str(), multicam_mode_); + // Warm up GPU and create CUDA context before tracker initialization + // Supposedly this will speed up the tracker initialization + CUVSLAM_WarmUpGPU(); #endif } @@ -248,7 +236,14 @@ OdometryCuVSLAM::~OdometryCuVSLAM() void OdometryCuVSLAM::reset(const Transform & initialPose) { Odometry::reset(initialPose); - + +#ifdef RTABMAP_CUVSLAM + this->cleanupCuVSLAMResources(); +#endif +} + +void OdometryCuVSLAM::cleanupCuVSLAMResources() +{ #ifdef RTABMAP_CUVSLAM // Clean up cuVSLAM handles if(cuvslam_handle_) @@ -300,9 +295,19 @@ Transform OdometryCuVSLAM::computeTransform( #ifdef RTABMAP_CUVSLAM UTimer timer; + UDEBUG("=== computeTransform ENTRY === lost_=%s, tracking_=%s, initialized_=%s", + lost_ ? "true" : "false", + tracking_ ? "true" : "false", + initialized_ ? "true" : "false"); + // If we are lost after tracking has begun, return null transform // We wait until a reset is triggered. if(lost_ && tracking_) { + UDEBUG("EARLY EXIT: lost_ && tracking_ is true, returning null"); + if(info) { + info->reg.covariance = cv::Mat::eye(6, 6, CV_64FC1) * 9999.0; + info->timeEstimation = timer.ticks(); + } return Transform(); } @@ -330,6 +335,7 @@ Transform OdometryCuVSLAM::computeTransform( cuvslam_handle_, ground_constraint_handle_, planar_constraints_, + multicam_mode_, gpu_left_image_data_, gpu_right_image_data_, gpu_left_image_sizes_, @@ -341,18 +347,8 @@ Transform OdometryCuVSLAM::computeTransform( UERROR("Failed to initialize cuVSLAM tracker"); return Transform(); } - - initialized_ = true; - if(info) - { - info->type = 0; - info->reg.covariance = cv::Mat::eye(6, 6, CV_64FC1) * 9999.0; - info->timeEstimation = timer.ticks(); - } - last_timestamp_ = data.stamp(); - return Transform(); } - + // Prepare images for cuVSLAM std::vector cuvslam_image_objects; if(!prepareImages( @@ -368,10 +364,9 @@ Transform OdometryCuVSLAM::computeTransform( return Transform(); } - // Process IMU data if available + // Not using the IMU yet if(!data.imu().empty()) { - // TODO: Implement IMU processing UWARN("IMU data available but processing not implemented yet"); } @@ -394,13 +389,14 @@ Transform OdometryCuVSLAM::computeTransform( predicted_pose = TocuVSLAMPose(absolute_guess); predicted_pose_ptr = &predicted_pose; } - + CUVSLAM_PoseEstimate vo_pose_estimate; const CUVSLAM_Status vo_status = CUVSLAM_TrackGpuMem( cuvslam_handle_, cuvslam_image_objects.data(), cuvslam_image_objects.size(), - predicted_pose_ptr, // can safely handle nullptr if no guess is provided + nullptr, // depth_image (not used in this mode) + predicted_pose_ptr, // can safely handle nullptr if no guess is provided &vo_pose_estimate ); @@ -409,23 +405,37 @@ Transform OdometryCuVSLAM::computeTransform( // Provide specific error message const char * error_msg = "Unknown error"; switch(vo_status) { - case 1: error_msg = "CUVSLAM_TRACKING_LOST"; break; - case 2: error_msg = "CUVSLAM_INVALID_PARAMETER"; break; - case 3: error_msg = "CUVSLAM_INVALID_IMAGE_FORMAT or CUVSLAM_INVALID_CAMERA_CONFIG"; break; - case 4: error_msg = "CUVSLAM_GPU_MEMORY_ERROR"; break; - case 5: error_msg = "CUVSLAM_INITIALIZATION_ERROR"; break; - default: error_msg = "Unknown cuVSLAM error"; break; + case CUVSLAM_TRACKING_LOST: error_msg = "CUVSLAM_TRACKING_LOST"; break; + case CUVSLAM_INVALID_ARG: error_msg = "CUVSLAM_INVALID_PARAMETER"; break; + case CUVSLAM_CAN_NOT_LOCALIZE: error_msg = "CUVSLAM_CAN_NOT_LOCALIZE"; break; + case CUVSLAM_GENERIC_ERROR: error_msg = "CUVSLAM_GENERIC_ERROR"; break; + case CUVSLAM_UNSUPPORTED_NUMBER_OF_CAMERAS: error_msg = "CUVSLAM_UNSUPPORTED_NUMBER_OF_CAMERAS"; break; + case CUVSLAM_SLAM_IS_NOT_INITIALIZED: error_msg = "CUVSLAM_SLAM_IS_NOT_INITIALIZED"; break; + default: error_msg = "Unknown cuVSLAM error"; break; } - - UERROR("cuVSLAM tracking error: %d (%s)", vo_status, error_msg); - + + // Update timing information even on failure + last_timestamp_ = data.stamp(); + if(info) { + // Report very high uncertainty to upstream consumers info->reg.covariance = cv::Mat::eye(6, 6, CV_64FC1) * 9999.0; info->timeEstimation = timer.ticks(); } - - last_timestamp_ = data.stamp(); + + // The cuVSLAM tracking status never reports lost in my testing. + // Thus we use covariance to detect lost state. + if(vo_status == CUVSLAM_TRACKING_LOST) + { + UWARN("LOST: cuVSLAM reported CUVSLAM_TRACKING_LOST"); + lost_ = true; + } + else + { + UWARN("cuVSLAM tracking error: %d (%s)", vo_status, error_msg); + } + return Transform(); } @@ -434,51 +444,41 @@ Transform OdometryCuVSLAM::computeTransform( for(int i = 0; i < 6; i++) { float & diag_val = vo_pose_estimate.covariance[i*6+i]; - // We allow 1.0 as a valid value, since cuVSLAM sends identity covariance for the first few frames. - if(!std::isfinite(diag_val) || diag_val <= 0.0 || (diag_val > 0.1 && diag_val != 1.0)) + + // conditions for immediate failure and tracking loss + if(!std::isfinite(diag_val) || diag_val < 0.0) { - diag_val = 9999.0; // Set to high uncertainty + diag_val = 9999.0; valid_covariance = false; } + // Tracker returns identity covariance and 0.0 values after initialization before motion. + if(std::abs(diag_val) < 1e-7f) + { + diag_val = 0.0001; + } + if(diag_val > 0.1) { + valid_covariance = false; + + // If we don't have a guess, we can't use velocity difference to detect lost state. + // Thus at this point, we are lost. Warn the user that cuVSLAM probably needs a guess to work well. + if(guess.isNull()) { + UWARN("No guess provided, but covariance is invalid: %.8f", diag_val); + UWARN("We cannot use velocity difference to detect lost state without a guess!"); + UWARN("Without a guess cuVSLAM is prone to getting lost easily!"); + UWARN("It is highly recommended to provide a guess to cuVSLAM!"); + lost_ = true; + if(info) { + info->reg.covariance = cv::Mat::eye(6, 6, CV_64FC1) * 9999.0; + info->timeEstimation = timer.ticks(); + } + return Transform(); + } + } } // Convert to RTABMAP covariance format and scale to meet RTABMAP expectations - cv::Mat covMat = convertCuVSLAMCovariance(vo_pose_estimate.covariance); - - // Handle invalid covariance. Protect against low velocity cases. - if(!valid_covariance) { - double velocity_ms = 9999.0; - double angular_velocity_rad_s = 9999.0; - if(!guess.isNull()) { - double time_s = data.stamp() - last_timestamp_; - velocity_ms = guess.getNorm() / time_s; - angular_velocity_rad_s = guess.getAngle(Transform::getIdentity()) / time_s; - } - if(velocity_ms < 0.1 && angular_velocity_rad_s < 0.1 && last_timestamp_ != -1.0) { - covMat = cv::Mat::eye(6, 6, CV_64FC1) * 0.0001; - } else { - // If we have already begun tracking, now we are lost. - if(tracking_) { - UWARN("LOST: Velocity is high and covariance is invalid, setting lost to true"); - lost_ = true; - } - // Still send covariance for debugging - if(info) { - info->reg.covariance = covMat; - } - return Transform(); - } - } + cv::Mat covMat = convertCuVSLAMCovariance(vo_pose_estimate.covariance, use_raw_covariance_); - // Tracking was successful and the covariance is valid, set tracking to true - tracking_ = true; - - if(info) - { - info->reg.covariance = covMat; - info->timeEstimation = timer.ticks(); - } - // Apply ground constraint if(planar_constraints_) { if(CUVSLAM_GroundConstraintAddNextPose(ground_constraint_handle_, &vo_pose_estimate.pose) != CUVSLAM_SUCCESS) { @@ -491,14 +491,62 @@ Transform OdometryCuVSLAM::computeTransform( } } - // Convert cuVSLAM pose to RTAB-Map Transform + // Convert cuVSLAM absolute pose to incremental RTAB-Map Transform Transform current_pose = FromcuVSLAMPose(vo_pose_estimate.pose); current_pose = canonical_pose_cuvslam * current_pose * cuvslam_pose_canonical; - - // Calculate incremental transform UASSERT(!previous_pose_.isNull()); Transform transform = previous_pose_.inverse() * current_pose; + // Compute guess and estimated velocity and report lost if velocity ratio is high and covariance is invalid + double time_delta_s = 0.0; + double guess_velocity_ms = 0.0; + double estimated_velocity_ms = 0.0; + + if(!guess.isNull() && last_timestamp_ > 0.0 && !use_raw_covariance_ && !valid_covariance) { + time_delta_s = data.stamp() - last_timestamp_; + + guess_velocity_ms = guess.getNorm() / time_delta_s; + estimated_velocity_ms = transform.getNorm() / time_delta_s; + double velocity_ratio = estimated_velocity_ms / guess_velocity_ms; + double velocity_difference = std::abs(estimated_velocity_ms - guess_velocity_ms); + + // Check if the expected and predicted velocities are divergent. + // Also ensure estimated velocity is not zero. + // In rapid deceleration cases, estimated velocity zeros out faster then the guess but we aren't lost yet. So we need to check for this. + bool zero_estimated_velocity = estimated_velocity_ms < zero_estimated_velocity_threshold_; + bool invalid_velocity_ratio = velocity_ratio > velocity_ratio_threshold_high_ || velocity_ratio < velocity_ratio_threshold_low_; + bool invalid_velocity_difference = velocity_difference > velocity_difference_threshold_; + + if(invalid_velocity_ratio && invalid_velocity_difference && !zero_estimated_velocity) { + UWARN("Velocity ratio is high and covariance is invalid: %.4f, returning null transform", velocity_ratio); + lost_ = true; + if(info) { + info->reg.covariance = cv::Mat::eye(6, 6, CV_64FC1) * 9999.0; + info->timeEstimation = timer.ticks(); + } + return Transform(); + } else { + covMat = cv::Mat::eye(6, 6, CV_64FC1) * 0.0001; + } + } + + // At this point we have passed the covariance lost checks, so we are tracking. + tracking_ = true; + + if(info) + { + info->reg.covariance = covMat; + info->timeEstimation = timer.ticks(); + } + + // extract 3D VO landmarks for visualization + // This will be used to determine if we have enough features to start tracking. + CUVSLAM_LandmarkVector landmark_vector; + landmark_vector.max = landmarks_.size(); + landmark_vector.landmarks = landmarks_.data(); + CUVSLAM_Status landmark_status = CUVSLAM_GetLastLandmarks(cuvslam_handle_, &landmark_vector); + int landmarks_num = landmark_vector.num; + // Fill info with visualization data if(info) { if(data.stereoCameraModels().size()==1) { @@ -522,11 +570,6 @@ Transform OdometryCuVSLAM::computeTransform( info->type = kTypeF2M; } - // extract 3D VO landmarks for visualization - CUVSLAM_LandmarkVector landmark_vector; - landmark_vector.max = landmarks_.size(); - landmark_vector.landmarks = landmarks_.data(); - CUVSLAM_Status landmark_status = CUVSLAM_GetLastLandmarks(cuvslam_handle_, &landmark_vector); std::vector local_transform_inv(data.stereoCameraModels().size()); for(size_t i=0; ireg.inliersIDs.push_back(landmark.id); break; } + + // Update landmarks number based on which landmarks were successfully reprojected in the current frame + landmarks_num = info->words.size(); } } } } } + + // If we are in a multi-camera setup and successfully reprojected landmarks into camera frames, + // use the number of successfully reprojected landmarks instead of the raw cuVSLAM landmark count. + if(data.stereoCameraModels().size() > 1) { + landmarks_num = (int)info->words.size(); + } + } + + // Check if we have enough features to start tracking. Otherwise we are lost. + if(landmarks_num < min_landmarks_threshold_ && !initialized_) { + if(info) { + info->reg.covariance = cv::Mat::eye(6, 6, CV_64FC1) * 9999.0; + info->timeEstimation = timer.ticks(); + } + // Free GPU resources and reset state. Prevent memory leaks on init loops. + cleanupCuVSLAMResources(); + lost_ = true; + tracking_ = false; + initialized_ = false; + return Transform(); + } else { + initialized_ = true; } previous_pose_ = current_pose; @@ -566,7 +634,7 @@ Transform OdometryCuVSLAM::computeTransform( UERROR("cuVSLAM support not compiled in RTAB-Map");\ return Transform(); #endif - + } #ifdef RTABMAP_CUVSLAM @@ -579,6 +647,7 @@ bool initializeCuVSLAM(const SensorData & data, CUVSLAM_TrackerHandle & cuvslam_handle, CUVSLAM_GroundConstraintHandle & ground_constraint_handle, bool planar_constraints, + int multicam_mode, std::vector & gpu_left_image_data, std::vector & gpu_right_image_data, std::vector & gpu_left_image_sizes, @@ -627,9 +696,9 @@ bool initializeCuVSLAM(const SensorData & data, rtabmap::Transform extrinsics = cuvslam_pose_canonical * stereoModel.localTransform() * optical_pose_cuvslam; cam_left.pose = TocuVSLAMPose(extrinsics); cam_left.border_top = 0; - cam_left.border_bottom = leftModel.imageHeight(); + cam_left.border_bottom = 0; cam_left.border_left = 0; - cam_left.border_right = leftModel.imageWidth(); + cam_left.border_right = 0; // Right camera cam_right.parameters = intrinsics_right.data(); @@ -649,9 +718,9 @@ bool initializeCuVSLAM(const SensorData & data, extrinsics = cuvslam_pose_canonical * stereoModel.localTransform() * baseline_transform * optical_pose_cuvslam; cam_right.pose = TocuVSLAMPose(extrinsics); cam_right.border_top = 0; - cam_right.border_bottom = rightModel.imageHeight(); + cam_right.border_bottom = 0; cam_right.border_left = 0; - cam_right.border_right = rightModel.imageWidth(); + cam_right.border_right = 0; } // Set up camera rig @@ -659,12 +728,12 @@ bool initializeCuVSLAM(const SensorData & data, camera_rig.cameras = cuvslam_cameras.data(); camera_rig.num_cameras = cuvslam_cameras.size(); - const CUVSLAM_Configuration configuration = CreateConfiguration(data); - PrintConfiguration(configuration); + const CUVSLAM_Configuration configuration = CreateConfiguration(data, multicam_mode); // Create tracker CUVSLAM_TrackerHandle tracker_handle; UTimer create_timer; create_timer.start(); + const CUVSLAM_Status status_tracker = CUVSLAM_CreateTracker(&tracker_handle, &camera_rig, &configuration); if (status_tracker != CUVSLAM_SUCCESS) { @@ -706,12 +775,11 @@ Implementation based on Isaac ROS VisualSlamNode::VisualSlamImpl::CreateConfigur Source: isaac_ros_visual_slam/isaac_ros_visual_slam/src/impl/visual_slam_impl.cpp:379-422 https://github.com/NVIDIA-ISAAC-ROS/isaac_ros_visual_slam/blob/19be8c781a55dee9cfbe9f097adca3986638feb1/isaac_ros_visual_slam/src/impl/visual_slam_impl.cpp#L379-L422 */ -CUVSLAM_Configuration CreateConfiguration(const SensorData & data) +CUVSLAM_Configuration CreateConfiguration(const SensorData & data, int multicam_mode) { CUVSLAM_Configuration configuration; CUVSLAM_InitDefaultConfiguration(&configuration); - configuration.multicam_mode = data.stereoCameraModels().size()>1?1:0; // Core Visual Odometry Settings configuration.use_motion_model = 1; // Enable motion model for better tracking @@ -726,19 +794,12 @@ CUVSLAM_Configuration CreateConfiguration(const SensorData & data) configuration.enable_landmarks_export = 0; // SLAM feature (optional) configuration.enable_reading_slam_internals = 0; // SLAM feature (optional) - // IMU Configuration (If we later implement IMU support) - configuration.enable_imu_fusion = 0; //data.imu().empty()?0:1; - configuration.debug_imu_mode = 0; // Disable IMU debug mode - // imu_calibration.gyroscope_noise_density = 0.0002f; - // imu_calibration.gyroscope_random_walk = 0.00003f; - // imu_calibration.accelerometer_noise_density = 0.01f; - // imu_calibration.accelerometer_random_walk = 0.001f; - // imu_calibration.frequency = 200.0f; - // configuration.imu_calibration = imu_calibration; - - // configuration.max_frame_delta_ms = 100.0; // Maximum frame interval (100ms default) - - // SLAM-specific parameters (disabled) + // Odometry configuration (Vision-only, no IMU) + configuration.odometry_mode = CUVSLAM_OdometryMode::Multicamera; + configuration.multicam_mode = multicam_mode; // moderate (0), performance (1) or precision (2). + configuration.debug_imu_mode = 0; + + // SLAM parameters (disabled) configuration.planar_constraints = 0; configuration.slam_throttling_time_ms = 0; configuration.slam_max_map_size = 0; @@ -907,6 +968,11 @@ bool prepareImages(const SensorData & data, left_cuvslam_image.camera_index = camera_index; left_cuvslam_image.pitch = left_image_slice.step; left_cuvslam_image.image_encoding = left_encoding; + // Mask fields (not used in this implementation) + left_cuvslam_image.input_mask = nullptr; + left_cuvslam_image.mask_width = 0; + left_cuvslam_image.mask_height = 0; + left_cuvslam_image.mask_pitch = 0; cuvslam_images.push_back(left_cuvslam_image); @@ -931,6 +997,11 @@ bool prepareImages(const SensorData & data, right_cuvslam_image.camera_index = camera_index; right_cuvslam_image.pitch = right_image_slice.step; right_cuvslam_image.image_encoding = right_encoding; + // Mask fields (not used in this implementation) + right_cuvslam_image.input_mask = nullptr; + right_cuvslam_image.mask_width = 0; + right_cuvslam_image.mask_height = 0; + right_cuvslam_image.mask_pitch = 0; cuvslam_images.push_back(right_cuvslam_image); @@ -953,10 +1024,11 @@ Convert cuVSLAM covariance to RTAB-Map format. Based on Isaac ROS implementation: FromcuVSLAMCovariance() Source: isaac_ros_visual_slam/src/impl/cuvslam_ros_conversion.cpp:275-299 */ -cv::Mat convertCuVSLAMCovariance(const float * cuvslam_covariance) +cv::Mat convertCuVSLAMCovariance(const float * cuvslam_covariance, bool use_raw_covariance) { + // Scale cuvslam covariance to make it more realistic - const double scaling_factor = 10.0; + const double scaling_factor = use_raw_covariance ? 1.0 : 10.0; // Handle null covariance pointer if(cuvslam_covariance == nullptr) @@ -982,32 +1054,36 @@ cv::Mat convertCuVSLAMCovariance(const float * cuvslam_covariance) block_canonical_pose_cuvslam.block<3, 3>(0, 0) = canonical_pose_cuvslam_mat; block_canonical_pose_cuvslam.block<3, 3>(3, 3) = canonical_pose_cuvslam_mat; - // Map cuVSLAM covariance array to Eigen matrix - Eigen::Matrix covariance_mat = + // Map cuVSLAM covariance array to Eigen matrix and convert to double for numerical stability + Eigen::Matrix covariance_mat_float = Eigen::Map>(const_cast(covariance)); + Eigen::Matrix covariance_mat = covariance_mat_float.cast(); - // Reorder covariance matrix elements + // Reorder covariance matrix elements (in double precision) // The covariance matrix from cuVSLAM arranges elements as follows: // (rotation about X axis, rotation about Y axis, rotation about Z axis, x, y, z) // However, in RTAB-Map, the order is: // (x, y, z, rotation about X axis, rotation about Y axis, rotation about Z axis) - Eigen::Matrix rtabmap_covariance_mat = Eigen::Matrix::Zero(); + Eigen::Matrix rtabmap_covariance_mat = Eigen::Matrix::Zero(); rtabmap_covariance_mat.block<3, 3>(0, 0) = covariance_mat.block<3, 3>(3, 3); // translation-translation rtabmap_covariance_mat.block<3, 3>(0, 3) = covariance_mat.block<3, 3>(3, 0); // translation-rotation rtabmap_covariance_mat.block<3, 3>(3, 0) = covariance_mat.block<3, 3>(0, 3); // rotation-translation rtabmap_covariance_mat.block<3, 3>(3, 3) = covariance_mat.block<3, 3>(0, 0); // rotation-rotation - // Apply coordinate system transformation - Eigen::Matrix covariance_mat_change_basis = - block_canonical_pose_cuvslam * rtabmap_covariance_mat * block_canonical_pose_cuvslam.transpose(); + // Convert transformation matrix to double for numerical stability in matrix operations + Eigen::Matrix block_canonical_pose_cuvslam_double = block_canonical_pose_cuvslam.cast(); - // Convert Eigen matrix to OpenCV Mat + // Apply coordinate system transformation (in double precision) + Eigen::Matrix covariance_mat_change_basis = + block_canonical_pose_cuvslam_double * rtabmap_covariance_mat * block_canonical_pose_cuvslam_double.transpose(); + + // Convert Eigen matrix to OpenCV Mat (already in double precision) cv::Mat cv_covariance(6, 6, CV_64FC1); for(int i = 0; i < 6; i++) { for(int j = 0; j < 6; j++) { - cv_covariance.at(i, j) = static_cast(covariance_mat_change_basis(i, j)); + cv_covariance.at(i, j) = covariance_mat_change_basis(i, j); // for angular values, scale again to make it more realistic if(i > 2 || j > 2) { cv_covariance.at(i, j) *= scaling_factor; From af7ff60a6c5b54db431434a7badb5d13c34668dc Mon Sep 17 00:00:00 2001 From: matlabbe Date: Mon, 15 Dec 2025 15:12:06 -0800 Subject: [PATCH 067/126] Added missing cuvslam panel in UI --- guilib/src/PreferencesDialog.cpp | 5 +- guilib/src/ui/preferencesDialog.ui | 85 +++++++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/guilib/src/PreferencesDialog.cpp b/guilib/src/PreferencesDialog.cpp index fabfd59a..56385632 100644 --- a/guilib/src/PreferencesDialog.cpp +++ b/guilib/src/PreferencesDialog.cpp @@ -1676,12 +1676,14 @@ PreferencesDialog::PreferencesDialog(QWidget * parent) : _ui->stereo_flow_eps->setObjectName(Parameters::kStereoEps().c_str()); _ui->stereo_opticalFlow->setObjectName(Parameters::kStereoOpticalFlow().c_str()); _ui->stereo_flow_gpu->setObjectName(Parameters::kStereoGpu().c_str()); - // Odometry Open3D _ui->odom_open3d_method->setObjectName(Parameters::kOdomOpen3DMethod().c_str()); _ui->odom_open3d_max_depth->setObjectName(Parameters::kOdomOpen3DMaxDepth().c_str()); + // Odometry CuVSLAM + _ui->odom_cuvslam_multicam_mode->setObjectName(Parameters::kOdomCuVSLAMMulticamMode().c_str()); + //StereoDense _ui->comboBox_stereoDense_strategy->setObjectName(Parameters::kStereoDenseStrategy().c_str()); connect(_ui->comboBox_stereoDense_strategy, SIGNAL(currentIndexChanged(int)), _ui->stackedWidget_stereoDense, SLOT(setCurrentIndex(int))); @@ -5542,6 +5544,7 @@ void PreferencesDialog::updateOdometryStackedIndex(int index) _ui->groupBox_odomVINSFusion->setVisible(index==9); _ui->groupBox_odomOpenVINS->setVisible(index==10); _ui->groupBox_odomOpen3D->setVisible(index==12); + _ui->groupBox_odomCuvslam->setVisible(index==13); } void PreferencesDialog::useOdomFeatures() diff --git a/guilib/src/ui/preferencesDialog.ui b/guilib/src/ui/preferencesDialog.ui index b335ff59..d14b7635 100644 --- a/guilib/src/ui/preferencesDialog.ui +++ b/guilib/src/ui/preferencesDialog.ui @@ -63,7 +63,7 @@ 0 - -1237 + -603 684 5110 @@ -16349,7 +16349,7 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - 5 + 13 @@ -21580,6 +21580,87 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag + + + + + + CuVSLAM + + + + + + <html><head/><body><p>CuVSLAM: <a href="https://github.com/NVIDIA-ISAAC-ROS/isaac_ros_visual_slam"><span style=" text-decoration: underline; color:#0000ff;">https://github.com/NVIDIA-ISAAC-ROS/isaac_ros_visual_slam</span></a></p><p>Working only with stereo cameras with current integration. If Reg/Force3DoF is enabled, planar constraints will be used by CuVSLAM.</p></body></html> + + + true + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + Multi-camera Mode + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + QComboBox::AdjustToContents + + + + Moderate + + + + + Performance + + + + + Precision + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 2069 + + + + + + From 26573456c2a3d5f3622d88e4ab698b0dc97be537 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Thu, 18 Dec 2025 16:38:13 -0800 Subject: [PATCH 068/126] Fixed assert on words index <0 when addMoreLoopClosures is called --- corelib/src/Memory.cpp | 3 +-- corelib/src/RegistrationVis.cpp | 6 ++++-- corelib/src/Rtabmap.cpp | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/corelib/src/Memory.cpp b/corelib/src/Memory.cpp index 124ec2a4..b73427b8 100644 --- a/corelib/src/Memory.cpp +++ b/corelib/src/Memory.cpp @@ -4305,8 +4305,7 @@ void Memory::getNodeWordsAndGlobalDescriptors(int nodeId, } if(!words.empty() && wordsKpts.empty() && _dbDriver) { - std::multimap tmpWords; - _dbDriver->getLocalFeatures(nodeId, tmpWords, wordsKpts, words3, wordsDescriptors); + _dbDriver->getLocalFeatures(nodeId, words, wordsKpts, words3, wordsDescriptors); } } diff --git a/corelib/src/RegistrationVis.cpp b/corelib/src/RegistrationVis.cpp index e49d3142..6105f636 100644 --- a/corelib/src/RegistrationVis.cpp +++ b/corelib/src/RegistrationVis.cpp @@ -333,11 +333,12 @@ Transform RegistrationVis::computeTransformationImpl( UDEBUG("Feature Detector = %d", (int)_detectorFrom->getType()); UDEBUG("guess=%s", guess.prettyPrint().c_str()); - UDEBUG("Input(%d): from=%d words, %d 3D words, %d words descriptors, %d kpts, %d kpts3D, %d descriptors, image=%dx%d models=%d stereo=%d", + UDEBUG("Input(%d): from=%d words, %d 3D words, %d words descriptors, %d words kpts, %d kpts, %d kpts3D, %d descriptors, image=%dx%d models=%d stereo=%d", fromSignature.id(), (int)fromSignature.getWords().size(), (int)fromSignature.getWords3().size(), (int)fromSignature.getWordsDescriptors().rows, + (int)fromSignature.getWordsKpts().size(), (int)fromSignature.sensorData().keypoints().size(), (int)fromSignature.sensorData().keypoints3D().size(), fromSignature.sensorData().descriptors().rows, @@ -346,11 +347,12 @@ Transform RegistrationVis::computeTransformationImpl( (int)fromSignature.sensorData().cameraModels().size(), (int)fromSignature.sensorData().stereoCameraModels().size()); - UDEBUG("Input(%d): to=%d words, %d 3D words, %d words descriptors, %d kpts, %d kpts3D, %d descriptors, image=%dx%d models=%d stereo=%d", + UDEBUG("Input(%d): to=%d words, %d 3D words, %d words descriptors, %d words kpts, %d kpts, %d kpts3D, %d descriptors, image=%dx%d models=%d stereo=%d", toSignature.id(), (int)toSignature.getWords().size(), (int)toSignature.getWords3().size(), (int)toSignature.getWordsDescriptors().rows, + (int)toSignature.getWordsKpts().size(), (int)toSignature.sensorData().keypoints().size(), (int)toSignature.sensorData().keypoints3D().size(), toSignature.sensorData().descriptors().rows, diff --git a/corelib/src/Rtabmap.cpp b/corelib/src/Rtabmap.cpp index 98b5432b..e6fca414 100644 --- a/corelib/src/Rtabmap.cpp +++ b/corelib/src/Rtabmap.cpp @@ -5638,6 +5638,7 @@ int Rtabmap::detectMoreLoopClosures( const ProgressState * processState, float clusterRadiusMin) { + UDEBUG(""); UASSERT(iterations>0); if(_graphOptimizer->iterations() <= 0) From 07c3b8c90129d2a9cf77cdbe3d08488a8f03c57b Mon Sep 17 00:00:00 2001 From: matlabbe Date: Thu, 18 Dec 2025 17:09:06 -0800 Subject: [PATCH 069/126] updating words have changed with getNodeWordsAndGlobalDescriptors() --- corelib/src/Memory.cpp | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/corelib/src/Memory.cpp b/corelib/src/Memory.cpp index b73427b8..514400ac 100644 --- a/corelib/src/Memory.cpp +++ b/corelib/src/Memory.cpp @@ -4276,6 +4276,29 @@ void Memory::getNodeWordsAndGlobalDescriptors(int nodeId, words3 = s->getWords3(); wordsDescriptors = s->getWordsDescriptors(); globalDescriptors = s->sensorData().globalDescriptors(); + + if(!words.empty() && wordsKpts.empty() && _dbDriver) + { + std::multimap tmpWords; + _dbDriver->getLocalFeatures(nodeId, tmpWords, wordsKpts, words3, wordsDescriptors); + if(!tmpWords.empty() && !wordsKpts.empty()) + { + UASSERT(tmpWords.size() == words.size()); + std::map wordsChanged = s->getWordsChanged(); + for(const auto & iter: wordsChanged) { + std::list subwords = uValues(tmpWords, iter.first); // old id + if(subwords.size()) + { + tmpWords.erase(iter.first); + for(std::list::const_iterator jter=subwords.begin(); jter!=subwords.end(); ++jter) + { + tmpWords.insert(std::pair(iter.second, (*jter))); // new id + } + } + } + words = tmpWords; + } + } } else if(_dbDriver) { @@ -4303,10 +4326,6 @@ void Memory::getNodeWordsAndGlobalDescriptors(int nodeId, } } } - if(!words.empty() && wordsKpts.empty() && _dbDriver) - { - _dbDriver->getLocalFeatures(nodeId, words, wordsKpts, words3, wordsDescriptors); - } } void Memory::getNodeCalibration(int nodeId, From 72bd453078c98b7b7acbc17fbfc206a677230bd9 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sat, 20 Dec 2025 14:48:09 -0800 Subject: [PATCH 070/126] Fixing build with gtsam 4.3 binaries --- CMakeLists.txt | 10 ++++++++++ corelib/src/optimizer/gtsam/GravityFactor.h | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 418cad37..47a8fbf8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -525,6 +525,16 @@ ENDIF(WITH_G2O) IF(WITH_GTSAM) # Force config mode to ignore PCL's findGTSAM.cmake file FIND_PACKAGE(GTSAM CONFIG QUIET) + IF(GTSAM_FOUND) + # For issue https://github.com/introlab/rtabmap/pull/1626 + FIND_FILE(GTSAM_NOISE_MODEL_FACTOR_N_FILE gtsam/nonlinear/NoiseModelFactorN.h + PATHS ${GTSAM_INCLUDE_DIR} + NO_DEFAULT_PATH) + IF(GTSAM_NOISE_MODEL_FACTOR_N_FILE) + MESSAGE(STATUS "GTSAM with NoiseModelFactorN.h") + ADD_DEFINITIONS("-DGTSAM_WITH_NOISE_MODEL_FACTOR_N") + ENDIF(GTSAM_NOISE_MODEL_FACTOR_N_FILE) + ENDIF(GTSAM_FOUND) ENDIF(WITH_GTSAM) IF(WITH_MRPT) diff --git a/corelib/src/optimizer/gtsam/GravityFactor.h b/corelib/src/optimizer/gtsam/GravityFactor.h index 062a0461..87fb9312 100644 --- a/corelib/src/optimizer/gtsam/GravityFactor.h +++ b/corelib/src/optimizer/gtsam/GravityFactor.h @@ -25,7 +25,7 @@ #pragma once #include -#if GTSAM_VERSION_NUMERIC >= 40300 +#if GTSAM_VERSION_NUMERIC >= 40300 && defined(GTSAM_WITH_NOISE_MODEL_FACTOR_N) #include #endif #include From af10d6b7784701e5e3ae34667b8d09c6e1f531c2 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sat, 20 Dec 2025 18:50:36 -0800 Subject: [PATCH 071/126] Reduced android-deps docker image size (fixing android CI out of space issue) --- docker/noble/android/Dockerfile | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/docker/noble/android/Dockerfile b/docker/noble/android/Dockerfile index 5f4404c5..2234b7c1 100644 --- a/docker/noble/android/Dockerfile +++ b/docker/noble/android/Dockerfile @@ -3,14 +3,17 @@ FROM ubuntu:24.04 # Install build dependencies -RUN apt-get update && apt-get install -y --no-install-recommends apt-utils +RUN apt-get update && apt-get install -y --no-install-recommends \ + apt-utils && apt-get clean && \ + rm -rf /var/lib/apt/lists/ RUN apt-get update && apt-get install -y \ git unzip wget ant cmake \ g++ lib32stdc++6 lib32z1 \ software-properties-common \ freeglut3-dev \ openjdk-8-jdk openjdk-8-jre \ - curl + curl && \ + apt-get clean && rm -rf /var/lib/apt/lists/ ENV ANDROID_HOME=/opt/android-sdk ENV PATH=$PATH:/opt/android-sdk/cmdline-tools/latest/bin:/opt/android-sdk/tools:/opt/android-sdk/platform-tools:/opt/android-sdk/ndk/21.4.7075529 @@ -53,6 +56,8 @@ RUN echo "Install boost..." && \ cd build && \ cmake -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake -DANDROID_ABI=arm64-v8a -DANDROID_NDK=$ANDROID_NDK -DANDROID_NATIVE_API_LEVEL=23 -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/opt/android/arm64-v8a -DCMAKE_FIND_ROOT_PATH="/opt/android/arm64-v8a/bin;/opt/android/arm64-v8a;/opt/android/arm64-v8a/share" .. && \ make -j4 && \ + find . -type f -name "*.a" -print0 \ + | xargs -0 $ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip -g -S -d --strip-debug && \ make install && \ cd /root && \ rm -r boost_1_59_0.tar.gz boost_1_59_0 @@ -80,6 +85,8 @@ RUN echo "Install flann..." && \ cd build && \ cmake -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake -DANDROID_ABI=arm64-v8a -DANDROID_NDK=$ANDROID_NDK -DANDROID_NATIVE_API_LEVEL=23 -DBUILD_SHARED_LIBS=OFF -DBUILD_PYTHON_BINDINGS=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/opt/android/arm64-v8a -DCMAKE_FIND_ROOT_PATH="/opt/android/arm64-v8a/bin;/opt/android/arm64-v8a;/opt/android/arm64-v8a/share" .. && \ make -j4 && \ + find . -type f -name "*.a" -print0 \ + | xargs -0 $ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip -g -S -d --strip-debug && \ make install && \ cd /root && \ rm -rf flann @@ -95,6 +102,8 @@ RUN echo "Install gtsam..." && \ cd build && \ cmake -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake -DANDROID_ABI=arm64-v8a -DANDROID_NDK=$ANDROID_NDK -DANDROID_NATIVE_API_LEVEL=23 -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/opt/android/arm64-v8a -DCMAKE_FIND_ROOT_PATH="/opt/android/arm64-v8a/bin;/opt/android/arm64-v8a;/opt/android/arm64-v8a/share" -DMETIS_SHARED=OFF -DGTSAM_BUILD_STATIC_LIBRARY=ON -DGTSAM_BUILD_TESTS=OFF -DGTSAM_BUILD_EXAMPLES_ALWAYS=OFF -DGTSAM_USE_SYSTEM_EIGEN=ON .. && \ make -j4 && \ + find . -type f -name "*.a" -print0 \ + | xargs -0 $ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip -g -S -d --strip-debug && \ make install && \ cd /root && \ rm -rf gtsam @@ -108,6 +117,8 @@ RUN echo "Install g2o..." && \ cd build && \ cmake -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake -DANDROID_ABI=arm64-v8a -DANDROID_NDK=$ANDROID_NDK -DANDROID_NATIVE_API_LEVEL=23 -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/opt/android/arm64-v8a -DCMAKE_FIND_ROOT_PATH="/opt/android/arm64-v8a/bin;/opt/android/arm64-v8a;/opt/android/arm64-v8a/share" -DBUILD_LGPL_SHARED_LIBS=OFF -DG2O_BUILD_APPS=OFF -DG2O_BUILD_EXAMPLES=OFF -DG2O_USE_OPENGL=OFF .. && \ make -j4 && \ + find "../lib" -type f -name "*.a" -print0 \ + | xargs -0 $ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip -g -S -d --strip-debug && \ make install && \ cd /root && \ rm -rf g2o @@ -117,12 +128,14 @@ RUN echo "Install VTK..." && \ git clone https://github.com/Kitware/VTK.git && \ cd VTK && \ git checkout tags/v8.2.0 && \ - wget https://gist.github.com/matlabbe/e217259fb8ece9ee6daf5a8f70e896a0/raw/2214b503a537d6431d764526b5b780f07d6f168d/vtk_8_2_0_android_r21_fix.patch && \ + wget https://gist.github.com/matlabbe/e217259fb8ece9ee6daf5a8f70e896a0/raw/36d879f0827289e75de969afb7b6515d9a1c71d7/vtk_8_2_0_android_r21_fix.patch && \ git apply vtk_8_2_0_android_r21_fix.patch && \ mkdir build && \ cd build && \ cmake -DBUILD_EXAMPLES=OFF -DBUILD_TESTING=OFF -DVTK_ANDROID_BUILD=ON -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/opt/android/arm64-v8a -DANDROID_ARCH_ABI=arm64-v8a -DCMAKE_FIND_ROOT_PATH="/opt/android/arm64-v8a/bin;/opt/android/arm64-v8a;/opt/android/arm64-v8a/share" .. && \ make -j4 && \ + find . -type f -name "*.a" -print0 \ + | xargs -0 $ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip -g -S -d --strip-debug && \ cp -r CMakeExternals/Install/vtk-android/* /opt/android/arm64-v8a/. && \ cd /root && \ rm -rf VTK @@ -140,6 +153,8 @@ RUN echo "Install pcl..." && \ cmake -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake -DANDROID_ABI=arm64-v8a -DANDROID_NDK=$ANDROID_NDK -DANDROID_NATIVE_API_LEVEL=23 -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/opt/android/arm64-v8a -DCMAKE_FIND_ROOT_PATH="/opt/android/arm64-v8a/bin;/opt/android/arm64-v8a;/opt/android/arm64-v8a/share" -DBUILD_apps=OFF -DBUILD_examples=OFF -DBUILD_tools=OFF -DBUILD_visualization=OFF -DBUILD_tracking=OFF -DBUILD_people=OFF -DBUILD_tools=OFF -DBUILD_global_tests=OFF -DWITH_QT=OFF -DWITH_OPENGL=OFF -DWITH_VTK=ON -DPCL_SHARED_LIBS=OFF .. || true && \ cmake -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake -DANDROID_ABI=arm64-v8a -DANDROID_NDK=$ANDROID_NDK -DANDROID_NATIVE_API_LEVEL=23 -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/opt/android/arm64-v8a -DCMAKE_FIND_ROOT_PATH="/opt/android/arm64-v8a/bin;/opt/android/arm64-v8a;/opt/android/arm64-v8a/share" -DBUILD_apps=OFF -DBUILD_examples=OFF -DBUILD_tools=OFF -DBUILD_visualization=OFF -DBUILD_tracking=OFF -DBUILD_people=OFF -DBUILD_tools=OFF -DBUILD_global_tests=OFF -DWITH_QT=OFF -DWITH_OPENGL=OFF -DWITH_VTK=ON -DPCL_SHARED_LIBS=OFF .. && \ make -j4 && \ + find . -type f -name "*.a" -print0 \ + | xargs -0 $ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip -g -S -d --strip-debug && \ make install && \ cd /root && \ rm -rf pcl @@ -161,14 +176,12 @@ RUN echo "Install OpenCV..." && \ cd build && \ cmake -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake -DANDROID_ABI=arm64-v8a -DANDROID_NDK=$ANDROID_NDK -DANDROID_NATIVE_API_LEVEL=23 -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/opt/android/arm64-v8a -DCMAKE_FIND_ROOT_PATH="/opt/android/arm64-v8a/bin;/opt/android/arm64-v8a;/opt/android/arm64-v8a/share" -DOPENCV_EXTRA_MODULES_PATH=/root/opencv_contrib/modules -DBUILD_TESTS=OFF -DBUILD_PERF_TESTS=OFF -DWITH_CUDA=OFF -DBUILD_opencv_structured_light=OFF -DBUILD_ANDROID_PROJECTS=OFF -DOPENCV_ENABLE_NONFREE=ON -DBUILD_ANDROID_EXAMPLES=OFF -DWITH_PROTOBUF=OFF -DBUILD_opencv_stereo=OFF -DBUILD_JAVA=OFF -DWITH_QUIRC=OFF -DBUILD_opencv_js_bindings_generator=OFF -DBUILD_opencv_objc_bindings_generator=OFF -DBUILD_opencv_objdetect=OFF -DBUILD_opencv_xobjdetect=OFF .. && \ make -j4 && \ + find . -type f -name "*.a" -print0 \ + | xargs -0 $ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip -g -S -d --strip-debug && \ make install && \ cd /root && \ rm -rf opencv opencv_contrib -RUN echo "Strip libraries..." && \ - $ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip -g -S -d --strip-debug --verbose /opt/android/arm64-v8a/lib/*.a && \ - $ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip -g -S -d --strip-debug --verbose /opt/android/arm64-v8a/sdk/native/staticlibs/arm64-v8a/*.a - RUN mkdir /opt/android/lib # tango From 92aff2129f0c78267880aba3485d38e719a33245 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 21 Dec 2025 11:52:38 -0800 Subject: [PATCH 072/126] CameraOrbbec: support explicit rate on non-TOF cameras. (#1627) * rtabmap-rgbd_camera: added width/height options, * using CameraViewer for visualization by default (use -pcl option for old visualization). * CameraViewer: added capture and render rate. * Note that non-TOF orbbec cameras could not be tested. --- .../rtabmap/core/camera/CameraOrbbecSDK.h | 16 +- corelib/src/camera/CameraOrbbecSDK.cpp | 30 +- guilib/include/rtabmap/gui/CameraViewer.h | 4 + guilib/src/CameraViewer.cpp | 8 +- tools/CameraRGBD/main.cpp | 319 +++++++++++------- tools/Report/CMakeLists.txt | 7 +- 6 files changed, 241 insertions(+), 143 deletions(-) diff --git a/corelib/include/rtabmap/core/camera/CameraOrbbecSDK.h b/corelib/include/rtabmap/core/camera/CameraOrbbecSDK.h index 1d7bdf08..c14cc78f 100644 --- a/corelib/include/rtabmap/core/camera/CameraOrbbecSDK.h +++ b/corelib/include/rtabmap/core/camera/CameraOrbbecSDK.h @@ -52,10 +52,10 @@ public: // deviceId can be either an index (e.g., "0"), an UID (e.g, "2-1-2" or "gmsl-1") or a serial ("AAA6454S") CameraOrbbecSDK( std::string deviceId = "", - int colorWidth = 800, - int colorHeight = 600, - int depthWidth = 800, - int depthHeight = 600, + unsigned int colorWidth = 800, + unsigned int colorHeight = 600, + unsigned int depthWidth = 800, + unsigned int depthHeight = 600, float imageRate = 0.0f, const Transform & localTransform = Transform::getIdentity()); virtual ~CameraOrbbecSDK(); @@ -79,10 +79,10 @@ private: #ifdef RTABMAP_ORBBEC_SDK std::string deviceId_; - int colorWidth_; - int colorHeight_; - int depthWidth_; - int depthHeight_; + unsigned int colorWidth_; + unsigned int colorHeight_; + unsigned int depthWidth_; + unsigned int depthHeight_; ob::Pipeline * pipeline_; ob::Pipeline * imuPipeline_; ob::Align * alignFilter_; diff --git a/corelib/src/camera/CameraOrbbecSDK.cpp b/corelib/src/camera/CameraOrbbecSDK.cpp index d93d332c..30d11277 100644 --- a/corelib/src/camera/CameraOrbbecSDK.cpp +++ b/corelib/src/camera/CameraOrbbecSDK.cpp @@ -155,10 +155,10 @@ bool CameraOrbbecSDK::available() CameraOrbbecSDK::CameraOrbbecSDK( std::string deviceId, - int colorWidth, - int colorHeight, - int depthWidth, - int depthHeight, + unsigned int colorWidth, + unsigned int colorHeight, + unsigned int depthWidth, + unsigned int depthHeight, float imageRate, const Transform & localTransform) : Camera(imageRate, localTransform) @@ -327,22 +327,36 @@ bool CameraOrbbecSDK::init(const std::string & calibrationFolder, const std::str UINFO("Resolution: %ldx%ld, FPS: %ld, Format: %d", profile->getWidth(), profile->getHeight(), profile->getFps(), profile->getFormat(), j==0?" (default)":""); + // Get maximum frame rate based on resolution selected if(sensors->getSensorType(i) == OB_SENSOR_DEPTH) { - if(profile->getFps() > maxDepthFps) { + if( profile->getFps() > maxDepthFps && + depthWidth_ == profile->getWidth() && + depthHeight_ == profile->getHeight()) + { maxDepthFps = profile->getFps(); } } else { - if(profile->getFps() > maxColorFps) { + if( profile->getFps() > maxColorFps && + colorWidth_ == profile->getWidth() && + colorHeight_ == profile->getHeight()) + { maxColorFps = profile->getFps(); } - } - + } } } } + // Note that for TOF camera, we want maximum frame rate to better + // sync rgb and depth. For stereo cameras, use the specified frame rate. + if(this->getImageRate()!=0.0f && device->getDeviceInfo()->getDeviceType() != OB_TOF_CAMERA) + { + maxColorFps = maxDepthFps = (unsigned int)this->getImageRate(); + this->setImageRate(0); + } + std::shared_ptr imuConfig; if(imuPublished_) { diff --git a/guilib/include/rtabmap/gui/CameraViewer.h b/guilib/include/rtabmap/gui/CameraViewer.h index 60aa212a..906d1735 100644 --- a/guilib/include/rtabmap/gui/CameraViewer.h +++ b/guilib/include/rtabmap/gui/CameraViewer.h @@ -32,6 +32,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include +#include #include #include @@ -73,6 +74,9 @@ private: QCheckBox * showScanCheckbox_; QCheckBox * markerCheckbox_; MarkerDetector * markerDetector_; + QElapsedTimer fpsTimer_; + double lastCapturePeriod_; + double previousCaptureStamp_; }; } /* namespace rtabmap */ diff --git a/guilib/src/CameraViewer.cpp b/guilib/src/CameraViewer.cpp index 1b1ce5de..601cbc6f 100644 --- a/guilib/src/CameraViewer.cpp +++ b/guilib/src/CameraViewer.cpp @@ -54,7 +54,9 @@ CameraViewer::CameraViewer(QWidget * parent, const ParametersMap & parameters) : cloudView_(new CloudViewer(this)), processingImages_(false), parameters_(parameters), - markerDetector_(0) + markerDetector_(0), + lastCapturePeriod_(0.0), + previousCaptureStamp_(0.0) { qRegisterMetaType("rtabmap::SensorData"); @@ -115,6 +117,7 @@ CameraViewer::CameraViewer(QWidget * parent, const ParametersMap & parameters) : vlayout->addLayout(layout2); this->setLayout(vlayout); + fpsTimer_.start(); } CameraViewer::~CameraViewer() @@ -180,6 +183,7 @@ void CameraViewer::showImage(const rtabmap::SensorData & data) imageView_->setImageDepth(depthOrRight); sizes.append(QString(" Depth=%1x%2").arg(depthOrRight.cols).arg(depthOrRight.rows)); } + sizes.append(QString(" FPS capture=%1 render=%2").arg(lastCapturePeriod_>0.0?(int)round(1.0/lastCapturePeriod_):0).arg((int)round(1.0/fpsTimer_.restart()*1000))); imageSizeLabel_->setText(sizes); if(!depthOrRight.empty() && @@ -308,6 +312,8 @@ bool CameraViewer::handleEvent(UEvent * event) { if(camEvent->data().isValid()) { + lastCapturePeriod_ = camEvent->data().stamp() - previousCaptureStamp_; + previousCaptureStamp_ = camEvent->data().stamp(); if(!processingImages_ && this->isVisible() && camEvent->data().isValid()) { processingImages_ = true; diff --git a/tools/CameraRGBD/main.cpp b/tools/CameraRGBD/main.cpp index 4430b54c..b8342e6e 100644 --- a/tools/CameraRGBD/main.cpp +++ b/tools/CameraRGBD/main.cpp @@ -30,11 +30,14 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/core/util2d.h" #include "rtabmap/core/util3d.h" #include "rtabmap/core/util3d_transforms.h" +#include "rtabmap/core/SensorCaptureThread.h" +#include #include "rtabmap/utilite/ULogger.h" #include "rtabmap/utilite/UMath.h" #include "rtabmap/utilite/UFile.h" #include "rtabmap/utilite/UDirectory.h" #include "rtabmap/utilite/UConversion.h" +#include "rtabmap/utilite/UEventsManager.h" #include #include #include @@ -44,11 +47,12 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include -void showUsage() +void showUsage(const char * executableName) { printf("\nUsage:\n" - "rtabmap-rgbd_camera [options] driver\n" + "%s [options] driver\n" " driver Driver number to use: 0=OpenNI-PCL (Kinect)\n" " 1=OpenNI2 (Kinect and Xtion PRO Live)\n" " 2=Freenect (Kinect)\n" @@ -68,8 +72,12 @@ void showUsage() " 16=XVSDK (SeerSense)\n" " 17=Orbbec SDK\n" " Options:\n" - " -rate #.# Input rate Hz (default 0=inf)\n" - " -device # Device ID (number or string)\n"); + " -r #.# Input rate Hz (default 0=inf)\n" + " -d # Device ID (number or string)\n" + " -w # Request a specific image width. Not all camera drivers support this.\n" + " -h # Request a specific image height. Not all camera drivers support this.\n" + " -pcl Use PCL's CloudViewer for visualization (legacy).\n", + executableName); exit(1); } @@ -91,15 +99,18 @@ int main(int argc, char * argv[]) int driver = 0; float rate = 0.0f; std::string deviceId; + int width = 0; + int height = 0; + bool usePCLViz = false; if(argc < 2) { - showUsage(); + showUsage(argv[0]); } else { for(int i=1; i 17) { UERROR("driver should be between 0 and 17."); - showUsage(); + showUsage(argv[0]); } } } @@ -153,7 +196,7 @@ int main(int argc, char * argv[]) rtabmap::Camera * camera = 0; if(driver == 0) { - camera = new rtabmap::CameraOpenni(deviceId); + camera = new rtabmap::CameraOpenni(deviceId, rate); } else if(driver == 1) { @@ -162,7 +205,7 @@ int main(int argc, char * argv[]) UERROR("Not built with OpenNI2 support..."); exit(-1); } - camera = new rtabmap::CameraOpenNI2(deviceId); + camera = new rtabmap::CameraOpenNI2(deviceId, rtabmap::CameraOpenNI2::kTypeColorDepth, rate); } else if(driver == 2) { @@ -171,7 +214,7 @@ int main(int argc, char * argv[]) UERROR("Not built with Freenect support..."); exit(-1); } - camera = new rtabmap::CameraFreenect(deviceId.empty()?0:uStr2Int(deviceId)); + camera = new rtabmap::CameraFreenect(deviceId.empty()?0:uStr2Int(deviceId), rtabmap::CameraFreenect::kTypeColorDepth, rate); } else if(driver == 3) { @@ -180,7 +223,7 @@ int main(int argc, char * argv[]) UERROR("Not built with OpenNI from OpenCV support..."); exit(-1); } - camera = new rtabmap::CameraOpenNICV(false); + camera = new rtabmap::CameraOpenNICV(false, rate); } else if(driver == 4) { @@ -189,7 +232,7 @@ int main(int argc, char * argv[]) UERROR("Not built with OpenNI from OpenCV support..."); exit(-1); } - camera = new rtabmap::CameraOpenNICV(true); + camera = new rtabmap::CameraOpenNICV(true, rate); } else if(driver == 5) { @@ -198,7 +241,7 @@ int main(int argc, char * argv[]) UERROR("Not built with Freenect2 support..."); exit(-1); } - camera = new rtabmap::CameraFreenect2(deviceId.empty()?0:uStr2Int(deviceId), rtabmap::CameraFreenect2::kTypeColor2DepthSD); + camera = new rtabmap::CameraFreenect2(deviceId.empty()?0:uStr2Int(deviceId), rtabmap::CameraFreenect2::kTypeColor2DepthSD, rate); } else if(driver == 6) { @@ -207,7 +250,7 @@ int main(int argc, char * argv[]) UERROR("Not built with DC1394 support..."); exit(-1); } - camera = new rtabmap::CameraStereoDC1394(); + camera = new rtabmap::CameraStereoDC1394(rate); } else if(driver == 7) { @@ -216,7 +259,7 @@ int main(int argc, char * argv[]) UERROR("Not built with FlyCapture2/Triclops support..."); exit(-1); } - camera = new rtabmap::CameraStereoFlyCapture2(); + camera = new rtabmap::CameraStereoFlyCapture2(rate); } else if(driver == 8) { @@ -225,7 +268,7 @@ int main(int argc, char * argv[]) UERROR("Not built with ZED sdk support..."); exit(-1); } - camera = new rtabmap::CameraStereoZed(deviceId.empty()?0:uStr2Int(deviceId)); + camera = new rtabmap::CameraStereoZed(deviceId.empty()?0:uStr2Int(deviceId), -1, 1, 100, false, rate); } else if (driver == 9) { @@ -234,7 +277,7 @@ int main(int argc, char * argv[]) UERROR("Not built with RealSense support..."); exit(-1); } - camera = new rtabmap::CameraRealSense(deviceId.empty()?0:uStr2Int(deviceId)); + camera = new rtabmap::CameraRealSense(deviceId.empty()?0:uStr2Int(deviceId), 0, 0, false, rate); } else if (driver == 10) { @@ -243,7 +286,7 @@ int main(int argc, char * argv[]) UERROR("Not built with Kinect for Windows 2 SDK support..."); exit(-1); } - camera = new rtabmap::CameraK4W2(deviceId.empty()?0:uStr2Int(deviceId)); + camera = new rtabmap::CameraK4W2(deviceId.empty()?0:uStr2Int(deviceId), rtabmap::CameraK4W2::kTypeDepth2ColorSD, rate); } else if (driver == 11) { @@ -252,7 +295,11 @@ int main(int argc, char * argv[]) UERROR("Not built with RealSense2 SDK support..."); exit(-1); } - camera = new rtabmap::CameraRealSense2(deviceId); + camera = new rtabmap::CameraRealSense2(deviceId, width > 0 && height > 0 ? 0.0f:rate); + if(width > 0 && height > 0) + { + ((rtabmap::CameraRealSense2*)camera)->setResolution(width, height, rate); + } } else if (driver == 12) { @@ -261,7 +308,7 @@ int main(int argc, char * argv[]) UERROR("Not built with Kinect for Azure SDK support..."); exit(-1); } - camera = new rtabmap::CameraK4A(1); + camera = new rtabmap::CameraK4A(1, rate); } else if (driver == 13) { @@ -270,7 +317,7 @@ int main(int argc, char * argv[]) UERROR("Not built with Mynt Eye S support..."); exit(-1); } - camera = new rtabmap::CameraMyntEye(deviceId); + camera = new rtabmap::CameraMyntEye(deviceId, false, false, rate); } else if (driver == 14) { @@ -279,7 +326,7 @@ int main(int argc, char * argv[]) UERROR("Not built with Zed Open Capture support..."); exit(-1); } - camera = new rtabmap::CameraStereoZedOC(deviceId.empty()?-1:uStr2Int(deviceId)); + camera = new rtabmap::CameraStereoZedOC(deviceId.empty()?-1:uStr2Int(deviceId), 3, rate); } else if (driver == 15) { @@ -288,7 +335,7 @@ int main(int argc, char * argv[]) UERROR("Not built with depthai-core support..."); exit(-1); } - camera = new rtabmap::CameraDepthAI(deviceId); + camera = new rtabmap::CameraDepthAI(deviceId, width>0?width:1280, rate); } else if (driver == 16) { @@ -297,7 +344,7 @@ int main(int argc, char * argv[]) UERROR("Not built with XVisio SDK support..."); exit(-1); } - camera = new rtabmap::CameraSeerSense(); + camera = new rtabmap::CameraSeerSense(false, rate); } else if (driver == 17) { @@ -306,7 +353,10 @@ int main(int argc, char * argv[]) UERROR("Not built with Orbbec SDK support..."); exit(-1); } - camera = new rtabmap::CameraOrbbecSDK(); + if(width>0 && height>0) + camera = new rtabmap::CameraOrbbecSDK(deviceId, width, height, width, height, rate); + else + camera = new rtabmap::CameraOrbbecSDK(deviceId, 800, 600, 800, 600, rate); } else { @@ -320,111 +370,134 @@ int main(int argc, char * argv[]) exit(1); } - rtabmap::SensorData data = camera->takeData(); - if (data.imageRaw().empty()) + if(usePCLViz) { - printf("Cloud not get frame from the camera!\n"); - delete camera; - exit(1); - } - if(data.imageRaw().cols % data.depthOrRightRaw().cols != 0 || data.imageRaw().rows % data.depthOrRightRaw().rows != 0) - { - UWARN("RGB (%d/%d) and depth (%d/%d) frames are not the same size! The registered cloud cannot be shown.", - data.imageRaw().cols, data.imageRaw().rows, data.depthOrRightRaw().cols, data.depthOrRightRaw().rows); - } - pcl::visualization::CloudViewer * viewer = 0; - if((data.stereoCameraModels().empty() || data.stereoCameraModels()[0].isValidForProjection()) && - (data.cameraModels().empty() || !data.cameraModels()[0].isValidForProjection())) - { - UWARN("Camera not calibrated! The registered cloud cannot be shown."); - } - else - { - viewer = new pcl::visualization::CloudViewer("cloud"); - } - - // to catch the ctrl-c - signal(SIGABRT, &sighandler); - signal(SIGTERM, &sighandler); - signal(SIGINT, &sighandler); - - while(!data.imageRaw().empty() && (viewer==0 || !viewer->wasStopped()) && running) - { - cv::Mat rgb = data.imageRaw(); - if(!data.depthRaw().empty() && (data.depthRaw().type() == CV_16UC1 || data.depthRaw().type() == CV_32FC1)) + rtabmap::SensorData data = camera->takeData(); + if (data.imageRaw().empty()) { - // depth - cv::Mat depth = data.depthRaw(); - if(depth.type() == CV_32FC1) - { - depth = rtabmap::util2d::cvtDepthFromFloat(depth); - } - - if(!rgb.empty() && rgb.cols % depth.cols == 0 && rgb.rows % depth.rows == 0 && - data.cameraModels().size() && - data.cameraModels()[0].isValidForProjection()) - { - pcl::PointCloud::Ptr cloud = rtabmap::util3d::cloudFromDepthRGB( - rgb, depth, - data.cameraModels()[0]); - cloud = rtabmap::util3d::transformPointCloud(cloud, rtabmap::Transform::opengl_T_rtabmap()*data.cameraModels()[0].localTransform()); - if(viewer) - viewer->showCloud(cloud, "cloud"); - } - else if(!depth.empty() && - data.cameraModels().size() && - data.cameraModels()[0].isValidForProjection()) - { - pcl::PointCloud::Ptr cloud = rtabmap::util3d::cloudFromDepth( - depth, - data.cameraModels()[0]); - cloud = rtabmap::util3d::transformPointCloud(cloud, rtabmap::Transform::opengl_T_rtabmap()*data.cameraModels()[0].localTransform()); - viewer->showCloud(cloud, "cloud"); - } - - cv::Mat tmp; - unsigned short min=0, max = 2048; - uMinMax((unsigned short*)depth.data, depth.rows*depth.cols, min, max); - depth.convertTo(tmp, CV_8UC1, 255.0/max); - - cv::imshow("Video", rgb); // show frame - cv::imshow("Depth", tmp); + printf("Cloud not get frame from the camera!\n"); + delete camera; + exit(1); } - else if(!data.rightRaw().empty()) + if(data.imageRaw().cols % data.depthOrRightRaw().cols != 0 || data.imageRaw().rows % data.depthOrRightRaw().rows != 0) { - // stereo - cv::Mat right = data.rightRaw(); - cv::imshow("Left", rgb); // show frame - cv::imshow("Right", right); + UWARN("RGB (%d/%d) and depth (%d/%d) frames are not the same size! The registered cloud cannot be shown.", + data.imageRaw().cols, data.imageRaw().rows, data.depthOrRightRaw().cols, data.depthOrRightRaw().rows); + } - if(rgb.cols == right.cols && rgb.rows == right.rows && data.stereoCameraModels().size()==1 && data.stereoCameraModels()[0].isValidForProjection()) + pcl::visualization::CloudViewer * viewer = 0; + if((data.stereoCameraModels().empty() || data.stereoCameraModels()[0].isValidForProjection()) && + (data.cameraModels().empty() || !data.cameraModels()[0].isValidForProjection())) + { + UWARN("Camera not calibrated! The registered cloud cannot be shown."); + } + else + { + viewer = new pcl::visualization::CloudViewer("cloud"); + } + + // to catch the ctrl-c + signal(SIGABRT, &sighandler); + signal(SIGTERM, &sighandler); + signal(SIGINT, &sighandler); + + while(!data.imageRaw().empty() && (viewer==0 || !viewer->wasStopped()) && running) + { + cv::Mat rgb = data.imageRaw(); + if(!data.depthRaw().empty() && (data.depthRaw().type() == CV_16UC1 || data.depthRaw().type() == CV_32FC1)) { - if(right.channels() == 3) + // depth + cv::Mat depth = data.depthRaw(); + if(depth.type() == CV_32FC1) { - cv::cvtColor(right, right, CV_BGR2GRAY); + depth = rtabmap::util2d::cvtDepthFromFloat(depth); } - pcl::PointCloud::Ptr cloud = rtabmap::util3d::cloudFromStereoImages( - rgb, right, - data.stereoCameraModels()[0]); - cloud = rtabmap::util3d::transformPointCloud(cloud, rtabmap::Transform::opengl_T_rtabmap()*data.stereoCameraModels()[0].localTransform()); - if(viewer) + + if(!rgb.empty() && rgb.cols % depth.cols == 0 && rgb.rows % depth.rows == 0 && + data.cameraModels().size() && + data.cameraModels()[0].isValidForProjection()) + { + pcl::PointCloud::Ptr cloud = rtabmap::util3d::cloudFromDepthRGB( + rgb, depth, + data.cameraModels()[0]); + cloud = rtabmap::util3d::transformPointCloud(cloud, rtabmap::Transform::opengl_T_rtabmap()*data.cameraModels()[0].localTransform()); + if(viewer) + viewer->showCloud(cloud, "cloud"); + } + else if(!depth.empty() && + data.cameraModels().size() && + data.cameraModels()[0].isValidForProjection()) + { + pcl::PointCloud::Ptr cloud = rtabmap::util3d::cloudFromDepth( + depth, + data.cameraModels()[0]); + cloud = rtabmap::util3d::transformPointCloud(cloud, rtabmap::Transform::opengl_T_rtabmap()*data.cameraModels()[0].localTransform()); viewer->showCloud(cloud, "cloud"); + } + + cv::Mat tmp; + unsigned short min=0, max = 2048; + uMinMax((unsigned short*)depth.data, depth.rows*depth.cols, min, max); + depth.convertTo(tmp, CV_8UC1, 255.0/max); + + cv::imshow("Video", rgb); // show frame + cv::imshow("Depth", tmp); } + else if(!data.rightRaw().empty()) + { + // stereo + cv::Mat right = data.rightRaw(); + cv::imshow("Left", rgb); // show frame + cv::imshow("Right", right); + + if(rgb.cols == right.cols && rgb.rows == right.rows && data.stereoCameraModels().size()==1 && data.stereoCameraModels()[0].isValidForProjection()) + { + if(right.channels() == 3) + { + cv::cvtColor(right, right, CV_BGR2GRAY); + } + pcl::PointCloud::Ptr cloud = rtabmap::util3d::cloudFromStereoImages( + rgb, right, + data.stereoCameraModels()[0]); + cloud = rtabmap::util3d::transformPointCloud(cloud, rtabmap::Transform::opengl_T_rtabmap()*data.stereoCameraModels()[0].localTransform()); + if(viewer) + viewer->showCloud(cloud, "cloud"); + } + } + + int c = cv::waitKey(10); // wait 10 ms or for key stroke + if(c == 27) + break; // if ESC, break and quit + + data = camera->takeData(); } - - int c = cv::waitKey(10); // wait 10 ms or for key stroke - if(c == 27) - break; // if ESC, break and quit - - data = camera->takeData(); + printf("Closing...\n"); + if(viewer) + { + delete viewer; + } + if(usePCLViz) + { + cv::destroyWindow("Video"); + cv::destroyWindow("Depth"); + } + delete camera; } - printf("Closing...\n"); - if(viewer) + else // Use our own visualizer { - delete viewer; + QApplication app(argc, argv); + + rtabmap::CameraViewer cameraViewer; + UEventsManager::addHandler(&cameraViewer); + + cameraViewer.setWindowTitle("Camera view"); + cameraViewer.resize(1280, 480); + + rtabmap::SensorCaptureThread cameraThread(camera); + + cameraThread.start(); + cameraViewer.exec(); + cameraThread.join(true); } - cv::destroyWindow("Video"); - cv::destroyWindow("Depth"); - delete camera; return 0; } diff --git a/tools/Report/CMakeLists.txt b/tools/Report/CMakeLists.txt index 6965f2c5..790a6a61 100644 --- a/tools/Report/CMakeLists.txt +++ b/tools/Report/CMakeLists.txt @@ -2,7 +2,6 @@ set(LIBRARIES rtabmap_core) IF(WITH_QT AND (QT4_FOUND OR Qt5_FOUND OR Qt6_FOUND)) - ADD_DEFINITIONS("-DWITH_QT") set(LIBRARIES ${LIBRARIES} rtabmap_gui) ENDIF(WITH_QT AND (QT4_FOUND OR Qt5_FOUND OR Qt6_FOUND)) @@ -10,8 +9,10 @@ ADD_EXECUTABLE(report main.cpp) TARGET_LINK_LIBRARIES(report ${LIBRARIES}) -SET_TARGET_PROPERTIES( report - PROPERTIES OUTPUT_NAME ${PROJECT_PREFIX}-report) +SET_TARGET_PROPERTIES(report PROPERTIES + OUTPUT_NAME ${PROJECT_PREFIX}-report + COMPILE_DEFINITIONS "WITH_QT" +) INSTALL(TARGETS report RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" COMPONENT runtime From 93ca4ed8d7ba8a82bfb163517d1fc7b6bd723f40 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Mon, 12 Jan 2026 10:42:46 -0800 Subject: [PATCH 073/126] ImageView: fixed depth colormap in height with multicameras --- guilib/src/ImageView.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/guilib/src/ImageView.cpp b/guilib/src/ImageView.cpp index 5ab33397..cdcb28bb 100644 --- a/guilib/src/ImageView.cpp +++ b/guilib/src/ImageView.cpp @@ -1401,7 +1401,7 @@ void ImageView::setImageDepth(const cv::Mat & imageDepth, const cv::Mat & imageD cv::Point3f pt; int cameraIndex = u/subImageWidth; UASSERT(cameraIndex>=0 && cameraIndex < (int)models.size() && subImageWidth == models[cameraIndex].imageWidth()); - models[cameraIndex].project(u,v,float(val)/1000.0f, pt.x, pt.y, pt.z); + models[cameraIndex].project(u-(cameraIndex*subImageWidth),v,float(val)/1000.0f, pt.x, pt.y, pt.z); pt = util3d::transformPoint(pt, models[cameraIndex].localTransform()); val = (unsigned short)(pt.z*1000.0f); } @@ -1417,7 +1417,7 @@ void ImageView::setImageDepth(const cv::Mat & imageDepth, const cv::Mat & imageD cv::Point3f pt; int cameraIndex = u/subImageWidth; UASSERT(cameraIndex>=0 && cameraIndex < (int)models.size() && subImageWidth == models[cameraIndex].imageWidth()); - models[cameraIndex].project(u,v,val, pt.x, pt.y, pt.z); + models[cameraIndex].project(u-(cameraIndex*subImageWidth),v,val, pt.x, pt.y, pt.z); pt = util3d::transformPoint(pt, models[cameraIndex].localTransform()); val = pt.z; } From 7db14533f11be9f35d98e1eb0c1e42d417f66eed Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sat, 17 Jan 2026 18:07:32 -0800 Subject: [PATCH 074/126] Added realsense2 playback support (bag) (#1636) --- .../rtabmap/core/camera/CameraRealSense2.h | 1 + corelib/src/camera/CameraRealSense2.cpp | 154 +++++++---- guilib/src/ui/preferencesDialog.ui | 250 +++++++++--------- 3 files changed, 224 insertions(+), 181 deletions(-) diff --git a/corelib/include/rtabmap/core/camera/CameraRealSense2.h b/corelib/include/rtabmap/core/camera/CameraRealSense2.h index 47788f0f..10843a26 100644 --- a/corelib/include/rtabmap/core/camera/CameraRealSense2.h +++ b/corelib/include/rtabmap/core/camera/CameraRealSense2.h @@ -146,6 +146,7 @@ private: Transform dualExtrinsics_; std::string jsonConfig_; bool closing_; + bool playback_; static Transform realsense2PoseRotation_; static Transform realsense2PoseRotationInv_; diff --git a/corelib/src/camera/CameraRealSense2.cpp b/corelib/src/camera/CameraRealSense2.cpp index 5f986a4f..fb15b0c3 100644 --- a/corelib/src/camera/CameraRealSense2.cpp +++ b/corelib/src/camera/CameraRealSense2.cpp @@ -30,6 +30,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include #include #ifdef RTABMAP_REALSENSE2 @@ -78,7 +79,8 @@ CameraRealSense2::CameraRealSense2( cameraDepthFps_(30), globalTimeSync_(true), dualMode_(false), - closing_(false) + closing_(false), + playback_(false) #endif { UDEBUG(""); @@ -130,6 +132,7 @@ void CameraRealSense2::close() } closing_ = false; + playback_ = false; } void CameraRealSense2::imu_callback(rs2::frame frame) @@ -492,64 +495,76 @@ bool CameraRealSense2::init(const std::string & calibrationFolder, const std::st clockSyncWarningShown_ = false; imuGlobalSyncWarningShown_ = false; - rs2::device_list list = ctx_.query_devices(); - if (0 == list.size()) + if(uStrContains(deviceId_, ".bag")) { - UERROR("No RealSense2 devices were found!"); - return false; + // playback (bag recorded by realsense-viewer) + dev_.resize(1); + dev_[0] = ctx_.load_device(uReplaceChar(deviceId_, '~', UDirectory::homeDir())); + playback_ = true; + UINFO("Device ID is a bag (\"%s\"), using playback mode", deviceId_.c_str()); } - - bool found=false; - try + else { - for (rs2::device dev : list) + rs2::device_list list = ctx_.query_devices(); + if (0 == list.size()) { - auto sn = dev.get_info(RS2_CAMERA_INFO_SERIAL_NUMBER); - auto pid_str = dev.get_info(RS2_CAMERA_INFO_PRODUCT_ID); - auto name = dev.get_info(RS2_CAMERA_INFO_NAME); + UERROR("No RealSense2 devices were found!"); + return false; + } - uint16_t pid; - std::stringstream ss; - ss << std::hex << pid_str; - ss >> pid; - UINFO("Device \"%s\" with serial number %s was found with product ID=%d.", name, sn, (int)pid); - if(dualMode_ && pid == 0x0B37) + bool found=false; + try + { + for (rs2::device dev : list) { - // Dual setup: device[0] = D400, device[1] = T265 - // T265 - dev_.resize(2); - dev_[1] = dev; - } - else if (!found && (deviceId_.empty() || deviceId_ == sn || uStrContains(name, uToUpperCase(deviceId_)))) - { - if(dev_.empty()) + auto sn = dev.get_info(RS2_CAMERA_INFO_SERIAL_NUMBER); + auto pid_str = dev.get_info(RS2_CAMERA_INFO_PRODUCT_ID); + auto name = dev.get_info(RS2_CAMERA_INFO_NAME); + + uint16_t pid; + std::stringstream ss; + ss << std::hex << pid_str; + ss >> pid; + UINFO("Device \"%s\" with serial number %s was found with product ID=%d.", name, sn, (int)pid); + if(dualMode_ && pid == 0x0B37) { - dev_.resize(1); + // Dual setup: device[0] = D400, device[1] = T265 + // T265 + dev_.resize(2); + dev_[1] = dev; + } + else if (!found && (deviceId_.empty() || deviceId_ == sn || uStrContains(name, uToUpperCase(deviceId_)))) + { + if(dev_.empty()) + { + dev_.resize(1); + } + dev_[0] = dev; + found=true; } - dev_[0] = dev; - found=true; } } - } - catch(const rs2::error & error) - { - UWARN("%s. Is the camera already used with another app?", error.what()); + catch(const rs2::error & error) + { + UWARN("%s. Is the camera already used with another app?", error.what()); + } + + if (!found) + { + if(dualMode_ && dev_.size()==2) + { + UERROR("Dual setup is enabled, but a D400 camera is not detected!"); + dev_.clear(); + } + else + { + UERROR("The requested device \"%s\" is NOT found!", deviceId_.c_str()); + } + return false; + } } - if (!found) - { - if(dualMode_ && dev_.size()==2) - { - UERROR("Dual setup is enabled, but a D400 camera is not detected!"); - dev_.clear(); - } - else - { - UERROR("The requested device \"%s\" is NOT found!", deviceId_.c_str()); - } - return false; - } - else if(dualMode_ && dev_.size()!=2) + if(dualMode_ && dev_.size()!=2) { UERROR("Dual setup is enabled, but a T265 camera is not detected!"); dev_.clear(); @@ -634,7 +649,14 @@ bool CameraRealSense2::init(const std::string & calibrationFolder, const std::st sensors[1] = elem; if(sensors[1].supports(rs2_option::RS2_OPTION_EMITTER_ENABLED)) { - sensors[1].set_option(rs2_option::RS2_OPTION_EMITTER_ENABLED, emitterEnabled_); + if(!sensors[1].is_option_read_only(rs2_option::RS2_OPTION_EMITTER_ENABLED)) + { + sensors[1].set_option(rs2_option::RS2_OPTION_EMITTER_ENABLED, emitterEnabled_); + } + else if(!emitterEnabled_) + { + UWARN("rs2_option::RS2_OPTION_EMITTER_ENABLED option is read-only, cannot disable IR emitter."); + } } } else if ("Coded-Light Depth Sensor" == module_name) @@ -732,7 +754,8 @@ bool CameraRealSense2::init(const std::string & calibrationFolder, const std::st video_profile.width() == cameraWidth_ && video_profile.height() == cameraHeight_ && video_profile.fps() == cameraFps_) || - (strcmp(sensors[i].get_info(RS2_CAMERA_INFO_NAME), "L500 Depth Sensor")==0 && + ((strcmp(sensors[i].get_info(RS2_CAMERA_INFO_NAME), "L500 Depth Sensor")==0 || + (playback_ && strcmp(sensors[i].get_info(RS2_CAMERA_INFO_NAME), "Stereo Module")==0)) && video_profile.width() == cameraDepthWidth_ && video_profile.height() == cameraDepthHeight_ && video_profile.fps() == cameraDepthFps_)) @@ -741,7 +764,7 @@ bool CameraRealSense2::init(const std::string & calibrationFolder, const std::st // rgb or ir left if((!ir_ && video_profile.format() == RS2_FORMAT_RGB8 && video_profile.stream_type() == RS2_STREAM_COLOR) || - (ir_ && video_profile.format() == RS2_FORMAT_Y8 && (video_profile.stream_index() == 1 || isL500))) + (ir_ && video_profile.format() == RS2_FORMAT_Y8 && (video_profile.stream_index() == 1 || isL500))) { if(!profilesPerSensor[i].empty()) { @@ -880,8 +903,17 @@ bool CameraRealSense2::init(const std::string & calibrationFolder, const std::st } if (!added) { - UERROR("Given stream configuration is not supported by the device! " - "Stream Index: %d, Width: %d, Height: %d, FPS: %d", i, cameraWidth_, cameraHeight_, cameraFps_); + if(strcmp(sensors[i].get_info(RS2_CAMERA_INFO_NAME), "L500 Depth Sensor")==0 || + (playback_ && strcmp(sensors[i].get_info(RS2_CAMERA_INFO_NAME), "Stereo Module")==0)) + { + UERROR("Given stream configuration is not supported by the device! " + "Stream Index: %d, Width: %d, Height: %d, FPS: %d", i, cameraDepthWidth_, cameraDepthHeight_, cameraDepthFps_); + } + else + { + UERROR("Given stream configuration is not supported by the device! " + "Stream Index: %d, Width: %d, Height: %d, FPS: %d", i, cameraWidth_, cameraHeight_, cameraFps_); + } UERROR("Available configurations:"); for (auto& profile : profiles) { @@ -1087,9 +1119,16 @@ bool CameraRealSense2::init(const std::string & calibrationFolder, const std::st } if(globalTimeSync_ && sensors[i].supports(rs2_option::RS2_OPTION_GLOBAL_TIME_ENABLED)) { - float value = sensors[i].get_option(rs2_option::RS2_OPTION_GLOBAL_TIME_ENABLED); - UINFO("Set RS2_OPTION_GLOBAL_TIME_ENABLED=1 (was %f) for sensor %d", value, (int)i); - sensors[i].set_option(rs2_option::RS2_OPTION_GLOBAL_TIME_ENABLED, 1); + float value = sensors[i].get_option(rs2_option::RS2_OPTION_GLOBAL_TIME_ENABLED); + UINFO("Set RS2_OPTION_GLOBAL_TIME_ENABLED=1 (was %f) for sensor %d", value, (int)i); + if(!sensors[i].is_option_read_only(rs2_option::RS2_OPTION_GLOBAL_TIME_ENABLED)) + { + sensors[i].set_option(rs2_option::RS2_OPTION_GLOBAL_TIME_ENABLED, 1); + } + else if(value != 1) + { + UWARN("rs2_option::RS2_OPTION_GLOBAL_TIME_ENABLED option is read-only, cannot enable it."); + } } sensors[i].open(profilesPerSensor[i]); if(sensors[i].is()) @@ -1538,7 +1577,10 @@ SensorData CameraRealSense2::captureImage(SensorCaptureInfo * info) } catch(const std::exception& ex) { - UERROR("An error has occurred during frame callback: %s", ex.what()); + if(!playback_) + { + UERROR("An error has occurred during frame callback: %s", ex.what()); + } } #else UERROR("CameraRealSense2: RTAB-Map is not built with RealSense2 support!"); diff --git a/guilib/src/ui/preferencesDialog.ui b/guilib/src/ui/preferencesDialog.ui index d14b7635..61a08cd4 100644 --- a/guilib/src/ui/preferencesDialog.ui +++ b/guilib/src/ui/preferencesDialog.ui @@ -63,7 +63,7 @@ 0 - -603 + -747 684 5110 @@ -95,7 +95,7 @@ QFrame::Raised - 19 + 5 @@ -3590,7 +3590,7 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - 12 + 9 @@ -4578,8 +4578,15 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki RealSense2 - - + + + + ... + + + + + @@ -4588,10 +4595,13 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - + + + + + - IR emitter enabled + Use depth image in IR mode (instead of right image). true @@ -4601,13 +4611,16 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - + + - + (L515 or playback) Depth stream rate. - - false + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse @@ -4624,45 +4637,15 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - + + - - - - false - - - - - - - Use depth image in IR mode (instead of right image). + <html><head/><body><p>D400 Series Visual Presets. See this <a href="https://github.com/IntelRealSense/librealsense/wiki/D400-Series-Visual-Presets"><span style=" text-decoration: underline; color:#0000ff;">page</span></a>.</p></body></html> true - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - pix - - - 9999 - - - - - - - RGB/IR stream width. Set 1280 for L515. - - + true @@ -4670,16 +4653,6 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - - - pix - - - 9999 - - - @@ -4716,8 +4689,67 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - + + + + RGB/IR stream width. Set 1280 for L515. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + pix + + + 9999 + + + + + + + IR emitter enabled + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + false + + + + + + + (L515 or playback) Depth stream height. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + pix @@ -4729,7 +4761,7 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - (L515) Depth stream width. + (L515 or playback) Depth stream width. true @@ -4739,54 +4771,8 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - - - pix - - - 9999 - - - - - - - (L515) Depth stream height. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - Hz - - - 999 - - - - - - - (L515) Depth stream rate. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - + + @@ -4808,29 +4794,43 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - - - ... + + + + pix + + + 9999 - - + + + + Hz + + + 999 + + - - + + + + pix + + + 9999 + + + + + - <html><head/><body><p>D400 Series Visual Presets. See this <a href="https://github.com/IntelRealSense/librealsense/wiki/D400-Series-Visual-Presets"><span style=" text-decoration: underline; color:#0000ff;">page</span></a>.</p></body></html> + - - true - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + false From 42faef39413a5a840f91edd2f24244e321d04adc Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sat, 17 Jan 2026 18:21:16 -0800 Subject: [PATCH 075/126] DBReader: added ignore imu option, GUI: added tf overrides option (#1637) * DBReader: added ignore imu option, GUI: added tf overrides option * transform offset --- corelib/include/rtabmap/core/DBReader.h | 3 + .../include/rtabmap/core/RegistrationInfo.h | 3 + corelib/include/rtabmap/core/Statistics.h | 1 + .../include/rtabmap/core/util3d_features.h | 1 + corelib/src/DBReader.cpp | 21 +- corelib/src/Features2d.cpp | 4 +- corelib/src/Memory.cpp | 17 +- corelib/src/RegistrationIcp.cpp | 1 + corelib/src/RegistrationVis.cpp | 6 + corelib/src/Rtabmap.cpp | 4 + corelib/src/odometry/OdometryMono.cpp | 1 + corelib/src/util3d_features.cpp | 5 +- guilib/src/PreferencesDialog.cpp | 65 +++- guilib/src/ui/aboutDialog.ui | 2 +- guilib/src/ui/preferencesDialog.ui | 354 ++++++++++-------- tools/Reprocess/main.cpp | 8 + 16 files changed, 330 insertions(+), 166 deletions(-) diff --git a/corelib/include/rtabmap/core/DBReader.h b/corelib/include/rtabmap/core/DBReader.h index 9aae674d..7bbeaa8a 100644 --- a/corelib/include/rtabmap/core/DBReader.h +++ b/corelib/include/rtabmap/core/DBReader.h @@ -59,6 +59,7 @@ public: int startMapId = 0, int stopMapId = -1, bool priorsIgnored = false, + bool imuIgnored = false, const std::vector & cameraLocalTransformOverrides = std::vector()); DBReader(const std::list & databasePaths, float frameRate = 0.0f, // -1 = use Database stamps, 0 = inf @@ -74,6 +75,7 @@ public: int startMapId = 0, int stopMapId = -1, bool priorsIgnored = false, + bool imuIgnored = false, const std::vector & cameraLocalTransformOverrides = std::vector()); virtual ~DBReader(); @@ -107,6 +109,7 @@ private: bool _landmarksIgnored; bool _featuresIgnored; bool _priorsIgnored; + bool _imuIgnored; int _startMapId; int _stopMapId; std::vector _cameraLocalTransformOverrides; diff --git a/corelib/include/rtabmap/core/RegistrationInfo.h b/corelib/include/rtabmap/core/RegistrationInfo.h index 75592898..3ec65098 100644 --- a/corelib/include/rtabmap/core/RegistrationInfo.h +++ b/corelib/include/rtabmap/core/RegistrationInfo.h @@ -41,6 +41,7 @@ public: inliersMeanDistance(0.0f), inliersDistribution(0.0f), matches(0), + variance(0.0f), icpInliersRatio(0), icpTranslation(0.0f), icpRotation(0.0f), @@ -64,6 +65,7 @@ public: output.inliersDistribution = inliersDistribution; output.matches = matches; output.matchesPerCam = matchesPerCam; + output.variance = variance; output.icpInliersRatio = icpInliersRatio; output.icpTranslation = icpTranslation; output.icpRotation = icpRotation; @@ -85,6 +87,7 @@ public: float inliersDistribution; std::vector inliersIDs; int matches; + float variance; std::vector matchesIDs; std::vector projectedIDs; // "From" IDs std::vector inliersPerCam; diff --git a/corelib/include/rtabmap/core/Statistics.h b/corelib/include/rtabmap/core/Statistics.h index 61abddea..8f7a32ce 100644 --- a/corelib/include/rtabmap/core/Statistics.h +++ b/corelib/include/rtabmap/core/Statistics.h @@ -67,6 +67,7 @@ class RTABMAP_CORE_EXPORT Statistics RTABMAP_STATS(Loop, Visual_inliers,); RTABMAP_STATS(Loop, Visual_inliers_ratio,); RTABMAP_STATS(Loop, Visual_matches,); + RTABMAP_STATS(Loop, Visual_variance,); RTABMAP_STATS(Loop, Distance_since_last_loc, m); RTABMAP_STATS(Loop, Last_id,); RTABMAP_STATS(Loop, Optimization_max_error, m); diff --git a/corelib/include/rtabmap/core/util3d_features.h b/corelib/include/rtabmap/core/util3d_features.h index 2a02bf2e..2ea95bf7 100644 --- a/corelib/include/rtabmap/core/util3d_features.h +++ b/corelib/include/rtabmap/core/util3d_features.h @@ -80,6 +80,7 @@ std::map RTABMAP_CORE_EXPORT generateWords3DMono( Transform & cameraTransform, float ransacReprojThreshold = 3.0f, float ransacConfidence = 0.99f, + int varianceMedianRatio = 4, const std::map & refGuess3D = std::map(), double * variance = 0, std::vector * matchesOut = 0); diff --git a/corelib/src/DBReader.cpp b/corelib/src/DBReader.cpp index 55300b0c..17181c9a 100644 --- a/corelib/src/DBReader.cpp +++ b/corelib/src/DBReader.cpp @@ -56,6 +56,7 @@ DBReader::DBReader(const std::string & databasePath, int startMapId, int stopMapId, bool priorsIgnored, + bool imuIgnored, const std::vector & cameraLocalTransformOverrides) : Camera(frameRate), _paths(uSplit(databasePath, ';')), @@ -69,6 +70,7 @@ DBReader::DBReader(const std::string & databasePath, _landmarksIgnored(landmarksIgnored), _featuresIgnored(featuresIgnored), _priorsIgnored(priorsIgnored), + _imuIgnored(imuIgnored), _startMapId(startMapId), _stopMapId(stopMapId), _cameraLocalTransformOverrides(cameraLocalTransformOverrides), @@ -96,6 +98,7 @@ DBReader::DBReader(const std::list & databasePaths, int startMapId, int stopMapId, bool priorsIgnored, + bool imuIgnored, const std::vector & cameraLocalTransformOverrides) : Camera(frameRate), _paths(databasePaths), @@ -109,6 +112,7 @@ DBReader::DBReader(const std::list & databasePaths, _landmarksIgnored(landmarksIgnored), _featuresIgnored(featuresIgnored), _priorsIgnored(priorsIgnored), + _imuIgnored(imuIgnored), _startMapId(startMapId), _stopMapId(stopMapId), _cameraLocalTransformOverrides(cameraLocalTransformOverrides), @@ -463,14 +467,17 @@ SensorData DBReader::getNextData(SensorCaptureInfo * info) } Transform gravityTransform; - std::multimap gravityLinks; - _dbDriver->loadLinks(*_currentId, gravityLinks, Link::kGravity); - if( gravityLinks.size() && - !gravityLinks.begin()->second.transform().isNull() && - gravityLinks.begin()->second.infMatrix().cols == 6 && - gravityLinks.begin()->second.infMatrix().rows == 6) + if(!_imuIgnored) { - gravityTransform = gravityLinks.begin()->second.transform(); + std::multimap gravityLinks; + _dbDriver->loadLinks(*_currentId, gravityLinks, Link::kGravity); + if( gravityLinks.size() && + !gravityLinks.begin()->second.transform().isNull() && + gravityLinks.begin()->second.infMatrix().cols == 6 && + gravityLinks.begin()->second.infMatrix().rows == 6) + { + gravityTransform = gravityLinks.begin()->second.transform(); + } } Landmarks landmarks; diff --git a/corelib/src/Features2d.cpp b/corelib/src/Features2d.cpp index 329e5a53..41b5a12a 100644 --- a/corelib/src/Features2d.cpp +++ b/corelib/src/Features2d.cpp @@ -303,7 +303,7 @@ void Feature2D::limitKeypoints(std::vector & keypoints, std::vecto cv::Mat descriptorsTmp; if(ssc) { - ULOGGER_DEBUG("too much words (%d), removing words with SSC", keypoints.size()); + ULOGGER_DEBUG("too many words (%d), removing words with SSC", keypoints.size()); // Sorting keypoints by deacreasing order of strength std::vector responseVector; @@ -419,7 +419,7 @@ void Feature2D::limitKeypoints(const std::vector & keypoints, std: inliers.resize(keypoints.size(), false); if(ssc) { - ULOGGER_DEBUG("too much words (%d), removing words with SSC", keypoints.size()); + ULOGGER_DEBUG("too many words (%d), removing words with SSC", keypoints.size()); // Sorting keypoints by deacreasing order of strength std::vector responseVector; diff --git a/corelib/src/Memory.cpp b/corelib/src/Memory.cpp index 514400ac..a7a1c41b 100644 --- a/corelib/src/Memory.cpp +++ b/corelib/src/Memory.cpp @@ -5836,7 +5836,6 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor cameraModels.size() == 1 && words.size() && (words3D.size() == 0 || (words.size() == words3D.size() && words3DValid!=(int)words3D.size())) && - _registrationPipeline->isImageRequired() && _signatures.size() && _signatures.rbegin()->second->mapId() == _idMapCount) // same map { @@ -5880,11 +5879,14 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor // The following is used only to re-estimate the correspondences, the returned transform is ignored Transform tmpt; - RegistrationVis reg(parameters_); + ParametersMap tmpParams = parameters_; + // Pure 2D-2D without guess would generate variance=1 + uInsert(tmpParams, ParametersPair(Parameters::kVisEpipolarGeometryVar(), "1")); + RegistrationVis reg(tmpParams); if(_registrationPipeline->isScanRequired()) { // If icp is used, remove it to just do visual registration - RegistrationVis vis(parameters_); + RegistrationVis vis(tmpParams); tmpt = vis.computeTransformationMod(cpCurrent, cpPrevious, cameraTransform); } else @@ -5906,11 +5908,18 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor { previousWords.insert(std::make_pair(iter->first, cpPrevious.getWordsKpts()[iter->second])); } + float reprojError = Parameters::defaultVisPnPReprojError(); + int varianceMedianRatio = Parameters::defaultVisPnPVarianceMedianRatio(); + Parameters::parse(parameters_, Parameters::kVisPnPReprojError(), reprojError); + Parameters::parse(parameters_, Parameters::kVisPnPVarianceMedianRatio(), varianceMedianRatio); std::map inliers = util3d::generateWords3DMono( currentWords, previousWords, cameraModels[0], - cameraTransform); + cameraTransform, + reprojError, + 0.99f, + varianceMedianRatio); UDEBUG("inliers=%d", (int)inliers.size()); diff --git a/corelib/src/RegistrationIcp.cpp b/corelib/src/RegistrationIcp.cpp index 11611ae1..29bda086 100644 --- a/corelib/src/RegistrationIcp.cpp +++ b/corelib/src/RegistrationIcp.cpp @@ -529,6 +529,7 @@ Transform RegistrationIcp::computeTransformationImpl( double toComplexity = util3d::computeNormalsComplexity(toScan, guess, &complexityVectorsTo, &complexityValuesTo); float complexity = fromComplexity(0,0); + } UDEBUG("inliers=%d/%d", info.inliers, info.matches); UDEBUG("transform=%s", transform.prettyPrint().c_str()); diff --git a/corelib/src/Rtabmap.cpp b/corelib/src/Rtabmap.cpp index e6fca414..96146792 100644 --- a/corelib/src/Rtabmap.cpp +++ b/corelib/src/Rtabmap.cpp @@ -2613,6 +2613,7 @@ bool Rtabmap::process( int loopClosureVisualInliers = 0; // for statistics float loopClosureVisualInliersRatio = 0.0f; int loopClosureVisualMatches = 0; + float loopClosureVisualVariance = 0.0f; float loopClosureLinearVariance = 0.0f; float loopClosureAngularVariance = 0.0f; float loopClosureVisualInliersMeanDist = 0; @@ -2795,6 +2796,7 @@ bool Rtabmap::process( loopClosureVisualInliers = info.inliers; loopClosureVisualInliersRatio = info.inliersRatio; loopClosureVisualMatches = info.matches; + loopClosureVisualVariance = info.variance; cv::Mat information = getInformation(info.covariance); loopClosureLinearVariance = 1.0/information.at(0,0); @@ -3062,6 +3064,7 @@ bool Rtabmap::process( loopClosureVisualInliers = info.inliers; loopClosureVisualInliersRatio = info.inliersRatio; loopClosureVisualMatches = info.matches; + loopClosureVisualVariance = info.variance; rejectedLoopClosure = transform.isNull(); if(rejectedLoopClosure) { @@ -4049,6 +4052,7 @@ bool Rtabmap::process( statistics_.addStatistic(Statistics::kLoopVisual_inliers(), loopClosureVisualInliers); statistics_.addStatistic(Statistics::kLoopVisual_inliers_ratio(), loopClosureVisualInliersRatio); statistics_.addStatistic(Statistics::kLoopVisual_matches(), loopClosureVisualMatches); + statistics_.addStatistic(Statistics::kLoopVisual_variance(), loopClosureVisualVariance); statistics_.addStatistic(Statistics::kLoopLinear_variance(), loopClosureLinearVariance); statistics_.addStatistic(Statistics::kLoopAngular_variance(), loopClosureAngularVariance); statistics_.addStatistic(Statistics::kLoopLast_id(), _memory->getLastGlobalLoopClosureId()); diff --git a/corelib/src/odometry/OdometryMono.cpp b/corelib/src/odometry/OdometryMono.cpp index 7df312e7..c8e7f1be 100644 --- a/corelib/src/odometry/OdometryMono.cpp +++ b/corelib/src/odometry/OdometryMono.cpp @@ -775,6 +775,7 @@ Transform OdometryMono::computeTransform(SensorData & data, const Transform & gu cameraTransform, fundMatrixReprojError_, fundMatrixConfidence_, + 4, refWords3Guess); // for scale estimation if(cameraTransform.getNorm() < minTranslation_*5) diff --git a/corelib/src/util3d_features.cpp b/corelib/src/util3d_features.cpp index f6eb40f4..c2871be4 100644 --- a/corelib/src/util3d_features.cpp +++ b/corelib/src/util3d_features.cpp @@ -213,6 +213,7 @@ std::map generateWords3DMono( Transform & cameraTransform, float ransacReprojThreshold, float ransacConfidence, + int varianceMedianRatio, const std::map & refGuess3D, double * varianceOut, std::vector * matchesOut) @@ -345,7 +346,7 @@ std::map generateWords3DMono( errorSqrdDists[j] = uNormSquared(refPt.x-newPt.x, refPt.y-newPt.y, refPt.z-newPt.z); } std::sort(errorSqrdDists.begin(), errorSqrdDists.end()); - double median_error_sqr = (double)errorSqrdDists[errorSqrdDists.size () >> 2]; + double median_error_sqr = (double)errorSqrdDists[errorSqrdDists.size () >> varianceMedianRatio]; float var = 2.1981 * median_error_sqr; //UDEBUG("scale %d = %f variance = %f", (int)i, s, variance); @@ -369,7 +370,7 @@ std::map generateWords3DMono( errorSqrdDists[j] = uNormSquared(refPt.x-newPt.x, refPt.y-newPt.y, refPt.z-newPt.z); } std::sort(errorSqrdDists.begin(), errorSqrdDists.end()); - double median_error_sqr = (double)errorSqrdDists[errorSqrdDists.size () >> 2]; + double median_error_sqr = (double)errorSqrdDists[errorSqrdDists.size () >> varianceMedianRatio]; variance = 2.1981 * median_error_sqr; } } diff --git a/guilib/src/PreferencesDialog.cpp b/guilib/src/PreferencesDialog.cpp index 56385632..68a2b5bf 100644 --- a/guilib/src/PreferencesDialog.cpp +++ b/guilib/src/PreferencesDialog.cpp @@ -755,10 +755,13 @@ PreferencesDialog::PreferencesDialog(QWidget * parent) : connect(_ui->source_checkBox_ignoreLandmarks, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->source_checkBox_ignoreFeatures, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->source_checkBox_ignorePriors, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); + connect(_ui->source_checkBox_ignoreIMU, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->source_spinBox_databaseStartId, SIGNAL(valueChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->source_spinBox_databaseStopId, SIGNAL(valueChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->source_checkBox_useDbStamps, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->source_lineEdit_databaseCameraIndex, SIGNAL(textChanged(const QString &)), this, SLOT(makeObsoleteSourcePanel())); + connect(_ui->source_checkBox_overrideLocalTransforms, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); + connect(_ui->source_lineEdit_databaseLocalTransformOffset, SIGNAL(textChanged(const QString &)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->source_checkBox_stereoToDepthDB, SIGNAL(toggled(bool)), _ui->checkbox_stereo_depthGenerated, SLOT(setChecked(bool))); connect(_ui->checkbox_stereo_depthGenerated, SIGNAL(toggled(bool)), _ui->source_checkBox_stereoToDepthDB, SLOT(setChecked(bool))); @@ -2194,10 +2197,13 @@ void PreferencesDialog::resetSettings(QGroupBox * groupBox) _ui->source_checkBox_ignoreLandmarks->setChecked(true); _ui->source_checkBox_ignoreFeatures->setChecked(true); _ui->source_checkBox_ignorePriors->setChecked(false); + _ui->source_checkBox_ignoreIMU->setChecked(false); _ui->source_spinBox_databaseStartId->setValue(0); _ui->source_spinBox_databaseStopId->setValue(0); _ui->source_lineEdit_databaseCameraIndex->setText(""); _ui->source_checkBox_useDbStamps->setChecked(true); + _ui->source_checkBox_overrideLocalTransforms->setChecked(false); + _ui->source_lineEdit_databaseLocalTransformOffset->setText(""); #ifdef _WIN32 _ui->comboBox_cameraRGBD->setCurrentIndex(kSrcOpenNI2-kSrcRGBD); // openni2 @@ -2951,10 +2957,14 @@ void PreferencesDialog::readCameraSettings(const QString & filePath) _ui->source_checkBox_ignoreLandmarks->setChecked(settings.value("ignoreLandmarks", _ui->source_checkBox_ignoreLandmarks->isChecked()).toBool()); _ui->source_checkBox_ignoreFeatures->setChecked(settings.value("ignoreFeatures", _ui->source_checkBox_ignoreFeatures->isChecked()).toBool()); _ui->source_checkBox_ignorePriors->setChecked(settings.value("ignorePriors", _ui->source_checkBox_ignorePriors->isChecked()).toBool()); + _ui->source_checkBox_ignoreIMU->setChecked(settings.value("ignoreImu", _ui->source_checkBox_ignoreIMU->isChecked()).toBool()); + _ui->source_spinBox_databaseStartId->setValue(settings.value("startId", _ui->source_spinBox_databaseStartId->value()).toInt()); _ui->source_spinBox_databaseStopId->setValue(settings.value("stopId", _ui->source_spinBox_databaseStopId->value()).toInt()); _ui->source_lineEdit_databaseCameraIndex->setText(settings.value("cameraIndices", _ui->source_lineEdit_databaseCameraIndex->text()).toString()); _ui->source_checkBox_useDbStamps->setChecked(settings.value("useDatabaseStamps", _ui->source_checkBox_useDbStamps->isChecked()).toBool()); + _ui->source_checkBox_overrideLocalTransforms->setChecked(settings.value("overrideLocalTransforms", _ui->source_checkBox_overrideLocalTransforms->isChecked()).toBool()); + _ui->source_lineEdit_databaseLocalTransformOffset->setText(settings.value("localTransformOffsets", _ui->source_lineEdit_databaseLocalTransformOffset->text()).toString()); settings.endGroup(); // Database settings.endGroup(); // Camera @@ -3566,10 +3576,13 @@ void PreferencesDialog::writeCameraSettings(const QString & filePath) const settings.setValue("ignoreLandmarks", _ui->source_checkBox_ignoreLandmarks->isChecked()); settings.setValue("ignoreFeatures", _ui->source_checkBox_ignoreFeatures->isChecked()); settings.setValue("ignorePriors", _ui->source_checkBox_ignorePriors->isChecked()); + settings.setValue("ignoreImu", _ui->source_checkBox_ignoreIMU->isChecked()); settings.setValue("startId", _ui->source_spinBox_databaseStartId->value()); settings.setValue("stopId", _ui->source_spinBox_databaseStopId->value()); settings.setValue("cameraIndices", _ui->source_lineEdit_databaseCameraIndex->text()); settings.setValue("useDatabaseStamps", _ui->source_checkBox_useDbStamps->isChecked()); + settings.setValue("overrideLocalTransforms", _ui->source_checkBox_overrideLocalTransforms->isChecked()); + settings.setValue("localTransformOffsets", _ui->source_lineEdit_databaseLocalTransformOffset->text()); settings.endGroup(); // Database settings.endGroup(); // Camera @@ -7171,6 +7184,54 @@ Camera * PreferencesDialog::createCamera( } } } + + std::vector localTransformOverrides; + if(_ui->source_checkBox_overrideLocalTransforms->isChecked()) + { + if(!_ui->lineEdit_sourceLocalTransform->text().isEmpty()) + { + std::list transforms = uSplit(_ui->lineEdit_sourceLocalTransform->text().replace("PI_2", QString::number(3.141592/2.0)).toStdString(), ';'); + for(auto t: transforms) + { + localTransformOverrides.push_back(Transform::fromString(t)); + } + + // offset(s)? + if(!_ui->source_lineEdit_databaseLocalTransformOffset->text().isEmpty()) + { + std::vector localTransformOffsetOverrides; + std::list offsetStr = uSplit(_ui->source_lineEdit_databaseLocalTransformOffset->text().toStdString(), ' '); + for(std::list::iterator iter=offsetStr.begin(); iter!=offsetStr.end(); ++iter) + { + localTransformOffsetOverrides.push_back(uStr2Float(*iter)); + UINFO("Camera offset = %f", localTransformOffsetOverrides.back()); + } + if(!localTransformOffsetOverrides.empty()) + { + if(!localTransformOverrides.empty() && localTransformOffsetOverrides.size() > 1 && localTransformOffsetOverrides.size() != localTransformOverrides.size()) + { + QMessageBox::warning(this, tr("DBReader"), + tr( "Camera lens offset vector size (%1) is not equal to local transform overrides (%2). " + "Camera lens offset vector should be one to affect all cameras or the same size than local transforms overrides.").arg(localTransformOffsetOverrides.size()).arg(localTransformOverrides.size()), QMessageBox::Ok); + return 0; + } + else { + for(size_t i=0; isource_lineEdit_databaseLocalTransformOffset->text().isEmpty()) + { + UWARN("Overriding camera offsets can only be used when camera local transforms are overriden. Ignoring offsets :\"%s\"", + _ui->source_lineEdit_databaseLocalTransformOffset->text().toStdString().c_str()); + } + } camera = new DBReader(_ui->source_database_lineEdit_path->text().toStdString(), _ui->source_checkBox_useDbStamps->isChecked()?-1:this->getGeneralInputRate(), @@ -7185,7 +7246,9 @@ Camera * PreferencesDialog::createCamera( _ui->source_checkBox_ignoreFeatures->isChecked(), 0, -1, - _ui->source_checkBox_ignorePriors->isChecked()); + _ui->source_checkBox_ignorePriors->isChecked(), + _ui->source_checkBox_ignoreIMU->isChecked(), + localTransformOverrides); } else { diff --git a/guilib/src/ui/aboutDialog.ui b/guilib/src/ui/aboutDialog.ui index f35ced5d..ac562b74 100644 --- a/guilib/src/ui/aboutDialog.ui +++ b/guilib/src/ui/aboutDialog.ui @@ -778,7 +778,7 @@ p, li { white-space: pre-wrap; } - + With cuVSLAM : diff --git a/guilib/src/ui/preferencesDialog.ui b/guilib/src/ui/preferencesDialog.ui index 61a08cd4..39641972 100644 --- a/guilib/src/ui/preferencesDialog.ui +++ b/guilib/src/ui/preferencesDialog.ui @@ -7180,17 +7180,7 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki false - - - - 0 - - - 99999 - - - - + 0 @@ -7203,19 +7193,6 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - - - Ignore odometry saved in the database, so if RGB-D SLAM is activated, odometry will be recomputed. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - @@ -7223,60 +7200,13 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - - - Ignore goals saved in the database. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - + + - - - - Open database viewer - - - - - - - :/images/mag_glass.png:/images/mag_glass.png - - - - - - - - - - - - - - Ignore goal delay. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - @@ -7290,52 +7220,6 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - - - Use database stamps as input rate. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - Stop position (node ID) is the last node to process. If 0, all nodes after start position are published. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - Camera index. If the database contains multi-camera data, you can choose which camera to use. Leave empty to use all cameras. Can also be multiple indices split by spaces in a string like "0 2" to stream cameras 0 and 2 only. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - - - - @@ -7343,24 +7227,10 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - + + - - - - - - - - - - - - - - - Ignore landmarks. + Override camera local transform(s) with local transform(s) set above. For multi-cameras, use a ";" between each transform. true @@ -7370,13 +7240,44 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - + + - Start position (node ID). + - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + + + + + + + + + + Open database viewer + + + + + + + :/images/mag_glass.png:/images/mag_glass.png + + + + + + + @@ -7387,10 +7288,10 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - + + - If the database contains stereo data, generate disparity image and convert it to depth. The resulting output is a RGB-D image instead of stereo images. Dense disparity parameters can be found under StereoBM tab. + Ignore odometry saved in the database, so if RGB-D SLAM is activated, odometry will be recomputed. true @@ -7400,10 +7301,10 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - + + - Ignore priors. + Camera index. If the database contains multi-camera data, you can choose which camera to use. Leave empty to use all cameras. Can also be multiple indices split by spaces in a string like "0 2" to stream cameras 0 and 2 only. true @@ -7420,8 +7321,163 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - + + + + Add an y-axis offset before optical rotation on the overriden local transform(s). For multi-cameras,explicitly enumerate offsets if they are different (e.g., "0.05 0.075" for two cameras setup), or set single number to apply to all camera transforms. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + 0 + + + 99999 + + + + + + + Use database stamps as input rate. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + + + + + Start position (node ID). + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + + + + + Ignore priors. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Ignore landmarks. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Ignore goal delay. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + If the database contains stereo data, generate disparity image and convert it to depth. The resulting output is a RGB-D image instead of stereo images. Dense disparity parameters can be found under StereoBM tab. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Stop position (node ID) is the last node to process. If 0, all nodes after start position are published. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Ignore goals saved in the database. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Ignore IMU (i.e., gravity links). + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + diff --git a/tools/Reprocess/main.cpp b/tools/Reprocess/main.cpp index 867f6381..4006a036 100644 --- a/tools/Reprocess/main.cpp +++ b/tools/Reprocess/main.cpp @@ -93,6 +93,7 @@ void showUsage() " e.g., \"0.05 0.075\" for two cameras setup.\n" " -nolandmark Don't republish landmarks contained in input database.\n" " -nopriors Don't republish priors contained in input database.\n" + " -noimu Don't republish IMU contained in input database.\n" " -pub_loops Republish loop closures contained in input database.\n" " -loc_null On localization mode, reset localization pose to null and map correction to identity between sessions.\n" " -gt When reprocessing a single database, load its original optimized graph, then \n" @@ -272,6 +273,7 @@ int main(int argc, char * argv[]) int framesToSkip = 0; bool ignoreLandmarks = false; bool ignorePriors = false; + bool ignoreImu = false; bool republishLoopClosures = false; bool locNull = false; bool originalGraphAsGT = false; @@ -490,6 +492,11 @@ int main(int argc, char * argv[]) ignorePriors = true; printf("Ignoring priors from input database (-nopriors option).\n"); } + else if(strcmp(argv[i], "-noimu") == 0 || strcmp(argv[i], "--noimu") == 0) + { + ignoreImu = true; + printf("Ignoring IMU from input database (-noimu option).\n"); + } else if(strcmp(argv[i], "-pub_loops") == 0 || strcmp(argv[i], "--pub_loops") == 0) { republishLoopClosures = true; @@ -900,6 +907,7 @@ int main(int argc, char * argv[]) startMapId, stopMapId, ignorePriors, + ignoreImu, cameraLocalTransformOverrides); dbReader->init(); From 48f529b4c6d09f54fa47e9a9a813b9ba8194a654 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Mon, 19 Jan 2026 15:43:14 -0800 Subject: [PATCH 076/126] Cleanup OccupancyGrid's cellCount_ unused member (#1639) (#1640) --- .../rtabmap/core/global_map/OccupancyGrid.h | 1 - corelib/src/global_map/OccupancyGrid.cpp | 75 ------------------- 2 files changed, 76 deletions(-) diff --git a/corelib/include/rtabmap/core/global_map/OccupancyGrid.h b/corelib/include/rtabmap/core/global_map/OccupancyGrid.h index 915c3dc1..ddcd3a9c 100644 --- a/corelib/include/rtabmap/core/global_map/OccupancyGrid.h +++ b/corelib/include/rtabmap/core/global_map/OccupancyGrid.h @@ -57,7 +57,6 @@ protected: private: cv::Mat map_; cv::Mat mapInfo_; - std::map > cellCount_; // float minMapSize_; bool erode_; diff --git a/corelib/src/global_map/OccupancyGrid.cpp b/corelib/src/global_map/OccupancyGrid.cpp index 7bc0db0f..19c1aa4d 100644 --- a/corelib/src/global_map/OccupancyGrid.cpp +++ b/corelib/src/global_map/OccupancyGrid.cpp @@ -92,7 +92,6 @@ void OccupancyGrid::clear() { map_ = cv::Mat(); mapInfo_ = cv::Mat(); - cellCount_.clear(); GlobalMap::clear(); } @@ -433,11 +432,6 @@ void OccupancyGrid::assemble(const std::list > & newPo if(iter != emptyLocalMaps.end() || jter!=occupiedLocalMaps.end()) { addAssembledNode(kter->first, kter->second); - std::map >::iterator cter = cellCount_.find(kter->first); - if(cter == cellCount_.end() && kter->first > 0) - { - cter = cellCount_.insert(std::make_pair(kter->first, std::pair(0,0))).first; - } if(iter!=emptyLocalMaps.end()) { for(int i=0; isecond.cols; ++i) @@ -459,30 +453,12 @@ void OccupancyGrid::assemble(const std::list > & newPo // cannot rewrite on cells referred by more recent nodes continue; } - if(nodeId > 0) - { - std::map >::iterator eter = cellCount_.find(nodeId); - UASSERT_MSG(eter != cellCount_.end(), uFormat("current pose=%d nodeId=%d", kter->first, nodeId).c_str()); - if(value == 0) - { - eter->second.first -= 1; - } - else if(value == 100) - { - eter->second.second -= 1; - } - if(kter->first < 0) - { - eter->second.first += 1; - } - } } if(kter->first > 0) { info[0] = (float)kter->first; info[1] = ptf[0]; info[2] = ptf[1]; - cter->second.first+=1; } value = 0; // free space @@ -533,23 +509,6 @@ void OccupancyGrid::assemble(const std::list > & newPo // cannot rewrite on cells referred by more recent nodes continue; } - if(nodeId>0) - { - std::map >::iterator eter = cellCount_.find(nodeId); - UASSERT_MSG(eter != cellCount_.end(), uFormat("current pose=%d nodeId=%d", kter->first, nodeId).c_str()); - if(value == 0) - { - eter->second.first -= 1; - } - else if(value == 100) - { - eter->second.second -= 1; - } - if(kter->first < 0) - { - eter->second.first += 1; - } - } } if(kter->first > 0) { @@ -557,7 +516,6 @@ void OccupancyGrid::assemble(const std::list > & newPo info[1] = float(i) * cellSize_ + xMin; info[2] = float(j) * cellSize_ + yMin; info[3] = logOddsClampingMin_; - cter->second.first+=1; } value = -2; // free space (footprint) } @@ -585,30 +543,12 @@ void OccupancyGrid::assemble(const std::list > & newPo // cannot rewrite on cells referred by more recent nodes continue; } - if(nodeId>0) - { - std::map >::iterator eter = cellCount_.find(nodeId); - UASSERT_MSG(eter != cellCount_.end(), uFormat("current pose=%d nodeId=%d", kter->first, nodeId).c_str()); - if(value == 0) - { - eter->second.first -= 1; - } - else if(value == 100) - { - eter->second.second -= 1; - } - if(kter->first < 0) - { - eter->second.second += 1; - } - } } if(kter->first > 0) { info[0] = (float)kter->first; info[1] = ptf[0]; info[2] = ptf[1]; - cter->second.second+=1; } // update odds @@ -651,20 +591,6 @@ void OccupancyGrid::assemble(const std::list > & newPo mapInfo_ = mapInfo; minValues_[0] = xMin; minValues_[1] = yMin; - - // clean cellCount_ - for(std::map >::iterator iter= cellCount_.begin(); iter!=cellCount_.end();) - { - UASSERT(iter->second.first >= 0 && iter->second.second >= 0); - if(iter->second.first == 0 && iter->second.second == 0) - { - cellCount_.erase(iter++); - } - else - { - ++iter; - } - } } } @@ -677,7 +603,6 @@ unsigned long OccupancyGrid::getMemoryUsed() const memoryUsage += map_.total() * map_.elemSize(); memoryUsage += mapInfo_.total() * mapInfo_.elemSize(); - memoryUsage += cellCount_.size()*(sizeof(int)*3 + sizeof(std::pair) + sizeof(std::map >::iterator)) + sizeof(std::map >); return memoryUsage; } From da55168f60d40c8829ff0b6531c26c06ac376bff Mon Sep 17 00:00:00 2001 From: matlabbe Date: Thu, 22 Jan 2026 15:59:55 -0800 Subject: [PATCH 077/126] Read-only localization mode (#1643) * Read-only localization mode * Localization: don't update map's node timestamps if statistic after run are not saved * Updated Mem/LocalizationReadOnly description * Updated description of Kp/FlannIndexSaved --- CMakeLists.txt | 2 +- corelib/include/rtabmap/core/DBDriver.h | 4 +- .../include/rtabmap/core/DBDriverSqlite3.h | 2 +- corelib/include/rtabmap/core/Memory.h | 2 + corelib/include/rtabmap/core/Parameters.h | 3 +- corelib/src/DBDriver.cpp | 14 +- corelib/src/DBDriverSqlite3.cpp | 16 +- corelib/src/Memory.cpp | 35 +- corelib/src/Rtabmap.cpp | 5 + guilib/src/PreferencesDialog.cpp | 1 + guilib/src/ui/preferencesDialog.ui | 372 +++++++++--------- 11 files changed, 248 insertions(+), 208 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 47a8fbf8..3265f673 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,7 +20,7 @@ SET(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake_modules") ####################### SET(RTABMAP_MAJOR_VERSION 0) SET(RTABMAP_MINOR_VERSION 23) -SET(RTABMAP_PATCH_VERSION 3) +SET(RTABMAP_PATCH_VERSION 4) SET(RTABMAP_VERSION ${RTABMAP_MAJOR_VERSION}.${RTABMAP_MINOR_VERSION}.${RTABMAP_PATCH_VERSION}) diff --git a/corelib/include/rtabmap/core/DBDriver.h b/corelib/include/rtabmap/core/DBDriver.h index 54fe0a06..847caa50 100644 --- a/corelib/include/rtabmap/core/DBDriver.h +++ b/corelib/include/rtabmap/core/DBDriver.h @@ -134,7 +134,7 @@ public: public: // Mutex-protected methods of abstract versions below - bool openConnection(const std::string & url, bool overwritten = false); + bool openConnection(const std::string & url, bool overwritten = false, bool readOnly = false); void closeConnection(bool save = true, const std::string & outputUrl = ""); bool isConnected() const; unsigned long getMemoryUsed() const; // In bytes @@ -193,7 +193,7 @@ public: protected: DBDriver(const ParametersMap & parameters = ParametersMap()); - virtual bool connectDatabaseQuery(const std::string & url, bool overwritten = false) = 0; + virtual bool connectDatabaseQuery(const std::string & url, bool overwritten = false, bool readOnly = false) = 0; virtual void disconnectDatabaseQuery(bool save = true, const std::string & outputUrl = "") = 0; virtual bool isConnectedQuery() const = 0; virtual unsigned long getMemoryUsedQuery() const = 0; // In bytes diff --git a/corelib/include/rtabmap/core/DBDriverSqlite3.h b/corelib/include/rtabmap/core/DBDriverSqlite3.h index 052ffb8e..a5df8e92 100644 --- a/corelib/include/rtabmap/core/DBDriverSqlite3.h +++ b/corelib/include/rtabmap/core/DBDriverSqlite3.h @@ -51,7 +51,7 @@ public: void setTempStore(int tempStore); protected: - virtual bool connectDatabaseQuery(const std::string & url, bool overwritten = false); + virtual bool connectDatabaseQuery(const std::string & url, bool overwritten = false, bool readOnly = false); virtual void disconnectDatabaseQuery(bool save = true, const std::string & outputUrl = ""); virtual bool isConnectedQuery() const; virtual unsigned long getMemoryUsedQuery() const; // In bytes diff --git a/corelib/include/rtabmap/core/Memory.h b/corelib/include/rtabmap/core/Memory.h index e3b24677..5de7c741 100644 --- a/corelib/include/rtabmap/core/Memory.h +++ b/corelib/include/rtabmap/core/Memory.h @@ -211,6 +211,7 @@ public: std::set getAllSignatureIds(bool ignoreChildren = true) const; bool memoryChanged() const {return _memoryChanged;} bool isIncremental() const {return _incrementalMemory;} + bool isReadOnly() const {return !_incrementalMemory && _localizationReadOnly;} bool isLocalizationDataSaved() const {return _localizationDataSaved;} const Signature * getSignature(int id) const; bool isInSTM(int signatureId) const {return _stMem.find(signatureId) != _stMem.end();} @@ -307,6 +308,7 @@ private: std::string _rgbCompressionFormat; std::string _depthCompressionFormat; bool _incrementalMemory; + bool _localizationReadOnly; bool _localizationDataSaved; bool _flannIndexSaved; bool _reduceGraph; diff --git a/corelib/include/rtabmap/core/Parameters.h b/corelib/include/rtabmap/core/Parameters.h index 729d61eb..3436f79f 100644 --- a/corelib/include/rtabmap/core/Parameters.h +++ b/corelib/include/rtabmap/core/Parameters.h @@ -213,6 +213,7 @@ class RTABMAP_CORE_EXPORT Parameters RTABMAP_PARAM_STR(Mem, DepthCompressionFormat, ".rvl", "Depth image compression format for 16UC1 depth type. It should be \".png\" or \".rvl\". If depth type is 32FC1, \".png\" is used."); RTABMAP_PARAM(Mem, STMSize, unsigned int, 10, "Short-term memory size."); RTABMAP_PARAM(Mem, IncrementalMemory, bool, true, "SLAM mode, otherwise it is Localization mode."); + RTABMAP_PARAM(Mem, LocalizationReadOnly, bool, false, uFormat("In localization mode, open the database in read-only mode (ignored if %s=true). Currrenty incompatible with memory management (%s and %s cannot be used) and if there are disjoint sessions in working memory. Last localization pose won't be saved back in the database at the end of the session, so the robot will always restart to original last localization pose, unless %s is used or an external initial pose is provided on initialization.", kMemIncrementalMemory().c_str(), kRtabmapLoopThr().c_str(), kRtabmapMemoryThr().c_str(), kRGBDStartAtOrigin().c_str()).c_str()); RTABMAP_PARAM(Mem, LocalizationDataSaved, bool, false, uFormat("Save localization data during localization session (when %s=false). When enabled, the database will then also grow in localization mode. This mode would be used only for debugging purpose.", kMemIncrementalMemory().c_str()).c_str()); RTABMAP_PARAM(Mem, ReduceGraph, bool, false, uFormat("Reduce graph. Merge nodes when loop closures are added (ignoring those with user data). Note that this approach assumes that 100%% of the loop closures accepted are good, so it is highly recommended to enable \"%s\" at the same time.", kRGBDOptimizeMaxError().c_str())); RTABMAP_PARAM(Mem, RecentWmRatio, float, 0.2, "Ratio of locations after the last loop closure in WM that cannot be transferred."); @@ -261,7 +262,7 @@ class RTABMAP_CORE_EXPORT Parameters RTABMAP_PARAM_STR(Kp, RoiRatios, "0.0 0.0 0.0 0.0", "Region of interest ratios [left, right, top, bottom]."); RTABMAP_PARAM_STR(Kp, DictionaryPath, "", "Path of the pre-computed dictionary"); RTABMAP_PARAM(Kp, NewWordsComparedTogether, bool, true, "When adding new words to dictionary, they are compared also with each other (to detect same words in the same signature)."); - RTABMAP_PARAM(Kp, FlannIndexSaved, bool, false, uFormat("Save FLANN index during localization session (when %s=false). The FLANN index will be saved to database after the first time localization mode is used, then on next sessions, the index is reloaded from the database instead of being rebuilt again. This can save significant loading time when the visual word dictionary is big (>1M words). Note that if the dictionary is modified (parameters or data), the index will be rebuilt and saved again on the next session.", kMemIncrementalMemory().c_str()).c_str()); + RTABMAP_PARAM(Kp, FlannIndexSaved, bool, false, uFormat("Save FLANN index during localization session (when %s=false). The FLANN index will be saved to database after the first time localization mode is used, then on next sessions, the index is reloaded from the database instead of being rebuilt again. This can save significant loading time when the visual word dictionary is big (>1M words). Note that if the dictionary is modified (parameters or data), the index will be rebuilt and saved again on the next session. Ignored on initialization if %s is enabled.", kMemIncrementalMemory().c_str(), kMemInitWMWithAllNodes().c_str()).c_str()); RTABMAP_PARAM(Kp, SerializeWithChecksum, bool, true, "On serialization of the FLANN index, compute checksum of the data used by the FLANN index. This adds a slight overhead on serialization/deserialization to make sure that the dictionary data correspond to same data used when the index was built."); RTABMAP_PARAM(Kp, SubPixWinSize, int, 3, "See cv::cornerSubPix()."); RTABMAP_PARAM(Kp, SubPixIterations, int, 0, "See cv::cornerSubPix(). 0 disables sub pixel refining."); diff --git a/corelib/src/DBDriver.cpp b/corelib/src/DBDriver.cpp index 5e645584..d71c1400 100644 --- a/corelib/src/DBDriver.cpp +++ b/corelib/src/DBDriver.cpp @@ -45,7 +45,7 @@ DBDriver * DBDriver::create(const ParametersMap & parameters) DBDriver::DBDriver(const ParametersMap & parameters) : _emptyTrashesTime(0), - _timestampUpdate(true) + _timestampUpdate(false) { this->parseParameters(parameters); } @@ -73,7 +73,15 @@ void DBDriver::closeConnection(bool save, const std::string & outputUrl) else { _trashesMutex.lock(); + for(auto & iter: _trashSignatures) + { + delete iter.second; + } _trashSignatures.clear(); + for(auto & iter: _trashVisualWords) + { + delete iter.second; + } _trashVisualWords.clear(); _trashesMutex.unlock(); } @@ -83,12 +91,12 @@ void DBDriver::closeConnection(bool save, const std::string & outputUrl) UDEBUG(""); } -bool DBDriver::openConnection(const std::string & url, bool overwritten) +bool DBDriver::openConnection(const std::string & url, bool overwritten, bool readOnly) { UDEBUG(""); _url = url; _dbSafeAccessMutex.lock(); - if(this->connectDatabaseQuery(url, overwritten)) + if(this->connectDatabaseQuery(url, overwritten, readOnly)) { _dbSafeAccessMutex.unlock(); return true; diff --git a/corelib/src/DBDriverSqlite3.cpp b/corelib/src/DBDriverSqlite3.cpp index 7c250fa0..d2118f9b 100644 --- a/corelib/src/DBDriverSqlite3.cpp +++ b/corelib/src/DBDriverSqlite3.cpp @@ -320,7 +320,7 @@ bool DBDriverSqlite3::getDatabaseVersionQuery(std::string & version) const return false; } -bool DBDriverSqlite3::connectDatabaseQuery(const std::string & url, bool overwritten) +bool DBDriverSqlite3::connectDatabaseQuery(const std::string & url, bool overwritten, bool readOnly) { this->disconnectDatabaseQuery(); // Open a database connection @@ -332,7 +332,7 @@ bool DBDriverSqlite3::connectDatabaseQuery(const std::string & url, bool overwri if(!url.empty()) { dbFileExist = UFile::exists(url.c_str()); - if(dbFileExist && overwritten) + if(dbFileExist && overwritten && !readOnly) { UINFO("Deleting database %s...", url.c_str()); UASSERT(UFile::erase(url.c_str()) == 0); @@ -354,12 +354,12 @@ bool DBDriverSqlite3::connectDatabaseQuery(const std::string & url, bool overwri { ULOGGER_INFO("Using empty database in the memory."); } - rc = sqlite3_open_v2(":memory:", &_ppDb, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, 0); + rc = sqlite3_open_v2(":memory:", &_ppDb, readOnly ? SQLITE_OPEN_READONLY : SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, 0); } else { ULOGGER_INFO("Using database \"%s\" from the hard drive.", url.c_str()); - rc = sqlite3_open_v2(url.c_str(), &_ppDb, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, 0); + rc = sqlite3_open_v2(url.c_str(), &_ppDb, readOnly ? SQLITE_OPEN_READONLY : SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, 0); } if(rc != SQLITE_OK) { @@ -4343,7 +4343,7 @@ void DBDriverSqlite3::loadLinksQuery(std::list & signatures) const void DBDriverSqlite3::updateQuery(const std::list & nodes, bool updateTimestamp) const { - UDEBUG("nodes = %d", nodes.size()); + UDEBUG("nodes = %d, updateTimestamp = %s", nodes.size(), updateTimestamp?"true":"false"); if(_ppDb && nodes.size()) { UTimer timer; @@ -4382,7 +4382,7 @@ void DBDriverSqlite3::updateQuery(const std::list & nodes, bool upd { s = *i; int index = 1; - if(s) + if(s && (s->isModified() || updateTimestamp)) { rc = sqlite3_bind_int(ppStmt, index++, s->getWeight()); UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); @@ -4406,7 +4406,7 @@ void DBDriverSqlite3::updateQuery(const std::list & nodes, bool upd //step rc=sqlite3_step(ppStmt); - UASSERT_MSG(rc == SQLITE_DONE, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); + UASSERT_MSG(rc == SQLITE_DONE, uFormat("DB error (%s): %s (node id = %d map=%d)", _version.c_str(), sqlite3_errmsg(_ppDb), s->id(), s->mapId()).c_str()); rc = sqlite3_reset(ppStmt); UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); @@ -5751,7 +5751,7 @@ cv::Mat DBDriverSqlite3::loadOptimizedMeshQuery( void DBDriverSqlite3::saveFlannIndexQuery(const std::vector & data) const { - UDEBUG(""); + UDEBUG("data size = %ld bytes", data.size()); if(_ppDb && uStrNumCmp(_version, "0.23.0") >= 0) { UTimer timer; diff --git a/corelib/src/Memory.cpp b/corelib/src/Memory.cpp index a7a1c41b..6d5aa8ad 100644 --- a/corelib/src/Memory.cpp +++ b/corelib/src/Memory.cpp @@ -83,6 +83,7 @@ Memory::Memory(const ParametersMap & parameters) : _rgbCompressionFormat(Parameters::defaultMemImageCompressionFormat()), _depthCompressionFormat(Parameters::defaultMemDepthCompressionFormat()), _incrementalMemory(Parameters::defaultMemIncrementalMemory()), + _localizationReadOnly(Parameters::defaultMemLocalizationReadOnly()), _localizationDataSaved(Parameters::defaultMemLocalizationDataSaved()), _flannIndexSaved(Parameters::defaultKpFlannIndexSaved()), _reduceGraph(Parameters::defaultMemReduceGraph()), @@ -185,10 +186,6 @@ bool Memory::init(const std::string & dbUrl, bool dbOverwritten, const Parameter _dbDriver = 0; // HACK for the clear() below to think that there is no db } } - else if(!_memoryChanged && _linksChanged) - { - _dbDriver->setTimestampUpdateEnabled(false); // update links only - } this->clear(); if(postInitClosingEvents) UEventsManager::post(new RtabmapEventInit("Clearing memory, done!")); @@ -212,10 +209,10 @@ bool Memory::init(const std::string & dbUrl, bool dbOverwritten, const Parameter bool success = true; if(_dbDriver) { - _dbDriver->setTimestampUpdateEnabled(true); // make sure that timestamp update is enabled (may be disabled above) + success = false; if(postInitClosingEvents) UEventsManager::post(new RtabmapEventInit(std::string("Connecting to database \"") + dbUrl + "\"...")); - if(_dbDriver->openConnection(dbUrl, dbOverwritten)) + if(_dbDriver->openConnection(dbUrl, dbOverwritten, isReadOnly())) { success = true; if(postInitClosingEvents) UEventsManager::post(new RtabmapEventInit(std::string("Connecting to database \"") + dbUrl + "\", done!")); @@ -245,6 +242,7 @@ void Memory::loadDataFromDb(bool postInitClosingEvents) if(loadAllNodesInWM) { + UDEBUG("Loading all nodes to WM..."); if(postInitClosingEvents) UEventsManager::post(new RtabmapEventInit(std::string("Loading all nodes to WM..."))); std::set ids; _dbDriver->getAllNodeIds(ids, true); @@ -252,6 +250,7 @@ void Memory::loadDataFromDb(bool postInitClosingEvents) } else { + UDEBUG("Loading last nodes to WM..."); // load previous session working memory if(postInitClosingEvents) UEventsManager::post(new RtabmapEventInit(std::string("Loading last nodes to WM..."))); _dbDriver->loadLastNodes(dbSignatures, !_loadVisualLocalFeaturesOnInit); @@ -531,16 +530,23 @@ void Memory::close(bool databaseSaved, bool postInitClosingEvents, const std::st UDEBUG("_memoryChanged=%d _linksChanged=%d databaseNameChanged=%d", _memoryChanged?1:0, _linksChanged?1:0, databaseNameChanged?1:0); - if(!databaseSaved || (!_memoryChanged && !_linksChanged && !databaseNameChanged)) + if(!databaseSaved || (!_memoryChanged && !_linksChanged && !databaseNameChanged) || this->isReadOnly()) { if(postInitClosingEvents) UEventsManager::post(new RtabmapEventInit(uFormat("No changes added to database."))); UINFO("No changes added to database."); if(_dbDriver) { - saveFlannIndex(postInitClosingEvents); + if(!this->isReadOnly()) { + saveFlannIndex(postInitClosingEvents); + } + else if(_memoryChanged || _linksChanged || databaseNameChanged) + { + UWARN("Memory has been modified (nodes=%s links=%s name=%s) but the database is read-only, changes are not saved to database.", + _memoryChanged?"true":"false", _linksChanged?"true":"false", databaseNameChanged?"true":"false"); + } if(postInitClosingEvents) UEventsManager::post(new RtabmapEventInit(uFormat("Closing database \"%s\"...", _dbDriver->getUrl().c_str()))); - _dbDriver->closeConnection(false, ouputDatabasePath); + _dbDriver->closeConnection(false); delete _dbDriver; _dbDriver = 0; if(postInitClosingEvents) UEventsManager::post(new RtabmapEventInit("Closing database, done!")); @@ -556,12 +562,6 @@ void Memory::close(bool databaseSaved, bool postInitClosingEvents, const std::st if(!_memoryChanged && _dbDriver) { saveFlannIndex(postInitClosingEvents); - - if(_linksChanged) { - // don't update the time stamps! - UDEBUG(""); - _dbDriver->setTimestampUpdateEnabled(false); - } } this->clear(); if(_dbDriver) @@ -663,6 +663,7 @@ void Memory::parseParameters(const ParametersMap & parameters) Parameters::parse(params, Parameters::kMarkerVarianceOrientationIgnored(), _markerOrientationIgnored); Parameters::parse(params, Parameters::kMemLocalizationDataSaved(), _localizationDataSaved); Parameters::parse(params, Parameters::kKpFlannIndexSaved(), _flannIndexSaved); + Parameters::parse(params, Parameters::kMemLocalizationReadOnly(), _localizationReadOnly); if(_markerAngVariance>=9999) { @@ -1865,6 +1866,7 @@ void Memory::clear() uInsert(parameters, parameters_); parameters.erase(Parameters::kRtabmapWorkingDirectory()); // don't save working directory as it is machine dependent UDEBUG(""); + _dbDriver->setTimestampUpdateEnabled(true); // Only re-stamp if we updated the memory _dbDriver->addInfoAfterRun(memSize, _lastSignature?_lastSignature->id():0, UProcessInfo::getMemoryUsage(), @@ -1938,6 +1940,7 @@ void Memory::clear() _dbDriver->join(true); cleanUnusedWords(); _dbDriver->emptyTrashes(); + _dbDriver->setTimestampUpdateEnabled(false); } _vwd->clear(_dbDriver!=NULL); UDEBUG(""); @@ -3626,7 +3629,7 @@ void Memory::updateLink(const Link & link, bool updateInDatabase) if(oldType!=Link::kVirtualClosure || link.type()!=Link::kVirtualClosure) { - _linksChanged = true; + _linksChanged = _incrementalMemory || (fromS->isSaved() && toS->isSaved()); } } else diff --git a/corelib/src/Rtabmap.cpp b/corelib/src/Rtabmap.cpp index 96146792..7b6bab1c 100644 --- a/corelib/src/Rtabmap.cpp +++ b/corelib/src/Rtabmap.cpp @@ -507,6 +507,11 @@ void Rtabmap::close(bool databaseSaved, const std::string & ouputDatabasePath) } if(_memory) { + if(_memory->isReadOnly() && databaseSaved) + { + UWARN("Database is read-only, latest optimized poses, latest localization pose and latest state of the memory are not saved."); + databaseSaved = false; + } if(databaseSaved) { if(_memory->isGraphReduced() && _memory->isIncremental()) diff --git a/guilib/src/PreferencesDialog.cpp b/guilib/src/PreferencesDialog.cpp index 68a2b5bf..96f5331b 100644 --- a/guilib/src/PreferencesDialog.cpp +++ b/guilib/src/PreferencesDialog.cpp @@ -1070,6 +1070,7 @@ PreferencesDialog::PreferencesDialog(QWidget * parent) : // Database _ui->checkBox_dbInMemory->setObjectName(Parameters::kDbSqlite3InMemory().c_str()); + _ui->checkBox_dbReadOnly->setObjectName(Parameters::kMemLocalizationReadOnly().c_str()); _ui->spinBox_dbCacheSize->setObjectName(Parameters::kDbSqlite3CacheSize().c_str()); _ui->comboBox_dbJournalMode->setObjectName(Parameters::kDbSqlite3JournalMode().c_str()); _ui->comboBox_dbSynchronous->setObjectName(Parameters::kDbSqlite3Synchronous().c_str()); diff --git a/guilib/src/ui/preferencesDialog.ui b/guilib/src/ui/preferencesDialog.ui index 39641972..47c11627 100644 --- a/guilib/src/ui/preferencesDialog.ui +++ b/guilib/src/ui/preferencesDialog.ui @@ -63,7 +63,7 @@ 0 - -747 + 0 684 5110 @@ -95,7 +95,7 @@ QFrame::Raised - 5 + 10 @@ -12114,6 +12114,47 @@ When set to false, no new words are added to dictionary, so no more updates are Database + + + + Sqlite3 journal mode, +see Sqlite3 doc 'PRAGMA journal_mode'. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Sqlite3 cache size, +see Sqlite3 doc 'PRAGMA cache_size'. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Target database version for backward compatibility purpose. Only Major and minor versions are used and should be set (e.g., "0.19" vs "0.20" or "1.0" vs "2.0"). Patch version is ignored (e.g., "0.20.1" and "0.20.3" will generate a "0.20" database). + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + @@ -12124,6 +12165,146 @@ When set to false, no new words are added to dictionary, so no more updates are + + + + Using database in the memory instead of a file on the hard disk (this greatly improve database access performance but it requires more RAM memory). + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Sqlite3 synchronous, +see Sqlite3 doc 'PRAGMA synchronous'. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + true + + + + + + + 10 + + + 999999 + + + 100 + + + 2000 + + + + + + + 2 + + + + OFF + + + + + NORMAL + + + + + FULL + + + + + + + + + DEFAULT + + + + + FILE + + + + + MEMORY + + + + + + + + Depth image compression format (should be ".png" or ".rvl"). + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Keep not linked nodes in db (rehearsed nodes and deleted nodes are saved). + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + major.minor + + + + + + + RGB image compression format (should be ".jpg" or ".png"). + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + @@ -12137,14 +12318,7 @@ When set to false, no new words are added to dictionary, so no more updates are - - - - - - - - + @@ -12173,20 +12347,7 @@ When set to false, no new words are added to dictionary, so no more updates are - - - - Using database in the memory instead of a file on the hard disk (this greatly improve database access performance but it requires more RAM memory). - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - + Sqlite3 temp store, @@ -12200,132 +12361,23 @@ see Sqlite3 doc 'PRAGMA temp_store'. - - - - - DEFAULT - - - - - FILE - - - - - MEMORY - - - + + - - + + + + + - - true - - - - - - - 10 - - - 999999 - - - 100 - - - 2000 - - - - - - - Sqlite3 cache size, -see Sqlite3 doc 'PRAGMA cache_size'. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - Sqlite3 journal mode, -see Sqlite3 doc 'PRAGMA journal_mode'. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - Sqlite3 synchronous, -see Sqlite3 doc 'PRAGMA synchronous'. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - 2 - - - - OFF - - - - - NORMAL - - - - - FULL - - - - - - - - Keep not linked nodes in db (rehearsed nodes and deleted nodes are saved). - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - + - RGB image compression format (should be ".jpg" or ".png"). + In localization mode, open the database in read-only mode (ignored in SLAM mode). Currrenty incompatible with memory management (i.e., Time and/or Memory thresholds cannot be used) and if there are disjoint sessions in working memory. Last localization pose won't be saved back in the database at the end of the session, so the robot will always restart to original last localization pose, unless RGBD/StartAtOrigin is used or an external initial pose is provided on initialization. true @@ -12336,41 +12388,9 @@ see Sqlite3 doc 'PRAGMA synchronous'. - - - - + - Depth image compression format (should be ".png" or ".rvl"). - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - - - - Target database version for backward compatibility purpose. Only Major and minor versions are used and should be set (e.g., "0.19" vs "0.20" or "1.0" vs "2.0"). Patch version is ignored (e.g., "0.20.1" and "0.20.3" will generate a "0.20" database). - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - major.minor + From e6bc39028aa8e353fb6d5142cebf2f66a8408e86 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Thu, 22 Jan 2026 17:42:04 -0800 Subject: [PATCH 078/126] Fixed RGBD/StartAtOrigin using landmark position --- corelib/src/Rtabmap.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/corelib/src/Rtabmap.cpp b/corelib/src/Rtabmap.cpp index 7b6bab1c..939a624d 100644 --- a/corelib/src/Rtabmap.cpp +++ b/corelib/src/Rtabmap.cpp @@ -385,19 +385,19 @@ void Rtabmap::init(const ParametersMap & parameters, const std::string & databas !_optimizeFromGraphEnd?_memory->getWorkingMem().lower_bound(1)->first:_memory->getWorkingMem().rbegin()->first, false, _optimizedPoses, cov, &_constraints); } - if(!_optimizedPoses.empty()) + if(_optimizedPoses.lower_bound(1) != _optimizedPoses.end()) { if(_restartAtOrigin) { - UWARN("last localization pose is ignored (%s=true), assuming we start at the origin of the map.", Parameters::kRGBDStartAtOrigin().c_str()); - lastPose = _optimizedPoses.begin()->second; + UWARN("last localization pose is ignored (%s=true), assuming we start at the first node of the map.", Parameters::kRGBDStartAtOrigin().c_str()); + lastPose = _optimizedPoses.lower_bound(1)->second; } _lastLocalizationPose = lastPose; UINFO("Loaded optimizedPoses=%d firstPose %d=%s lastLocalizationPose=%s", _optimizedPoses.size(), - _optimizedPoses.begin()->first, - _optimizedPoses.begin()->second.prettyPrint().c_str(), + _optimizedPoses.lower_bound(1)->first, + _optimizedPoses.lower_bound(1)->second.prettyPrint().c_str(), _lastLocalizationPose.prettyPrint().c_str()); if(_constraints.empty()) @@ -411,7 +411,7 @@ void Rtabmap::init(const ParametersMap & parameters, const std::string & databas UTimer time; std::map likelihood; likelihood.insert(std::make_pair(Memory::kIdVirtual, 1)); - for(std::map::iterator iter=_optimizedPoses.begin(); iter!=_optimizedPoses.end(); ++iter) + for(std::map::iterator iter=_optimizedPoses.lower_bound(1); iter!=_optimizedPoses.end(); ++iter) { if(_memory->getSignature(iter->first)) { From 040de14ec86cee11ce60e1755d5069a0be4ad0ad Mon Sep 17 00:00:00 2001 From: Borong Yuan Date: Tue, 27 Jan 2026 05:27:00 +0800 Subject: [PATCH 079/126] fix another typo for ceres optimizer (#1645) --- corelib/src/optimizer/ceres/pose_graph_2d/angle_manifold.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/corelib/src/optimizer/ceres/pose_graph_2d/angle_manifold.h b/corelib/src/optimizer/ceres/pose_graph_2d/angle_manifold.h index 2f551038..5a3d1e28 100644 --- a/corelib/src/optimizer/ceres/pose_graph_2d/angle_manifold.h +++ b/corelib/src/optimizer/ceres/pose_graph_2d/angle_manifold.h @@ -77,7 +77,7 @@ class AngleManifold { #else -class AngleManfold { +class AngleManifold { public: template @@ -90,7 +90,7 @@ class AngleManfold { } static ceres::LocalParameterization* Create() { - return (new ceres::AutoDiffLocalParameterization); + return (new ceres::AutoDiffLocalParameterization); } }; From e23c38f471a9edf3172edb85f527b773f5cfc39f Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sat, 31 Jan 2026 13:33:23 -0800 Subject: [PATCH 080/126] DataRecorder: fixed record in RAM option. --- guilib/src/DataRecorder.cpp | 10 ++++++---- tools/DataRecorder/main.cpp | 12 ++++++++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/guilib/src/DataRecorder.cpp b/guilib/src/DataRecorder.cpp index 31baa28d..ebbac03d 100644 --- a/guilib/src/DataRecorder.cpp +++ b/guilib/src/DataRecorder.cpp @@ -72,13 +72,15 @@ bool DataRecorder::init(const QString & path, bool recordInRAM) ParametersMap customParameters; customParameters.insert(ParametersPair(Parameters::kMemRehearsalSimilarity(), "1.0")); // deactivate rehearsal customParameters.insert(ParametersPair(Parameters::kKpMaxFeatures(), "-1")); // deactivate keypoints extraction - customParameters.insert(ParametersPair(Parameters::kMemBinDataKept(), "true")); // to keep images + customParameters.insert(ParametersPair(Parameters::kMemBinDataKept(), "true")); // make sure we keep images customParameters.insert(ParametersPair(Parameters::kMemMapLabelsAdded(), "false")); // don't create map labels + customParameters.insert(ParametersPair(Parameters::kRGBDCreateOccupancyGrid(), "false")); // make sure we don't create local grids customParameters.insert(ParametersPair(Parameters::kMemBadSignaturesIgnored(), "true")); // make sure memory cleanup is done - customParameters.insert(ParametersPair(Parameters::kMemIntermediateNodeDataKept(), "true")); - if(!recordInRAM) + customParameters.insert(ParametersPair(Parameters::kMemNotLinkedNodesKept(), "true")); // make sure we save every frame + + if(recordInRAM) { - customParameters.insert(ParametersPair(Parameters::kDbSqlite3InMemory(), "false")); + customParameters.insert(ParametersPair(Parameters::kDbSqlite3InMemory(), "true")); } memory_ = new Memory(); if(!memory_->init(path.toStdString(), true, customParameters)) diff --git a/tools/DataRecorder/main.cpp b/tools/DataRecorder/main.cpp index ced1611c..7a4a4ba4 100644 --- a/tools/DataRecorder/main.cpp +++ b/tools/DataRecorder/main.cpp @@ -52,7 +52,8 @@ void showUsage() " after modifying Source settings.\n" "Options:\n" " -debug Show debug log.\n" - " -hide Don't display the current cloud recorded.\n"); + " -hide Don't display the current cloud recorded.\n" + " -ram Record all frames in RAM before dumping on hard-drive on exit.\n"); exit(1); } @@ -105,6 +106,7 @@ int main (int argc, char * argv[]) // parse arguments std::string fileName; bool show = true; + bool recordInRAM = false; std::string configFile; if(argc < 3) @@ -123,6 +125,11 @@ int main (int argc, char * argv[]) show = false; continue; } + if(strcmp(argv[i], "-ram") == 0) + { + recordInRAM = true; + continue; + } printf("Unrecognized option : %s\n", argv[i]); showUsage(); } @@ -139,6 +146,7 @@ int main (int argc, char * argv[]) UINFO("Output = %s", fileName.c_str()); UINFO("Show = %s", show?"true":"false"); + UINFO("RecordInRAM = %s", recordInRAM?"true":"false"); UINFO("Config = %s", configFile.c_str()); app = new QApplication(argc, argv); @@ -207,7 +215,7 @@ int main (int argc, char * argv[]) DataRecorder recorder; - if(recorder.init(fileName.c_str())) + if(recorder.init(fileName.c_str(), recordInRAM)) { recorder.registerToEventsManager(); if(show) From b47c4093a6014ae2ae897b0fcc6af0cc45e036b1 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Thu, 5 Feb 2026 15:45:29 -0800 Subject: [PATCH 081/126] CudaSIFT: using abs(sharpness) instead of sharpness as response value (this slightly change how most significant features are retained with Vis/MaxFeatures or Kp/MaxFeatures). OptimizerTORO: changed a fatal log to error log when loading a link with not existing poses. --- corelib/src/Features2d.cpp | 4 ++-- corelib/src/RegistrationVis.cpp | 2 ++ corelib/src/optimizer/OptimizerTORO.cpp | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/corelib/src/Features2d.cpp b/corelib/src/Features2d.cpp index 41b5a12a..e3fc45fd 100644 --- a/corelib/src/Features2d.cpp +++ b/corelib/src/Features2d.cpp @@ -1414,7 +1414,7 @@ std::vector SIFT::generateKeypointsImpl(const cv::Mat & image, con } //Keep track of the data, to be easier to manage the data in the next step - hessianMap.insert(std::pair(cudaSiftData_->h_data[i].sharpness, i)); + hessianMap.insert(std::pair(abs(cudaSiftData_->h_data[i].sharpness), i)); } if((int)hessianMap.size() < maxKeypoints) @@ -1434,7 +1434,7 @@ std::vector SIFT::generateKeypointsImpl(const cv::Mat & image, con keypoints[k].pt.y = cudaSiftData_->h_data[i].ypos; keypoints[k].size = 2.0f*cudaSiftData_->h_data[i].scale; // x2 because the scale is more like a radius than a diameter, see CudaSift's ExtractSiftDescriptors function to see how they convert scale to patch size keypoints[k].angle = cudaSiftData_->h_data[i].orientation; - keypoints[k].response = cudaSiftData_->h_data[i].sharpness; + keypoints[k].response = abs(cudaSiftData_->h_data[i].sharpness); keypoints[k].octave = log2(cudaSiftData_->h_data[i].subsampling)-(upscale_?1:0); } } diff --git a/corelib/src/RegistrationVis.cpp b/corelib/src/RegistrationVis.cpp index 7531b672..d1f999f0 100644 --- a/corelib/src/RegistrationVis.cpp +++ b/corelib/src/RegistrationVis.cpp @@ -2184,6 +2184,8 @@ Transform RegistrationVis::computeTransformationImpl( // We take the second eigen value info.inliersDistribution = pca_analysis.eigenvalues.at(0, 1); + UDEBUG("Visual distribution: %f (eigen values = %f %f)", info.inliersDistribution, pca_analysis.eigenvalues.at(0, 0), pca_analysis.eigenvalues.at(0, 1)); + if(info.inliersDistribution < _minInliersDistributionThr) { msg = uFormat("The distribution (%f) of inliers is under %s threshold (%f)", diff --git a/corelib/src/optimizer/OptimizerTORO.cpp b/corelib/src/optimizer/OptimizerTORO.cpp index 175f616d..1ac00398 100644 --- a/corelib/src/optimizer/OptimizerTORO.cpp +++ b/corelib/src/optimizer/OptimizerTORO.cpp @@ -543,7 +543,7 @@ bool OptimizerTORO::loadGraph( } else { - UFATAL("Referred poses from the link not exist!"); + UERROR("Referred poses from the link (%d->%d) don't exist! Link ignored!", idFrom, idTo); } } else if(strList.size()) From b3b8fbddbea5aacdc59e596cf7008960450d5c6a Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 8 Feb 2026 16:23:55 -0800 Subject: [PATCH 082/126] export: fixed exported NaN points when voxel filter is not used --- tools/Export/main.cpp | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/tools/Export/main.cpp b/tools/Export/main.cpp index 48575636..18c79d22 100644 --- a/tools/Export/main.cpp +++ b/tools/Export/main.cpp @@ -1691,15 +1691,24 @@ int main(int argc, char * argv[]) { if(voxelSize>0.0f) { - if(cloud.get() && !cloud->empty()) + if(cloud.get() && !cloud->empty()) { cloud = rtabmap::util3d::voxelize(cloud, indices, voxelSize); - else if(cloudI.get() && !cloudI->empty()) + if(!cloud->empty()) + cloud = rtabmap::util3d::transformPointCloud(cloud, iter->second); + } + else if(cloudI.get() && !cloudI->empty()) { cloudI = rtabmap::util3d::voxelize(cloudI, indices, voxelSize); + if(!cloudI->empty()) + cloudI = rtabmap::util3d::transformPointCloud(cloudI, iter->second); + } + } + else + { + if(cloud.get() && !cloud->empty()) + cloud = rtabmap::util3d::transformPointCloud(cloud, indices, iter->second); + else if(cloudI.get() && !cloudI->empty()) + cloudI = rtabmap::util3d::transformPointCloud(cloudI, indices, iter->second); } - if(cloud.get() && !cloud->empty()) - cloud = rtabmap::util3d::transformPointCloud(cloud, iter->second); - else if(cloudI.get() && !cloudI->empty()) - cloudI = rtabmap::util3d::transformPointCloud(cloudI, iter->second); if(filter_ceiling != 0.0 || filter_floor != 0.0f) { From d6cf470402abc37dde56661b6be99d493b288546 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Fri, 13 Feb 2026 10:46:26 -0800 Subject: [PATCH 083/126] Support proximity detection when switching from localization mode to slam mode (#1644) --- corelib/src/Rtabmap.cpp | 72 +++++++++++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 24 deletions(-) diff --git a/corelib/src/Rtabmap.cpp b/corelib/src/Rtabmap.cpp index 939a624d..727ae28c 100644 --- a/corelib/src/Rtabmap.cpp +++ b/corelib/src/Rtabmap.cpp @@ -728,29 +728,24 @@ void Rtabmap::parseParameters(const ParametersMap & parameters) isMemIncremental != _memory->isIncremental()) { // Mode has changed from Mapping to Localization, cleanup the local graph - if(_memory->isGraphReduced() && _memory->isIncremental()) + if(_memory->isIncremental()) { - // Force reducing graph, then remove filtered nodes from the optimized poses - std::map reducedIds; - _memory->incrementMapId(&reducedIds); - for(std::map::iterator iter=reducedIds.begin(); iter!=reducedIds.end(); ++iter) + if(_memory->isGraphReduced()) { - _optimizedPoses.erase(iter->first); + // Force reducing graph, then remove filtered nodes from the optimized poses + std::map reducedIds; + _memory->incrementMapId(&reducedIds); + for(std::map::iterator iter=reducedIds.begin(); iter!=reducedIds.end(); ++iter) + { + _optimizedPoses.erase(iter->first); + } } + _odomCachePoses.clear(); + _odomCacheConstraints.clear(); } // In both cases, we save the latest optimized graph and latest localization pose _memory->saveOptimizedPoses(_optimizedPoses, _lastLocalizationPose); - - // Mode changed from Localization to Mapping, clear local graph - if(!_memory->isIncremental()) { - _optimizedPoses.clear(); - _lastLocalizationPose.setNull(); - _mapCorrection.setIdentity(); - _mapCorrectionBackup.setNull(); - _localizationCovariance = cv::Mat(); - _lastLocalizationNodeId = 0; - } } _memory->parseParameters(parameters); @@ -765,12 +760,6 @@ void Rtabmap::parseParameters(const ParametersMap & parameters) { this->createGlobalScanMap(); } - - if(_memory->isIncremental()) - { - _odomCachePoses.clear(); - _odomCacheConstraints.clear(); - } } if(!_epipolarGeometry) @@ -1794,7 +1783,7 @@ bool Rtabmap::process( } } _lastLocalizationPose = newPose; // keep in cache the latest corrected pose - if(!_memory->isIncremental() && signature->getWeight() >= 0) + if(signature->getWeight() >= 0) { UDEBUG("Update odometry localization cache (size=%d/%d)", (int)_odomCachePoses.size(), _maxOdomCacheSize); if(!_odomCachePoses.empty()) @@ -2672,7 +2661,9 @@ bool Rtabmap::process( std::map nearestIds = graph::findNearestNodes(signature->id(), _optimizedPoses, _localRadius); UDEBUG("nearestIds=%d/%d", (int)nearestIds.size(), (int)_optimizedPoses.size()); std::map nearestPoses; + std::map optimizedPosesWithOdomCache; std::multimap links; + std::map * refPoses = &_optimizedPoses; if(_memory->isIncremental() && _proximityMaxGraphDepth>0) { // get bidirectional links @@ -2684,6 +2675,25 @@ bool Rtabmap::process( links.insert(std::make_pair(iter->second.to(), iter->second.from())); // <-> } } + if(_odomCachePoses.size() > 1) + { + // Add odometry cache if it contains a loop closure + // That could happen when we just switched from localization mode to + // mapping mode while being localized on the previous session. + optimizedPosesWithOdomCache = _optimizedPoses; + optimizedPosesWithOdomCache.insert(_odomCachePoses.begin(), _odomCachePoses.end()); + refPoses = &optimizedPosesWithOdomCache; + for(std::multimap::iterator iter=_odomCacheConstraints.begin(); iter!=_odomCacheConstraints.end(); ++iter) + { + if(uContains(optimizedPosesWithOdomCache, iter->second.from()) && + uContains(optimizedPosesWithOdomCache, iter->second.to()) && + iter->second.from() != iter->second.to()) + { + links.insert(std::make_pair(iter->second.from(), iter->second.to())); + links.insert(std::make_pair(iter->second.to(), iter->second.from())); // <-> + } + } + } } for(std::map::iterator iter=nearestIds.lower_bound(1); iter!=nearestIds.end(); ++iter) { @@ -2691,7 +2701,7 @@ bool Rtabmap::process( { if(_memory->isIncremental() && _proximityMaxGraphDepth > 0) { - std::list > path = graph::computePath(_optimizedPoses, links, signature->id(), iter->first); + std::list > path = graph::computePath(*refPoses, links, signature->id(), iter->first); UDEBUG("Graph depth to %d = %ld", iter->first, path.size()); if(!path.empty() && (int)path.size() <= _proximityMaxGraphDepth) { @@ -4318,6 +4328,20 @@ bool Rtabmap::process( // If there is a too small displacement, remove the node signaturesRemoved.push_back(signature->id()); _memory->deleteLocation(signature->id()); + + // Update odom cache (if we just switched from mapping mode to localization mode) + _odomCachePoses.erase(signature->id()); + for(std::multimap::iterator iter=_odomCacheConstraints.begin(); iter!=_odomCacheConstraints.end();) + { + if(iter->second.from() == signature->id() || iter->second.to() == signature->id()) + { + _odomCacheConstraints.erase(iter++); + } + else + { + ++iter; + } + } } else { From 0ef907757b2bdbeb532f0baf957171ce6fef9480 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Fri, 13 Feb 2026 10:49:12 -0800 Subject: [PATCH 084/126] Detect more loop closures from/to specific map ID only option (#1653) * Detect more loop closures from/to specific map ID only option * default value -1 * Added elapsed time * Avoid loading ALL signatures in RAM, only load them when necessary * Avoid creating flann index when initializing with Kp/NNStrategy>=3 * Setting opt params by default --- corelib/include/rtabmap/core/Rtabmap.h | 3 +- corelib/src/Rtabmap.cpp | 68 +++++++--- corelib/src/VWDictionary.cpp | 20 ++- guilib/src/DatabaseViewer.cpp | 30 ++++- guilib/src/ui/DatabaseViewer.ui | 178 ++++++++++++++----------- tools/DetectMoreLoopClosures/main.cpp | 44 +++++- 6 files changed, 234 insertions(+), 109 deletions(-) diff --git a/corelib/include/rtabmap/core/Rtabmap.h b/corelib/include/rtabmap/core/Rtabmap.h index f15a8ef4..2905db9c 100644 --- a/corelib/include/rtabmap/core/Rtabmap.h +++ b/corelib/include/rtabmap/core/Rtabmap.h @@ -209,7 +209,8 @@ public: bool intraSession = true, bool interSession = true, const ProgressState * state = 0, - float clusterRadiusMin = 0.0f); + float clusterRadiusMin = 0.0f, + int toFromMapId = -1); bool globalBundleAdjustment( int optimizerType = 1 /*g2o*/, bool rematchFeatures = true, diff --git a/corelib/src/Rtabmap.cpp b/corelib/src/Rtabmap.cpp index 727ae28c..dd99a2c9 100644 --- a/corelib/src/Rtabmap.cpp +++ b/corelib/src/Rtabmap.cpp @@ -5669,7 +5669,8 @@ int Rtabmap::detectMoreLoopClosures( bool intraSession, bool interSession, const ProgressState * processState, - float clusterRadiusMin) + float clusterRadiusMin, + int toFromMapId) { UDEBUG(""); UASSERT(iterations>0); @@ -5696,17 +5697,23 @@ int Rtabmap::detectMoreLoopClosures( std::map posesToCheckLoopClosures; std::map poses; std::multimap links; - std::map signatures; // some signatures may be in LTM, get them all - this->getGraph(poses, links, true, true, &signatures); + this->getGraph(poses, links, true, true); std::map mapIds; UDEBUG("remove all invalid or intermediate nodes, fill mapIds"); for(std::map::iterator iter=poses.upper_bound(0); iter!=poses.end();++iter) { - if(signatures.at(iter->first).getWeight() >= 0) + Transform odom, gt; + int mapId, weight; + std::string l; + double s; + std::vector v; + GPS gps; + EnvSensors srs; + if(_memory->getNodeInfo(iter->first, odom, mapId, weight, l, s, gt, v, gps, srs, true) && weight >= 0) { posesToCheckLoopClosures.insert(*iter); - mapIds.insert(std::make_pair(iter->first, signatures.at(iter->first).mapId())); + mapIds.insert(std::make_pair(iter->first, mapId)); } } @@ -5720,7 +5727,28 @@ int Rtabmap::detectMoreLoopClosures( clusterRadiusMax, clusterAngle); - UINFO("Looking for more loop closures, clustering poses... found %d clusters.", (int)clusters.size()); + UINFO("Looking for more loop closures: clustering poses... found %ld clusters.", clusters.size()); + + if(toFromMapId >=0) + { + for(std::multimap::iterator iter=clusters.begin(); iter!=clusters.end();) + { + int mapId = uValue(mapIds, iter->first, 0); + if(mapId != toFromMapId) + { + iter = clusters.erase(iter); + } + else { + ++iter; + } + } + UINFO("Looking for more loop closures: filtered %ld clusters for map session %d.", clusters.size(), toFromMapId); + if(clusters.empty()) + { + UERROR("No clusters belong to mapId %d, aborting.", toFromMapId); + break; + } + } int i=0; std::set addedLinks; @@ -5772,8 +5800,10 @@ int Rtabmap::detectMoreLoopClosures( { checkedLoopClosures.insert(std::make_pair(from, to)); - UASSERT(signatures.find(from) != signatures.end()); - UASSERT(signatures.find(to) != signatures.end()); + Signature fromS = getSignatureCopy(from, false, true, false, false, true, false); + Signature toS = getSignatureCopy(to, false, true, false, false, true, false); + UASSERT(fromS.getWeight()>=0); + UASSERT(toS.getWeight()>=0); Transform guess; if(_proximityBySpace && uContains(poses, from) && uContains(poses, to)) @@ -5783,7 +5813,7 @@ int Rtabmap::detectMoreLoopClosures( RegistrationInfo info; // use signatures instead of IDs because some signatures may not be in WM - Transform t = _memory->computeTransform(signatures.at(from), signatures.at(to), guess, &info); + Transform t = _memory->computeTransform(fromS, toS, guess, &info); if(!t.isNull()) { @@ -5792,11 +5822,11 @@ int Rtabmap::detectMoreLoopClosures( //optimize the graph to see if the new constraint is globally valid int fromId = from; - int mapId = signatures.at(from).mapId(); + int mapId = fromS.mapId(); // use first node of the map containing from - for(std::map::iterator ster=signatures.begin(); ster!=signatures.end(); ++ster) + for(std::map::iterator ster=posesToCheckLoopClosures.begin(); ster!=posesToCheckLoopClosures.end(); ++ster) { - if(ster->second.mapId() == mapId) + if(uValue(mapIds, ster->first, 0) == mapId) { fromId = ster->first; break; @@ -5811,22 +5841,22 @@ int Rtabmap::detectMoreLoopClosures( float maxLinearErrorRatio = 0.0f; float maxAngularErrorRatio = 0.0f; std::map optimizedPoses; - std::multimap links; + std::multimap linksOut; UASSERT(poses.find(fromId) != poses.end()); UASSERT_MSG(poses.find(from) != poses.end(), uFormat("id=%d poses=%d links=%d", from, (int)poses.size(), (int)links.size()).c_str()); UASSERT_MSG(poses.find(to) != poses.end(), uFormat("id=%d poses=%d links=%d", to, (int)poses.size(), (int)links.size()).c_str()); - _graphOptimizer->getConnectedGraph(fromId, poses, linksIn, optimizedPoses, links); + _graphOptimizer->getConnectedGraph(fromId, poses, linksIn, optimizedPoses, linksOut); UASSERT(optimizedPoses.find(fromId) != optimizedPoses.end()); - UASSERT_MSG(optimizedPoses.find(from) != optimizedPoses.end(), uFormat("id=%d poses=%d links=%d", from, (int)optimizedPoses.size(), (int)links.size()).c_str()); - UASSERT_MSG(optimizedPoses.find(to) != optimizedPoses.end(), uFormat("id=%d poses=%d links=%d", to, (int)optimizedPoses.size(), (int)links.size()).c_str()); - UASSERT(graph::findLink(links, from, to) != links.end()); - optimizedPoses = _graphOptimizer->optimize(fromId, optimizedPoses, links); + UASSERT_MSG(optimizedPoses.find(from) != optimizedPoses.end(), uFormat("id=%d poses=%d links=%d", from, (int)optimizedPoses.size(), (int)linksOut.size()).c_str()); + UASSERT_MSG(optimizedPoses.find(to) != optimizedPoses.end(), uFormat("id=%d poses=%d links=%d", to, (int)optimizedPoses.size(), (int)linksOut.size()).c_str()); + UASSERT(graph::findLink(linksOut, from, to) != linksOut.end()); + optimizedPoses = _graphOptimizer->optimize(fromId, optimizedPoses, linksOut); std::string msg; if(optimizedPoses.size()) { graph::computeMaxGraphErrors( optimizedPoses, - links, + linksOut, maxLinearErrorRatio, maxAngularErrorRatio, maxLinearError, diff --git a/corelib/src/VWDictionary.cpp b/corelib/src/VWDictionary.cpp index 2b5f0d28..d10b69ae 100644 --- a/corelib/src/VWDictionary.cpp +++ b/corelib/src/VWDictionary.cpp @@ -571,19 +571,27 @@ void VWDictionary::update() else if(_strategy >= kNNBruteForce && _notIndexedWords.size() && _removedIndexedWords.size() == 0 && - _visualWords.size() && - _dataTree.rows) + _visualWords.size()) { //just add not indexed words int i = _dataTree.rows; - _dataTree.reserve(_dataTree.rows + _notIndexedWords.size()); + if(!_dataTree.empty()) { + _dataTree.reserve(_dataTree.rows + _notIndexedWords.size()); + } for(std::set::iterator iter=_notIndexedWords.begin(); iter!=_notIndexedWords.end(); ++iter) { VisualWord* w = uValue(_visualWords, *iter, (VisualWord*)0); UASSERT(w); - UASSERT(w->getDescriptor().cols == _dataTree.cols); - UASSERT(w->getDescriptor().type() == _dataTree.type()); - _dataTree.push_back(w->getDescriptor()); + if(_dataTree.empty()) + { + _dataTree = w->getDescriptor().clone(); + } + else + { + UASSERT(w->getDescriptor().cols == _dataTree.cols); + UASSERT(w->getDescriptor().type() == _dataTree.type()); + _dataTree.push_back(w->getDescriptor()); + } _mapIndexId.insert(_mapIndexId.end(), std::pair(i, w->id())); std::pair::iterator, bool> inserted = _mapIdIndex.insert(std::pair(w->id(), i)); UASSERT(inserted.second); diff --git a/guilib/src/DatabaseViewer.cpp b/guilib/src/DatabaseViewer.cpp index 0b85b5ae..f2bb5aab 100644 --- a/guilib/src/DatabaseViewer.cpp +++ b/guilib/src/DatabaseViewer.cpp @@ -855,6 +855,7 @@ void DatabaseViewer::restoreDefaultSettings() ui_->checkBox_detectMore_intraSession->setChecked(true); ui_->checkBox_detectMore_interSession->setChecked(true); ui_->checkBox_opt_graph_as_guess->setChecked(true); + ui_->spinBox_fromToMapId->setValue(-1); } void DatabaseViewer::openDatabase() @@ -4297,6 +4298,7 @@ void DatabaseViewer::detectMoreLoopClosures() bool intraSession = ui_->checkBox_detectMore_intraSession->isChecked(); bool interSession = ui_->checkBox_detectMore_interSession->isChecked(); bool useOptimizedGraphAsGuess = ui_->checkBox_opt_graph_as_guess->isChecked(); + int fromToMapId = ui_->spinBox_fromToMapId->value(); if(!interSession && !intraSession) { QMessageBox::warning(this, tr("Cannot detect more loop closures"), tr("Intra and inter session parameters are disabled! Enable one or both.")); @@ -4313,14 +4315,38 @@ void DatabaseViewer::detectMoreLoopClosures() ui_->doubleSpinBox_detectMore_radius->value(), ui_->doubleSpinBox_detectMore_angle->value()*CV_PI/180.0); - progressDialog->setMaximumSteps(progressDialog->maximumSteps()+(int)clusters.size()); - progressDialog->appendText(tr("Looking for more loop closures, %1 clusters found.").arg(clusters.size())); QApplication::processEvents(); if(progressDialog->isCanceled()) { break; } + progressDialog->appendText(tr("Looking for more loop closures: %1 clusters found.").arg(clusters.size())); + if(fromToMapId >=0) + { + for(std::multimap::iterator iter=clusters.begin(); iter!=clusters.end();) + { + int mapId = uValue(mapIds_, iter->first, 0); + if(mapId != fromToMapId) + { + iter = clusters.erase(iter); + } + else { + ++iter; + } + } + progressDialog->appendText(tr("Looking for more loop closures: filtered %1 clusters for map session %2.").arg(clusters.size()).arg(fromToMapId)); + if(clusters.empty()) + { + progressDialog->appendText(tr("No clusters belong to mapId %1, aborting!").arg(fromToMapId)); + QApplication::processEvents(); + break; + } + } + + progressDialog->setMaximumSteps(progressDialog->maximumSteps()+(int)clusters.size()); + QApplication::processEvents(); + std::set addedLinks; int i=0; for(std::multimap::iterator iter=clusters.begin(); iter!= clusters.end() && !progressDialog->isCanceled(); ++iter, ++i) diff --git a/guilib/src/ui/DatabaseViewer.ui b/guilib/src/ui/DatabaseViewer.ui index 798c9f75..54995ee9 100644 --- a/guilib/src/ui/DatabaseViewer.ui +++ b/guilib/src/ui/DatabaseViewer.ui @@ -61,7 +61,7 @@ 0 0 - 307 + 364 389 @@ -392,7 +392,7 @@ 0 0 - 306 + 364 389 @@ -1722,7 +1722,7 @@ - 1 + 2 @@ -1885,8 +1885,8 @@ 0 0 - 518 - 1071 + 276 + 1598 @@ -2604,15 +2604,22 @@ 0 - 0 - 222 - 306 + -152 + 403 + 287 Detect more loop closures + + + + + + + @@ -2632,13 +2639,6 @@ - - - - Radius Min - - - @@ -2646,23 +2646,43 @@ - - - - Qt::Vertical - - - - 20 - 40 - - - - - - + + - Radius Max + Iterations + + + + + + + 1 + + + 100 + + + 5 + + + + + + + + + + true + + + + + + + Use optimized graph as guess + + + true @@ -2673,25 +2693,6 @@ - - - - degrees - - - 0 - - - 180.000000000000000 - - - 1.000000000000000 - - - 30.000000000000000 - - - @@ -2711,18 +2712,18 @@ - - - - 1 + + + + Qt::Vertical - - 100 + + + 20 + 40 + - - 5 - - + @@ -2731,10 +2732,17 @@ - - + + - Iterations + Radius Max + + + + + + + Radius Min @@ -2745,30 +2753,42 @@ - - - - + + + + degrees + + + 0 + + + 180.000000000000000 + + + 1.000000000000000 + + + 30.000000000000000 - - - - + + + + -1 - - true + + 9999 + + + -1 - - + + - Use optimized graph as guess - - - true + From/to map ID only (-1 is all) diff --git a/tools/DetectMoreLoopClosures/main.cpp b/tools/DetectMoreLoopClosures/main.cpp index dba760bb..3f4f7219 100644 --- a/tools/DetectMoreLoopClosures/main.cpp +++ b/tools/DetectMoreLoopClosures/main.cpp @@ -58,6 +58,7 @@ void showUsage() " -i # Iterations (default 1).\n" " --intra Add only intra-session loop closures.\n" " --inter Add only inter-session loop closures.\n" + " --session # Add loop closures only from/to that map session ID (use -1 for last session).\n" "\n%s", Parameters::showUsage()); exit(1); } @@ -73,12 +74,17 @@ void sighandler(int sig) class PrintProgressState : public ProgressState { public: + PrintProgressState() : + stamp_(UTimer::now()) + {} virtual bool callback(const std::string & msg) const { if(!msg.empty()) - printf("%s \n", msg.c_str()); + printf("[%f] %s \n", UTimer::now()-stamp_, msg.c_str()); return g_loopForever; } +private: + double stamp_; }; int main(int argc, char * argv[]) @@ -101,6 +107,7 @@ int main(int argc, char * argv[]) int iterations = 1; bool intraSession = false; bool interSession = false; + int fromToMapId = -2; for(int i=1; iload2DMap(xMin, yMin, cellSize).empty(); + if(fromToMapId <= -2) + { + fromToMapId = -1; // means all below + } + else + { + bool last = false; + if(fromToMapId == -1) + { + // get last session ID + fromToMapId = rtabmap.getMemory()->getMapId(rtabmap.getMemory()->getLastSignatureId(), true); + } + printf("From/To Session ID = %d%s\n", fromToMapId, last?" (last session)":""); + } + PrintProgressState progress; printf("Detecting...\n"); - int detected = rtabmap.detectMoreLoopClosures(clusterRadiusMax, clusterAngle, iterations, intraSession, interSession, &progress, clusterRadiusMin); + int detected = rtabmap.detectMoreLoopClosures(clusterRadiusMax, clusterAngle, iterations, intraSession, interSession, &progress, clusterRadiusMin, fromToMapId); if(detected < 0) { if(!g_loopForever) From 8051be45b39236dab54cfef5cceb90b544d222e4 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Wed, 11 Mar 2026 15:10:52 -0700 Subject: [PATCH 085/126] Automated dictionary recovery/repairing (#1669) * Automated dictionary recovery/repairing * typo * updated comment * updating dictionary after fixing it * Fixing missing ref * Adding helping assert msg in case of repairing failure --- corelib/include/rtabmap/core/VWDictionary.h | 2 +- corelib/src/Memory.cpp | 114 +++++++++++++++++++- corelib/src/VWDictionary.cpp | 6 +- 3 files changed, 115 insertions(+), 7 deletions(-) diff --git a/corelib/include/rtabmap/core/VWDictionary.h b/corelib/include/rtabmap/core/VWDictionary.h index 7b58209d..ebfa3a62 100644 --- a/corelib/include/rtabmap/core/VWDictionary.h +++ b/corelib/include/rtabmap/core/VWDictionary.h @@ -89,7 +89,7 @@ public: std::vector findNN(const std::list & vws) const; std::vector findNN(const cv::Mat & descriptors) const; - void addWordRef(int wordId, int signatureId); + bool addWordRef(int wordId, int signatureId); void removeAllWordRef(int wordId, int signatureId); const VisualWord * getWord(int id) const; VisualWord * getUnusedWord(int id) const; diff --git a/corelib/src/Memory.cpp b/corelib/src/Memory.cpp index 6d5aa8ad..3d046724 100644 --- a/corelib/src/Memory.cpp +++ b/corelib/src/Memory.cpp @@ -437,7 +437,8 @@ void Memory::loadDataFromDb(bool postInitClosingEvents) UTimer timer; // Enable loaded signatures const std::map & signatures = this->getSignatures(); - for(std::map::const_iterator i=signatures.begin(); i!=signatures.end(); ++i) + bool corruptedDictionary = false; + for(std::map::const_iterator i=signatures.begin(); i!=signatures.end() && !corruptedDictionary; ++i) { Signature * s = this->_getSignature(i->first); UASSERT(s != 0); @@ -450,12 +451,114 @@ void Memory::loadDataFromDb(bool postInitClosingEvents) { if(iter->first > 0) { - _vwd->addWordRef(iter->first, i->first); + if(!_vwd->addWordRef(iter->first, s->id())) + { + corruptedDictionary = true; + break; + } + } + } + s->setEnabled(!corruptedDictionary); + if(corruptedDictionary) + { + //revert all changes from that signature till it broke above + for(std::multimap::const_iterator iter = words.begin(); iter!=words.end(); ++iter) + { + if(iter->first > 0) + { + _vwd->removeAllWordRef(iter->first, s->id()); + } } } - s->setEnabled(true); } } + if(corruptedDictionary) + { + if(!_vwd->isIncremental()) + { + UERROR("The dictionary is empty or missing some words from nodes in WM, " + "we cannot repair it because it is a fixed dictionary. Make sure you " + "are using the right fixed dictionary that was used to generate the map."); + } + else + { + std::string msg = uFormat( + "The dictionary is empty or missing some words from nodes in WM, " + "we will try to repair it. This can be caused by rtabmap closing before it has time " + "to save the dictionary. Re-creating the dictionary from %ld nodes...", + signatures.size()); + UWARN("%s", msg.c_str()); + if(postInitClosingEvents) UEventsManager::post(new RtabmapEventInit(msg)); + + //remove all words ref + + const std::map & addedWords = _vwd->getVisualWords(); + int nodesRepaired = 0; + size_t oldSize = addedWords.size(); + std::string assertMsg = + "If we assert here, the problem is maybe deeper. Try " + "to use rtabmap-recovery tool instead to fix the database."; + for(std::map::const_iterator i=signatures.begin(); i!=signatures.end(); ++i) + { + Signature * s = this->_getSignature(i->first); + UASSERT_MSG(s != 0, assertMsg.c_str()); + + if(s->isEnabled()) + { + // Words already in dictionary and references added + continue; + } + + const std::multimap * words = &s->getWords(); + if(words->size()) + { + cv::Mat descriptors = s->getWordsDescriptors(); + std::multimap loadedWords; + if(descriptors.empty()) + { + // We may have started rtabmap without loading features, check in the database + std::multimap w; + std::vector k; + std::vector p; + _dbDriver->getLocalFeatures(s->id(), loadedWords, k, p, descriptors); + UASSERT_MSG(loadedWords.size() == words->size(), assertMsg.c_str()); // Just doublecheck + words = &loadedWords; // The index will be set + UASSERT_MSG(!descriptors.empty(), assertMsg.c_str()); + } + bool repaired = false; + for(std::multimap::const_iterator iter = words->begin(); iter!=words->end(); ++iter) + { + if(iter->first > 0) + { + if(addedWords.find(iter->first) == addedWords.end()) + { + UASSERT_MSG(iter->second >= 0 && iter->second < descriptors.rows, + uFormat("iter->second=%d descriptors.rows=%d (signature=%d word=%d). %s", + iter->second, descriptors.rows, s->id(), iter->first, assertMsg.c_str()).c_str()); + _vwd->addWord(new VisualWord(iter->first, descriptors.row(iter->second).clone())); + repaired = true; + } + UASSERT_MSG(_vwd->addWordRef(iter->first, s->id()), assertMsg.c_str()); + } + } + nodesRepaired += (repaired?1:0); + s->setEnabled(true); + } + } + + msg = uFormat( + "Regenerated the dictionary with %ld missing words (%ld -> %ld) from %d nodes.", + addedWords.size() - oldSize, + oldSize, + addedWords.size(), + nodesRepaired); + UWARN("%s", msg.c_str()); + if(postInitClosingEvents) UEventsManager::post(new RtabmapEventInit(msg)); + _memoryChanged = true; // This will force rtabmap to save back the dictionary even if we don't process any new data + _vwd->update(); + } + } + if(postInitClosingEvents) UEventsManager::post(new RtabmapEventInit(uFormat("Adding word references, done! (%d)", _vwd->getTotalActiveReferences()))); if(_vwd->getUnusedWordsSize() && _vwd->isIncremental()) @@ -6599,7 +6702,10 @@ void Memory::enableWordsRef(const std::list & signatureIds) { if(keys.at(i)>0) { - _vwd->addWordRef(keys.at(i), (*j)->id()); + if(_vwd->addWordRef(keys.at(i), (*j)->id())) + { + UERROR("Could not add word ref %d to node %d!?", keys.at(i), (*j)->id()); + } } } (*j)->setEnabled(true); diff --git a/corelib/src/VWDictionary.cpp b/corelib/src/VWDictionary.cpp index d10b69ae..2829b301 100644 --- a/corelib/src/VWDictionary.cpp +++ b/corelib/src/VWDictionary.cpp @@ -868,7 +868,7 @@ int VWDictionary::getNextId() return ++_lastWordId; } -void VWDictionary::addWordRef(int wordId, int signatureId) +bool VWDictionary::addWordRef(int wordId, int signatureId) { VisualWord * vw = 0; vw = uValue(_visualWords, wordId, vw); @@ -878,10 +878,12 @@ void VWDictionary::addWordRef(int wordId, int signatureId) _totalActiveReferences += 1; _unusedWords.erase(vw->id()); + return true; } else { - UERROR("Not found word %d (dict size=%d)", wordId, (int)_visualWords.size()); + UWARN("Not found word %d (dict size=%d)", wordId, (int)_visualWords.size()); + return false; } } From 018b804a462b86810bb72a84dfef98695eb64214 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 15 Mar 2026 14:57:27 -0700 Subject: [PATCH 086/126] Adding OpenGV as submodule (optional) and use Github actions for windows CI (#1656) * Adding OpenGV as submodule (optional) * Remove submodule to migrate to FetchContent * using FetchContent * remove empty .gitmodules * Fixing OpenGV not able to find eigen on windows * patching opengv to adjust -march=native based on PCL * fixing patching on windows * eigen fix * PR cancel on-going CI builds if new commit is added to PR * try another approach * updating patch with some logs * more debug mesage * fixing EIGEN_INCLUDE_DIR * enable rolling_builds on appveyor * removed rolling_builds appveyor * fixing eigen cache * test * more debug logs * another try * cleanup * updated appveyor to work with fetchcontent * removed mkdir build (appveyor) * appveyor: trying ninja to increase CI speed * removed mkdr * appveyor: spitting opengv and rtabmap builds to be under 60 min per job * appveyor: caching dependencies * using global configuration * using baked image * fixing wget in ps * fixing not support for * pip error * reverted pip install * fixed realsense cache * fixing multi step build * removing baked image * typo * Windows: Converted appveyor to github actions * updated boost version * fixing boost * udpated boost config * boost... * platform_version * install boost directly * silent boost install * very silent boost * fixing ls * added boost install dir * showing boost install dir * moved windows dependencies in external action file * explicitly save boost cache to same time on iterations * pip install gdown * update * caching more deps * caching all depts * removed explicit boost cache save * mscv 14.0 * forcing building visual studio 14 2015 * installing v12 in 2022 instead * setup cmd prompt * init right toolset * fixing system version for opengv compilation error * fixing package and artifact * CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION * removing cmd * find manually psapi.lib * resolve * refactor psapi env * forcing 10.0.19041.0 * using windows=2022 runner instead * added vcpkg * commenting windows-latest for now * trying vcpkg instead * search path * updated path * moved json * removed deleted file for git * adding a real version * using version-string * cleanup vcpkg * building artifacts with vcpkg * removed old windows build approach, added custom dependencies (gtsam, libpointmatcher) to vcpkg build * udpated vcpkg version for issue https://github.com/microsoft/vcpkg/pull/49103 * try with minimal dep first * disable opengv for now * try vcpkg single compilation * space * updated link * fixed qupote * fixing cache name * adding debug folder * adjust path * adjust path * providing vcpkg binaries instead * ident * added triplet * added BOOST_ROOT * boost root * boost root * fixed path * adding eigen headers * try path * cmake prefix path * boost timer def * libnabo fixes * pointmatcher prefix * disabling pointmathcer tests * updated cmake parameters * changed how file is downloaded * trying curl instead * puttoing backe InvokeWeb because it is a dropbox issue * skipping optional deps for now * ficing env variable * triplet * installing triplets * not overriding default vcpkg env variables * missing path * manifet install off * that was working locally * missing protobuf path * fixing tiff not found * fixing vtk not found * more vtk fixes * another thy * changing download url * updated url * try * shoudl work now * protobuf exe * readding tiff * explicit vcpkg installed folder * missing commands * try without tiff * ficing vtk comple path * -DPSAPI_LIBRARIES=Psapi.lib * quoting * fixing psapi required * Set up MSVC Developer Command Prompt * disabling pckaging for now * openni.ini * renabling packing * Added stripped deps * fix name * updated binaries * format * updated opengv eigen path * added tbb dep * updatd archive name with vs version * updated archive in action * fixing patch error * corrupted * updated gtsam version / vcpkg * removed appveyor. Set internal opengv build disabled by defaut (because build can be very long on some machines), will enable it inside the ros release branches instead. * updated opengv patch * gtsam mkl dep * updated vcpkg binaries * updated patches * removed mkl dep * missing eigen in gtsam dep * disabling gtsam till we find a compatible version locally * all working locally! * removed ninja * working python calls * fixed hard symlink for python3.dll * fixed qt missing png, fixed opengv not finding eigen with config, fixed python3.dll missing * fixed flaoting dockwidget on start, removed cmd line window when launching bundled app * use sub-packages cudnn * Added cuda dev workflow * fixed archive name * Updated deps with pytorch cuda * updating ci PATH * fixing ci build without torch * rename cuda artifacts * cache cuda, add job to test internal opengv build * Updated output artifacts zip name * windows package: only zip on pull request * Change USE_INTERNAL_OPENGV to BUILD_OPENGV * use use-github-cache * updated artifacts path --- .appveyor.yml | 152 - .../install-windows-cuda-deps/action.yml | 53 + .../actions/install-windows-deps/action.yml | 37 + .github/workflows/cleanup-pr-artifacts.yml | 15 + .github/workflows/cmake-ros.yml | 4 + .github/workflows/cmake.yml | 86 +- CMakeLists.txt | 82 +- app/ios/RTABMapApp/install_deps.sh | 2 +- app/src/CMakeLists.txt | 139 +- bundle_windows_deps.bat | 227 ++ bundle_windows_deps_cuda.bat | 239 ++ corelib/src/CMakeLists.txt | 25 +- corelib/src/python/PyMatcher.cpp | 24 +- corelib/src/python/PythonInterface.cpp | 7 + corelib/src/python/rtabmap_superglue.py | 3 +- corelib/src/python/rtabmap_superpoint.py | 13 +- .../src/python/rtabmap_trace_superpoint.py | 13 + .../noble/android/rtabmap_apiXX/rtabmap.bash | 2 +- guilib/src/3rdParty/QMultiComboBox.cpp | 2 +- guilib/src/AboutDialog.cpp | 7 + guilib/src/ui/aboutDialog.ui | 2493 +++++++++-------- guilib/src/ui/mainWindow.ui | 39 +- patches/apply_patch.cmake | 18 + patches/gtsam_4_3a0-ros.patch | 13 + patches/libnabo_c925c47.patch | 58 + patches/opengv_91f4b19c.patch | 314 +++ patches/pointmatcher_7dc58e5.patch | 589 ++++ utilite/src/CMakeLists.txt | 2 +- vcpkg.json | 224 ++ 29 files changed, 3409 insertions(+), 1473 deletions(-) delete mode 100644 .appveyor.yml create mode 100644 .github/actions/install-windows-cuda-deps/action.yml create mode 100644 .github/actions/install-windows-deps/action.yml create mode 100644 .github/workflows/cleanup-pr-artifacts.yml create mode 100644 bundle_windows_deps.bat create mode 100644 bundle_windows_deps_cuda.bat create mode 100644 corelib/src/python/rtabmap_trace_superpoint.py create mode 100644 patches/apply_patch.cmake create mode 100644 patches/gtsam_4_3a0-ros.patch create mode 100644 patches/libnabo_c925c47.patch create mode 100644 patches/opengv_91f4b19c.patch create mode 100644 patches/pointmatcher_7dc58e5.patch create mode 100644 vcpkg.json diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index 4dc21dc6..00000000 --- a/.appveyor.yml +++ /dev/null @@ -1,152 +0,0 @@ - -branches: - only: - - master - - devel - -os: Visual Studio 2015 - -clone_folder: c:\projects\rtabmap - -platform: x64 -configuration: Release - -init: - - cmake --version - - call "C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\SetEnv.cmd" /x64 - - call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86_amd64 - -install: - # To download from google drive - - set PATH=C:\Python38-x64;C:\Python38-x64\Scripts;%PATH% - - ps: py -m pip --disable-pip-version-check install gdown>=5.1.0 - # Qt - - set QTDIR=C:\Qt\5.10.1\msvc2015_64 - # make sure Qt bin path is before cmake bin path to avoid copying qt5 dlls from cmake before qt installation - - set PATH=%QTDIR%\bin;%PATH% - # Boost - - set PATH=%PATH%;C:\Libraries\boost_1_62_0\lib64-msvc-14.0 - # Openni2 - - ps: wget 'https://dl.dropboxusercontent.com/s/d98jv79l6oy9fxz/OpenNI2.exe?dl=0' -outfile OpenNI2.exe - - cmd: OpenNI2.exe -o"C:\Program Files" -y - - ECHO "Installed OpenNI2:" - - ps: "ls \"C:/Program Files/OpenNI2\"" - - set PATH=%PATH%;C:\Program Files\OpenNI2\Redist - - set OPENNI2_INCLUDE64=C:\Program Files\OpenNI2\Include - - set OPENNI2_LIB64=C:\Program Files\OpenNI2\Lib - - set OPENNI2_REDIST64=C:\Program Files\OpenNI2\Redist - # OpenCV - #- appveyor-retry appveyor DownloadFile http://downloads.sourceforge.net/project/opencvlibrary/4.5.2/opencv-4.5.2-vc14_vc15.exe - #- cmd: opencv-4.5.2-vc14_vc15.exe -o"C:\Program Files" -y - #- ECHO "Installed OpenCV:" - #- ps: "ls \"C:/Program Files/opencv/build\"" - #- set PATH=%PATH%;C:\Program Files\opencv\build\x64\vc14\bin - - ps: wget 'https://dl.dropboxusercontent.com/s/o6ofn491bc0jso1/opencv450_vc14.exe?dl=0' -outfile opencv.exe - - cmd: opencv.exe -o"C:\Program Files" -y - - ECHO "Installed OpenCV:" - - ps: "ls \"C:/Program Files/opencv\"" - - set PATH=%PATH%;C:\Program Files\opencv\x64\vc14\bin - # VTK (including QVTK) - - ps: wget 'https://dl.dropboxusercontent.com/s/1l33b5l3f3y52gf/VTK-6_3-msvc140.exe?dl=0' -outfile VTK-6_3.exe - - cmd: VTK-6_3.exe -o"C:\Program Files" -y - - ECHO "Installed VTK:" - - ps: "ls \"C:/Program Files/VTK\"" - - set PATH=%PATH%;C:\Program Files\VTK\bin - # QHull - - ps: wget 'https://dl.dropboxusercontent.com/s/9widnk9msdsh2b8/Qhull-msvc140.exe?dl=0' -outfile Qhull.exe - - cmd: Qhull.exe -o"C:\Program Files" -y - - ECHO "Installed QHull:" - - ps: "ls \"C:/Program Files/Qhull\"" - - set PATH=%PATH%;C:\Program Files\Qhull\bin - # FLANN - - ps: wget 'https://dl.dropboxusercontent.com/s/7k58jbmqa51sxmh/FLANN-msvc140.exe?dl=0' -outfile FLANN.exe - - cmd: FLANN.exe -o"C:\Program Files" -y - - ECHO "Installed FLANN:" - - ps: "ls \"C:/Program Files/FLANN\"" - - set PATH=%PATH%;C:\Program Files\FLANN\bin - # Eigen - - ps: wget 'https://dl.dropboxusercontent.com/s/3v6i9i8dxj4o8ji/Eigen.exe?dl=0' -outfile Eigen.exe - - cmd: Eigen.exe -o"C:\Program Files" -y - - ECHO "Installed Eigen:" - - ps: "ls \"C:/Program Files/Eigen\"" - # PCL - - ps: wget 'https://dl.dropboxusercontent.com/s/2iayr4lyqa50i9j/PCL_181_August2018_x64_vc14.exe?dl=0' -outfile PCL_1.8.1.exe - - cmd: PCL_1.8.1.exe -o"C:\Program Files" -y - - ECHO "Installed PCL:" - - ps: "ls \"C:/Program Files/PCL\"" - - set PATH=%PATH%;C:\Program Files\PCL\bin - # zlib - - ps: gdown -q 0B46akLGdg-uaYm9MTTI4MUtUcmc - - ps: Expand-Archive zlib-1.2.8-vc2010-x64.zip -DestinationPath 'C:\Program Files' - - ECHO "Installed zlib:" - - ps: "ls \"C:/Program Files/zlib\"" - - set PATH=%PATH%;C:\Program Files\zlib\bin - # g2o - - ps: wget 'https://dl.dropboxusercontent.com/s/ht74s5pa21wokzw/g2o.exe?dl=0' -outfile g2o.exe - - cmd: g2o.exe -o"C:\Program Files" -y - - ECHO "Installed g2o:" - - ps: "ls \"C:/Program Files/g2o\"" - - set PATH=%PATH%;C:\Program Files\g2o\bin - # GTSAM - - ps: wget 'https://dl.dropboxusercontent.com/s/0fpr6r4cgsqmvhf/GTSAM-4_0_0_alpha2-msvc140.exe?dl=0' -outfile GTSAM.exe - - cmd: GTSAM.exe -o"C:\Program Files" -y - - ECHO "Installed GTSAM:" - - ps: "ls \"C:/Program Files/GTSAM\"" - - set PATH=%PATH%;C:\Program Files\GTSAM\bin - # OctoMap - - ps: wget 'https://dl.dropboxusercontent.com/s/6jpxu0nm8ne6e54/octomap_x64_vc14.exe?dl=0' -outfile octomap.exe - - cmd: octomap.exe -o"C:\Program Files" -y - - ECHO "Installed OctoMap:" - - ps: "ls \"C:/Program Files/octomap-distribution\"" - - set PATH=%PATH%;C:\Program Files\octomap-distribution\bin - # CPU-TSDF - - ps: wget 'https://dl.dropboxusercontent.com/s/mgges9va1uzxr0q/cpu_tsdf_sept2015_x64_vc14.exe?dl=0' -outfile cpu_tsdf.exe - - cmd: cpu_tsdf.exe -o"C:\Program Files" -y - - ECHO "Installed CPU-TSDF:" - - ps: "ls \"C:/Program Files/cpu_tsdf\"" - - set PATH=%PATH%;C:\Program Files\cpu_tsdf\bin - # Open Chisel - - ps: wget 'https://dl.dropboxusercontent.com/s/0aaphcde4acrinm/open_chisel_x64_vc14.exe?dl=0' -outfile open_chisel.exe - - cmd: open_chisel.exe -o"C:\Program Files" -y - - ECHO "Installed Open Chisel:" - - ps: "ls \"C:/Program Files/open_chisel\"" - - set PATH=%PATH%;C:\Program Files\open_chisel\bin - # yaml-cpp - - ps: wget 'https://dl.dropboxusercontent.com/s/22qfvftwj6zq8tj/yaml-cpp_x64_vc14.exe?dl=0' -outfile yaml-cpp.exe - - cmd: yaml-cpp.exe -o"C:\Program Files" -y - - ECHO "Installed yaml-cpp:" - - ps: "ls \"C:/Program Files/yaml-cpp\"" - # RealSense2 - - ps: wget 'https://github.com/IntelRealSense/librealsense/releases/download/v2.40.0/Intel.RealSense.SDK-WIN10-2.40.0.2482.exe' -outfile realsense2.exe - - cmd: realsense2.exe /VERYSILENT - - ECHO "Installed RealSense2:" - - ps: "ls \"C:/Program Files (x86)/Intel RealSense SDK 2.0\"" - - set PATH=%PATH%;C:\Program Files (x86)\Intel RealSense SDK 2.0\bin\x64 - - set RealSense2_ROOT_DIR=C:\Program Files (x86)\Intel RealSense SDK 2.0 - # Kinect 4 Azure - - ps: wget 'https://download.microsoft.com/download/3/d/6/3d6d9e99-a251-4cf3-8c6a-8e108e960b4b/Azure%20Kinect%20SDK%201.4.1.exe' -outfile azure.exe - - cmd: azure.exe /quiet - - ECHO "Installed Kinect For Azure:" - - ps: "ls \"C:/Program Files/Azure Kinect SDK v1.4.1\"" - - set PATH=%PATH%;C:\Program Files\Azure Kinect SDK v1.4.1\tools - - set K4A_ROOT_DIR=C:\Program Files\Azure Kinect SDK v1.4.1 - -before_build: - - cd c:\projects\rtabmap\build - - ECHO %PROGRAMFILES% - - ECHO %PATH% - - cmake -G "Visual Studio 14 2015 Win64" -DOpenCV_DIR="C:\Program Files\opencv\build" -DPCL_DIR="C:\Program Files\PCL\cmake" -DCPUTSDF_DIR="C:\Program Files\cpu_tsdf\share\cpu_tsdf" -Dyaml-cpp_DIR="C:\Program Files\yaml-cpp\CMake" -DBUILD_AS_BUNDLE=ON .. - -after_build : - - cmake --build . --config Release --target package - -artifacts: - - path: build\RTABMap-* - -notifications: - - provider: Email - to: - - matlabbe@gmail.com - on_build_success: false - on_build_failure: false - on_build_status_changed: true diff --git a/.github/actions/install-windows-cuda-deps/action.yml b/.github/actions/install-windows-cuda-deps/action.yml new file mode 100644 index 00000000..0027ee58 --- /dev/null +++ b/.github/actions/install-windows-cuda-deps/action.yml @@ -0,0 +1,53 @@ +name: 'Install Windows Dependencies with CUDA' +description: 'Installs PCL, Qt, VTK, g2o and others' +runs: + using: "composite" + steps: + - name: Set up MSVC Developer Command Prompt + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: x64 + + - name: Install CUDA + uses: Jimver/cuda-toolkit@v0.2.30 + id: cuda-toolkit + with: + cuda: '13.0.0' + use-github-cache: True + + - name: Verify CUDA + shell: bash + run: | + nvcc --version + echo "CUDA Path: $CUDA_PATH" + + - name: Cache vcpkg + id: cache-vcpkg + uses: actions/cache@v4 + with: + path: ${{ runner.workspace }}/vcpkg_installed + key: ${{ runner.os }}-vcpkg-export-66c0373d-x64-vs2022-cuda130_v1 + + - name: Download and Install vcpkg + if: steps.cache-vcpkg.outputs.cache-hit != 'true' + shell: pwsh + run: | + $install_dir = "${{ runner.workspace }}\vcpkg_installed" + $archivePath = "${{ runner.workspace }}\vcpkg-export.7z" + + # The file has been built locally with bundle-windows-deps.bat + $url = "https://github.com/introlab/rtabmap/releases/download/0.23.1/vcpkg-export-66c0373d-x64-vs2022-cuda130.7z" + + Invoke-WebRequest -Uri $url -OutFile $archivePath + & 7z x $archivePath "-o$install_dir" -y + + - name: Add vcpkg to PATH and env variable + shell: pwsh + run: | + $vcpkg_path = "${{ runner.workspace }}\vcpkg_installed" + echo "VCPKG_EXPORT_PATH=$vcpkg_path" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + echo "$vcpkg_path\installed\x64-windows-release\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + echo "${{env.CUDA_PATH}}\bin\x64" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + echo "${{env.CUDA_PATH}}\extras\CUPTI\lib64" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + echo "$vcpkg_path\installed\x64-windows-release\tools\python3\Lib\site-packages\torch\lib" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + echo "$vcpkg_path\installed\x64-windows-release\tools\python3\Lib\site-packages\numpy.libs" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append diff --git a/.github/actions/install-windows-deps/action.yml b/.github/actions/install-windows-deps/action.yml new file mode 100644 index 00000000..00983b29 --- /dev/null +++ b/.github/actions/install-windows-deps/action.yml @@ -0,0 +1,37 @@ +name: 'Install Windows Dependencies' +description: 'Installs PCL, Qt, VTK, g2o and others' +runs: + using: "composite" + steps: + - name: Set up MSVC Developer Command Prompt + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: x64 + + - name: Cache vcpkg + id: cache-vcpkg + uses: actions/cache@v4 + with: + path: ${{ runner.workspace }}/vcpkg_installed + key: ${{ runner.os }}-vcpkg-export-66c0373d-x64-vs2022-v4 + + - name: Download and Install vcpkg + if: steps.cache-vcpkg.outputs.cache-hit != 'true' + shell: pwsh + run: | + $install_dir = "${{ runner.workspace }}\vcpkg_installed" + $archivePath = "${{ runner.workspace }}\vcpkg-export.7z" + + # The file has been built locally with bundle-windows-deps.bat + $url = "https://github.com/introlab/rtabmap/releases/download/0.23.1/vcpkg-export-66c0373d-x64-vs2022.7z" + + Invoke-WebRequest -Uri $url -OutFile $archivePath + & 7z x $archivePath "-o$install_dir" -y + + - name: Add vcpkg to PATH and env variable + shell: pwsh + run: | + $vcpkg_path = "${{ runner.workspace }}\vcpkg_installed" + echo "VCPKG_EXPORT_PATH=$vcpkg_path" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + echo "$vcpkg_path\installed\x64-windows-release\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + echo "$vcpkg_path\installed\x64-windows-release\tools\python3\Lib\site-packages\numpy.libs" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append diff --git a/.github/workflows/cleanup-pr-artifacts.yml b/.github/workflows/cleanup-pr-artifacts.yml new file mode 100644 index 00000000..d5e2789a --- /dev/null +++ b/.github/workflows/cleanup-pr-artifacts.yml @@ -0,0 +1,15 @@ +name: Cleanup PR Artifacts +on: + pull_request: + types: [closed] + +jobs: + delete-artifacts: + runs-on: ubuntu-latest + permissions: + actions: write + steps: + - name: Delete PR Artifacts + uses: geekyeggo/delete-artifact@v5 + with: + name: build-output-* \ No newline at end of file diff --git a/.github/workflows/cmake-ros.yml b/.github/workflows/cmake-ros.yml index a7bf4ca6..064c5339 100644 --- a/.github/workflows/cmake-ros.yml +++ b/.github/workflows/cmake-ros.yml @@ -12,6 +12,10 @@ env: # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) BUILD_TYPE: Release +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: build: # The CMake configure and build commands are platform agnostic and should work equally diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 9a347527..ae67299b 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -11,43 +11,111 @@ on: env: BUILD_TYPE: Release +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: build: - name: ${{ matrix.os }} + name: ${{ matrix.build_name }} runs-on: ${{ matrix.os }} strategy: - fail-fast: false + fail-fast: true matrix: - os: [ubuntu-24.04, ubuntu-22.04] + build_name: [ubuntu-22.04, ubuntu-24.04, ubuntu-24.04-with-opengv, windows-2022, windows-2022-cuda] include: - - os: ubuntu-22.04 + - build_name: ubuntu-22.04 + os: ubuntu-22.04 extra_deps: "libunwind-dev libceres-dev" - extra_cmake_def: "" - - os: ubuntu-24.04 + extra_cmake_def: "-DWITH_CERES=ON" + - build_name: ubuntu-24.04 + os: ubuntu-24.04 extra_deps: "libg2o-dev libceres-dev" extra_cmake_def: "-DWITH_CERES=ON" + - build_name: ubuntu-24.04-with-opengv + os: ubuntu-24.04 + extra_deps: "libg2o-dev libceres-dev" + extra_cmake_def: "-DWITH_CERES=ON -DBUILD_OPENGV=ON" + - build_name: windows-2022 + os: windows-2022 + extra_deps: "" + extra_cmake_def: '-DBUILD_AS_BUNDLE=ON -DWITH_PYTHON=ON -DWITH_TORCH=OFF' + - build_name: windows-2022-cuda + os: windows-2022 + extra_deps: "" + extra_cmake_def: '-DBUILD_AS_BUNDLE=ON -DWITH_PYTHON=ON -DWITH_TORCH=ON' - steps: - - name: Install dependencies + steps: + - uses: actions/checkout@v4 + + - name: Install Windows Dependencies + if: matrix.build_name == 'windows-2022' + uses: ./.github/actions/install-windows-deps + + - name: Install Windows Dependencies with CUDA + if: matrix.build_name == 'windows-2022-cuda' + uses: ./.github/actions/install-windows-cuda-deps + + - name: Install Linux Dependencies + if: matrix.os != 'windows-2022' run: | DEBIAN_FRONTEND=noninteractive sudo apt-get update sudo apt-get -y install libopencv-dev libpcl-dev git cmake software-properties-common libyaml-cpp-dev ${{ matrix.extra_deps }} - - uses: actions/checkout@v4 + - name: Configure CMake + if: matrix.os == 'windows-2022' + run: | + cmake ` + -B ${{github.workspace}}/build ` + -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} ` + ${{ matrix.extra_cmake_def }} ` + -DVCPKG_MANIFEST_INSTALL=OFF ` + -DVCPKG_TARGET_TRIPLET=x64-windows-release ` + -DVCPKG_INSTALLED_DIR="${{env.VCPKG_EXPORT_PATH}}/installed" ` + -DCMAKE_TOOLCHAIN_FILE=${{env.VCPKG_EXPORT_PATH}}/scripts/buildsystems/vcpkg.cmake ` + -DTorch_DIR=${{env.VCPKG_EXPORT_PATH}}/installed/x64-windows-release/tools/python3/Lib/site-packages/torch/share/cmake/Torch - name: Configure CMake + if: matrix.os != 'windows-2022' run: | cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} ${{ matrix.extra_cmake_def }} - name: Build run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} + - name: Build Windows Package + if: matrix.os == 'windows-2022' + shell: pwsh + run: | + if ("${{ github.event_name }}" -eq "pull_request") { + cpack --config build/CPackConfig.cmake -G ZIP -B build + } else { + cmake --build ${{ github.workspace }}/build --config ${{ env.BUILD_TYPE }} --target package + } + + - name: Rename CUDA artifacts + if: matrix.build_name == 'windows-2022-cuda' + shell: pwsh + run: Get-ChildItem -Path "build" -Filter "RTABMap-*" | Rename-Item -NewName { $_.BaseName + "_cuda" + $_.Extension } + - name: Info working-directory: ${{github.workspace}}/build/bin run: | ./rtabmap-console --version + - name: Upload RTABMap Artifacts + if: matrix.os == 'windows-2022' + uses: actions/upload-artifact@v4 + with: + name: RTABMap-Binaries-${{ matrix.build_name }} + path: | + build/RTABMap-*.zip + build/RTABMap-*.exe + compression-level: 0 + if-no-files-found: warn + retention-days: ${{ github.event_name == 'pull_request' && 1 || 90 }} + # - name: Test # working-directory: ${{github.workspace}}/build # # Execute tests defined by the CMake configuration. diff --git a/CMakeLists.txt b/CMakeLists.txt index 3265f673..ab2e396a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,6 @@ # Top-Level CmakeLists.txt cmake_minimum_required(VERSION 3.14) + PROJECT( RTABMap ) SET(PROJECT_PREFIX rtabmap) @@ -11,6 +12,7 @@ IF(NOT MULTI_ARCH AND NOT DEFINED CMAKE_INSTALL_LIBDIR) ENDIF(NOT MULTI_ARCH AND NOT DEFINED CMAKE_INSTALL_LIBDIR) INCLUDE(GNUInstallDirs) +INCLUDE(FetchContent) ####### local cmake modules ####### SET(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake_modules") @@ -218,6 +220,7 @@ option(WITH_MADGWICK "Include Madgwick IMU filtering support" ON) option(WITH_FASTCV "Include FastCV support" ON) option(WITH_OPENMP "Include OpenMP support" ON) option(WITH_OPENGV "Include OpenGV support" ON) +option(BUILD_OPENGV "Build OpenGV internally instead of using the system one" OFF) IF(MOBILE_BUILD) option(PCL_OMP "With PCL OMP implementations" OFF) ELSE() @@ -639,7 +642,8 @@ IF(WITH_ZED) IF(CUDA_FOUND) MESSAGE(STATUS "Found CUDA: ${CUDA_INCLUDE_DIRS}") ELSE() - MESSAGE(FATAL_ERROR "CUDA is required to build with Zed sdk! Set -DWITH_ZED=OFF if you don't have CUDA.") + MESSAGE(WARNING "CUDA is required to build with Zed sdk! Set -DWITH_ZED=OFF if you don't have CUDA.") + SET(ZED_FOUND FALSE) ENDIF() ENDIF(ZED_FOUND) ENDIF(WITH_ZED) @@ -784,8 +788,6 @@ IF(WITH_OKVIS) MESSAGE(STATUS "Found okvis: ${OKVIS_INCLUDE_DIRS}") find_package(brisk 2 REQUIRED) MESSAGE(STATUS "Found brisk: ${BRISK_INCLUDE_DIRS}") - find_package(opengv REQUIRED) - MESSAGE(STATUS "Found opengv: ${OPENGV_INCLUDE_DIRS}") find_package(Ceres 1.9.0 REQUIRED EXACT) # OKVIS requires this specific version MESSAGE(STATUS "Found ceres ${Ceres_VERSION}: ${CERES_INCLUDE_DIRS}") ENDIF(okvis_FOUND) @@ -849,12 +851,58 @@ IF(WITH_FASTCV) ENDIF(FastCV_FOUND) ENDIF(WITH_FASTCV) -IF(WITH_OPENGV) - FIND_PACKAGE(opengv QUIET) - IF(opengv_FOUND) - MESSAGE(STATUS "Found OpenGV: ${opengv_INCLUDE_DIRS}") - ENDIF(opengv_FOUND) -ENDIF(WITH_OPENGV) +IF(WITH_OPENGV OR okvis_FOUND) + if(NOT BUILD_OPENGV) + FIND_PACKAGE(opengv QUIET) + endif() + + if(opengv_FOUND) + MESSAGE(STATUS "Found system-installed OpenGV: ${opengv_INCLUDE_DIRS}") + elseif(BUILD_OPENGV) + SET(PCL_USING_MARCHNATIVE OFF) + if(PCL_COMPILE_OPTIONS) + if("${PCL_COMPILE_OPTIONS}" MATCHES "-march=native") + set(PCL_USING_MARCHNATIVE ON) + endif() + elseif("${PCL_DEFINITIONS}" MATCHES "-march=native") + set(PCL_USING_MARCHNATIVE ON) + endif() + SET(MSG_EXTRA "without -march-native (not used by PCL)") + if(PCL_USING_MARCHNATIVE) + set(MSG_EXTRA "with -march-native (used by PCL)") + endif() + + message(STATUS "Download/Build OpenGV internally (BUILD_OPENGV=ON) ${MSG_EXTRA}.") + function(add_submodule_opengv) + FetchContent_Declare( + opengv + GIT_REPOSITORY https://github.com/laurentkneip/opengv.git + GIT_TAG 91f4b19c73450833a40e463ad3648aae80b3a7f3 + PATCH_COMMAND ${CMAKE_COMMAND} + -DPATCH_FILE=${CMAKE_CURRENT_LIST_DIR}/patches/opengv_91f4b19c.patch + -P ${CMAKE_CURRENT_LIST_DIR}/patches/apply_patch.cmake + ) + set(BUILD_SHARED_LIBS OFF) + set(BUILD_TESTS OFF) + set(CMAKE_BUILD_TYPE Release) + set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) + # Eigen should have been already added by PCL, just populate the compatible variables + IF(EIGEN_INCLUDE_DIRS) + set(EIGEN_INCLUDE_DIRS "${EIGEN_INCLUDE_DIRS}" CACHE PATH "Eigen include dirs" FORCE) + set(EIGEN_INCLUDE_DIR "${EIGEN_INCLUDE_DIRS}" CACHE PATH "Eigen include dir" FORCE) + ELSEIF(Eigen3_INCLUDE_DIRS) + set(EIGEN_INCLUDE_DIRS "${Eigen3_INCLUDE_DIRS}" CACHE PATH "Eigen include dirs" FORCE) + set(EIGEN_INCLUDE_DIR "${Eigen3_INCLUDE_DIRS}" CACHE PATH "Eigen include dir" FORCE) + ENDIF() + set(BUILD_WITH_MARCHNATIVE ${PCL_USING_MARCHNATIVE}) + FetchContent_MakeAvailable(opengv) + endfunction() + + add_submodule_opengv() + set(opengv_FOUND TRUE) + set(opengv_VERSION "internal") + endif() +ENDIF() IF(WITH_ORB_SLAM AND NOT G2O_FOUND) FIND_PACKAGE(ORB_SLAM) @@ -1285,7 +1333,17 @@ install(FILES package.xml DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/${PROJECT_PR ####################### IF(BUILD_AS_BUNDLE) SET(CMAKE_INSTALL_SYSTEM_RUNTIME_COMPONENT runtime) + IF(WIN32) + set(CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP TRUE) + set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS " + ExecWait '\\\"$INSTDIR\\\\vc_redist.x64.exe\\\" /quiet /norestart' + ") + ENDIF() INCLUDE(InstallRequiredSystemLibraries) + set(CPACK_NSIS_COMPONENT_INSTALL OFF) + set(CPACK_ARCHIVE_COMPONENT_INSTALL ON) + set(CPACK_COMPONENTS_GROUPING ALL_COMPONENTS_IN_ONE) + set(CPACK_COMPONENTS_ALL runtime) ENDIF(BUILD_AS_BUNDLE) SET(CPACK_PACKAGE_NAME "${PROJECT_NAME}") @@ -1604,7 +1662,11 @@ MESSAGE(STATUS " With Open3D = NO (Open3D not found)") ENDIF() IF(opengv_FOUND AND WITH_OPENGV) +IF(opengv_VERSION STREQUAL "internal") +MESSAGE(STATUS " With OpenGV (internal) = YES (License: BSD)") +ELSE() MESSAGE(STATUS " With OpenGV ${opengv_VERSION} = YES (License: BSD)") +ENDIF() ELSEIF(NOT WITH_OPENGV) MESSAGE(STATUS " With OpenGV = NO (WITH_OPENGV=OFF)") ELSE() @@ -1719,7 +1781,7 @@ ELSE() MESSAGE(STATUS " With FlyCapture2/Triclops = NO (Point Grey SDK not found)") ENDIF() -IF(ZED_FOUND AND CUDA_FOUND) +IF(ZED_FOUND) MESSAGE(STATUS " With ZED = YES") ELSEIF(NOT WITH_ZED) MESSAGE(STATUS " With ZED = NO (WITH_ZED=OFF)") diff --git a/app/ios/RTABMapApp/install_deps.sh b/app/ios/RTABMapApp/install_deps.sh index 0c575097..6eac2b07 100755 --- a/app/ios/RTABMapApp/install_deps.sh +++ b/app/ios/RTABMapApp/install_deps.sh @@ -288,6 +288,6 @@ cmake -DANDROID_PREBUILD=ON ../../../../.. cmake --build . --config Release mkdir -p ios cd ios -cmake -G Xcode -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_SYSROOT=$sysroot -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_DEPLOYMENT_TARGET=12.0 -DCMAKE_INSTALL_PREFIX=$prefix -DCMAKE_FIND_ROOT_PATH=$prefix -DWITH_QT=OFF -DBUILD_APP=OFF -DBUILD_TOOLS=OFF -DWITH_TORO=OFF -DWITH_VERTIGO=OFF -DWITH_MADGWICK=OFF -DWITH_ORB_OCTREE=ON -DBUILD_EXAMPLES=OFF -DWITH_LIBLAS=ON ../../../../../.. +cmake -G Xcode -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_SYSROOT=$sysroot -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_DEPLOYMENT_TARGET=12.0 -DCMAKE_INSTALL_PREFIX=$prefix -DCMAKE_FIND_ROOT_PATH=$prefix -DWITH_QT=OFF -DBUILD_APP=OFF -DBUILD_TOOLS=OFF -DWITH_TORO=OFF -DWITH_VERTIGO=OFF -DWITH_MADGWICK=OFF -DWITH_ORB_OCTREE=ON -DBUILD_EXAMPLES=OFF -DWITH_LIBLAS=ON -DWITH_OPENGV=OFF ../../../../../.. cmake --build . --config Release cmake --build . --config Release --target install diff --git a/app/src/CMakeLists.txt b/app/src/CMakeLists.txt index a6600f70..a58eeb3e 100644 --- a/app/src/CMakeLists.txt +++ b/app/src/CMakeLists.txt @@ -95,9 +95,11 @@ IF(BUILD_AS_BUNDLE AND (APPLE OR WIN32)) DESTINATION ${thirdparty_dest_dir} COMPONENT runtime REGEX ".*pdb" EXCLUDE) - INSTALL(FILES "${OpenNI2_BIN_DIR}/OpenNI.ini" + IF(NOT WIN32) + INSTALL(FILES "${OpenNI2_BIN_DIR}/OpenNI.ini" DESTINATION ${thirdparty_dest_dir} COMPONENT runtime) + ENDIF() ENDIF(OpenNI2_FOUND) IF(k4a_FOUND) @@ -138,23 +140,107 @@ IF(BUILD_AS_BUNDLE AND (APPLE OR WIN32)) ENDIF(OrbbecSDK_FOUND) IF(Torch_FOUND) - # Install needed cudnn_ops_infer64_8.dll and cudnn_cnn_infer64_8.dll - # TODO: should be a more general way to include them if version is different + # Install needed cudnn dlls IF(WIN32 AND CUDA_FOUND) - find_file(CUDNN_OPS_DLL NAMES cudnn_ops_infer64_8.dll) - find_file(CUDNN_CNN_DLL NAMES cudnn_cnn_infer64_8.dll) - IF(CUDNN_OPS_DLL AND CUDNN_CNN_DLL) - MESSAGE(STATUS "Found ${CUDNN_OPS_DLL}") - MESSAGE(STATUS "Found ${CUDNN_CNN_DLL}") - INSTALL(FILES ${CUDNN_OPS_DLL} ${CUDNN_CNN_DLL} - DESTINATION ${thirdparty_dest_dir} - COMPONENT runtime) - ELSE() - MESSAGE(AUTHOR_WARNING "Using Torch with CUDA, but cudnn_ops_infer64_8.dll and cudnn_cnn_infer64_8.dll are not found on the PATH, so it won't be added to package.") + find_path(cuDNN_BIN_DIR NAMES cudnn.dll cudnn64.dll cudnn64_9.dll) + IF(NOT cuDNN_BIN_DIR) + MESSAGE(FATAL_ERROR "cudnn dlls not found! Make sure the dlls are in a directory on your PATH.") + ENDIF(NOT cuDNN_BIN_DIR) + MESSAGE(STATUS "cuDNN_BIN_DIR = ${cuDNN_BIN_DIR}") + file(GLOB CUDNN_DLLS "${cuDNN_BIN_DIR}/cudnn*.dll") + IF(CUDNN_DLLS) + MESSAGE(STATUS "Found cuDNN DLLs: ${CUDNN_DLLS}") + INSTALL(FILES ${CUDNN_DLLS} + DESTINATION ${thirdparty_dest_dir} + COMPONENT runtime) ENDIF() ENDIF(WIN32 AND CUDA_FOUND) ENDIF(Torch_FOUND) + + set(python_pyd_dir "") + IF(Python3_FOUND) + # bundle python3 + IF(WIN32) + set(python_pyd_dir "bin/Lib/site-packages") + set(PYTHON_ZIP_NAME "python${Python3_VERSION_MAJOR}${Python3_VERSION_MINOR}.zip") + get_filename_component(PYTHON_ROOT "${Python3_EXECUTABLE}" DIRECTORY) + file(TO_CMAKE_PATH "${Python3_STDLIB}" SANITIZED_STDLIB) + MESSAGE(STATUS "Python3_EXECUTABLE=${Python3_EXECUTABLE}") + MESSAGE(STATUS "Python3_STDLIB=${SANITIZED_STDLIB}") + + install(FILES "${Python3_EXECUTABLE}" DESTINATION bin COMPONENT runtime) + + # when using python-opencv, it expects python3.dll, not python312.dll + get_filename_component(VCPKG_TRIPLET_ROOT "${PYTHON_TOOLS_DIR}/../../" ABSOLUTE) + set(VCPKG_BIN_DIR "${VCPKG_TRIPLET_ROOT}/bin") + find_file(PYTHON3_STABLE_DLL + NAMES python3.dll + PATHS "${VCPKG_BIN_DIR}" + NO_DEFAULT_PATH + ) + if(PYTHON3_STABLE_DLL) + message(STATUS "Found python3.dll at: ${PYTHON3_STABLE_DLL}") + install(FILES "${PYTHON3_STABLE_DLL}" DESTINATION bin COMPONENT runtime) + endif() + + # install python Lib in python312.zip (without site-packages, which is installed separatly afterwards) + file(TO_CMAKE_PATH "${Python3_STDLIB}" SANITIZED_STDLIB) + file(GLOB LIB_CONTENTS RELATIVE "${SANITIZED_STDLIB}" "${SANITIZED_STDLIB}/*") + list(REMOVE_ITEM LIB_CONTENTS "site-packages") + install(CODE " + execute_process( + COMMAND \"${CMAKE_COMMAND}\" -E tar cf \"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/bin/${PYTHON_ZIP_NAME}\" --format=zip -- ${LIB_CONTENTS} + WORKING_DIRECTORY \"${SANITIZED_STDLIB}\" + ) + " COMPONENT runtime) + set(PYTHON_SITE_PACKAGES "${SANITIZED_STDLIB}/site-packages") + + install(DIRECTORY "${PYTHON_SITE_PACKAGES}/" + DESTINATION "${thirdparty_dest_dir}/Lib/site-packages" + COMPONENT runtime + PATTERN "*.exe" EXCLUDE + PATTERN "*.lib" EXCLUDE + PATTERN "*.hpp" EXCLUDE + PATTERN "*.h" EXCLUDE + PATTERN "*/torch/*" EXCLUDE + ) + if(EXISTS "${PYTHON_SITE_PACKAGES}/torch") + install(DIRECTORY "${PYTHON_SITE_PACKAGES}/torch" + DESTINATION "${thirdparty_dest_dir}/Lib/site-packages/" + COMPONENT runtime + PATTERN "*.exe" EXCLUDE + PATTERN "*.lib" EXCLUDE + PATTERN "*.hpp" EXCLUDE + PATTERN "*.h" EXCLUDE + PATTERN "*.dll" EXCLUDE + ) + file(GLOB_RECURSE PY_DLL_FILES "${PYTHON_SITE_PACKAGES}/torch/*.dll") + if(PY_DLL_FILES) + install(FILES ${PY_DLL_FILES} DESTINATION bin COMPONENT runtime) + endif() + endif() + + # install python DDLs + file(GLOB_RECURSE PY_DLL_FILES \"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${python_pyd_dir}/*.dll\") + install(DIRECTORY "${PYTHON_ROOT}/DLLs" + DESTINATION "${thirdparty_dest_dir}/" + COMPONENT runtime + FILES_MATCHING + PATTERN "*.dll" + PATTERN "*.pyd" + ) + + # install our python scripts in share for convenience + install(DIRECTORY "${PROJECT_SOURCE_DIR}/corelib/src/python/" + DESTINATION share + COMPONENT runtime + FILES_MATCHING + PATTERN "*.py" + ) + ENDIF(WIN32) + ENDIF(Python3_FOUND) + IF(Qt6_FOUND) # Reference: https://doc-snapshots.qt.io/qt6-6.4/qt-deploy-runtime-dependencies.html # The following script must only be executed at install time @@ -248,8 +334,8 @@ IF(BUILD_AS_BUNDLE AND (APPLE OR WIN32)) SET(DIRS "${QT_LIBRARY_DIRS}" "\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/lib") IF(APPLE) SET(DIRS ${DIRS} /usr/local /usr/local/lib /opt/homebrew /opt/homebrew/lib /opt/homebrew/lib/gcc/current) - ENDIF(APPLE) - + ENDIF(APPLE) + # Now the work of copying dependencies into the bundle/package # The quotes are escaped and variables to use at install time have their $ escaped # An alternative is the do a configure_file() on a script and use install(SCRIPT ...). @@ -257,11 +343,26 @@ IF(BUILD_AS_BUNDLE AND (APPLE OR WIN32)) # over. # To find dependencies, cmake use "otool" on Apple and "dumpbin" on Windows (make sure you have one of them). install(CODE " - file(GLOB_RECURSE QTPLUGINS \"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${plugin_dest_dir}/*${CMAKE_SHARED_LIBRARY_SUFFIX}\") + # Glob Qt Plugins + file(GLOB_RECURSE ALL_LIBS \"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${plugin_dest_dir}/*${CMAKE_SHARED_LIBRARY_SUFFIX}\") + + if(NOT \"${python_pyd_dir}\" STREQUAL \"\") + file(GLOB_RECURSE PYD_FILES \"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${python_pyd_dir}/*.pyd\") + if(PYD_FILES) + list(APPEND ALL_LIBS \${PYD_FILES}) + endif() + if(WIN32) + file(GLOB_RECURSE DLL_FILES \"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${python_pyd_dir}/torch/*.dll\") + if(DLL_FILES) + list(APPEND ALL_LIBS \${DLL_FILES}) + endif() + endif() + endif() + set(BU_CHMOD_BUNDLE_ITEMS ON) include(\"BundleUtilities\") - fixup_bundle(\"${APPS}\" \"\${QTPLUGINS}\" \"${DIRS}\") - " COMPONENT runtime) - + fixup_bundle(\"${APPS}\" \"\${ALL_LIBS}\" \"${DIRS}\") + " COMPONENT runtime) + ENDIF(BUILD_AS_BUNDLE AND (APPLE OR WIN32)) diff --git a/bundle_windows_deps.bat b/bundle_windows_deps.bat new file mode 100644 index 00000000..7f255557 --- /dev/null +++ b/bundle_windows_deps.bat @@ -0,0 +1,227 @@ +@echo off +setlocal enabledelayedexpansion + +:: --- CONFIGURATION --- +set "VCPKG_ROOT=%~dp0vcpkg" +set "EXPORT_DIR=%~dp0vcpkg_binaries" +set "TRIPLET=x64-windows-release" +set "SEVENZIP_EXE=C:\Program Files\7-Zip\7z.exe" + +set "VCPKG_JSON=%~dp0vcpkg.json" +for /f "usebackq tokens=*" %%a in (`powershell -NoProfile -Command "(Get-Content '%VCPKG_JSON%' -Raw | ConvertFrom-Json).'builtin-baseline'"` ) do set "VCPKG_COMMIT=%%a" +echo [+] Detected VCPKG baseline commit: %VCPKG_COMMIT% + +if "%VCPKG_COMMIT%"=="" ( + echo [X] Error: Could not find 'builtin-baseline' in %VCPKG_JSON% + pause + exit /b 1 +) + +set VCPKG_COMMIT_SHORT=%VCPKG_COMMIT:~0,8% + +:: 1. Setup Local vcpkg +if not exist "%VCPKG_ROOT%" ( + echo [+] Local vcpkg not found. Cloning... + git clone https://github.com/microsoft/vcpkg.git "%VCPKG_ROOT%" +) +pushd "%VCPKG_ROOT%" +git checkout %VCPKG_COMMIT% +call .\bootstrap-vcpkg.bat +popd + +:: 2. Install vcpkg dependencies +echo [+] Installing dependencies via vcpkg manifest... +"%VCPKG_ROOT%\vcpkg.exe" install ^ + --triplet=%TRIPLET% ^ + --host-triplet=%TRIPLET% ^ + --clean-after-build ^ + --x-feature=tools ^ + --x-feature=k4w2 ^ + --x-feature=octomap ^ + --x-feature=openmp ^ + --x-feature=realsense2 ^ + --x-feature=openni2 ^ + --x-feature=gtsam-deps ^ + --x-feature=python ^ + --x-feature=libpointmatcher-deps || exit /b !errorlevel! + +:: 3. Export +echo [+] Exporting built binaries to raw folder... +set "VS_LOCATOR=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" +for /f "usebackq tokens=*" %%i in (`"%VS_LOCATOR%" -latest -property catalog_productLineVersion`) do set VS_YEAR=vs%%i +set TARGET_NAME=vcpkg-export-%VCPKG_COMMIT_SHORT%-x64-%VS_YEAR% +set TARGET_FULL_PATH=%EXPORT_DIR%\%TARGET_NAME% +if exist "%TARGET_FULL_PATH%" rd /s /q "%TARGET_FULL_PATH%" +"%VCPKG_ROOT%\vcpkg.exe" export --raw --output-dir="%EXPORT_DIR%" --triplet=%TRIPLET% || exit /b !errorlevel! + +:: Find the actual exported folder name (it usually contains a date/hash) +for /d %%i in ("%EXPORT_DIR%\vcpkg-export-20??????-??????") do set "FINAL_EXPORT_PATH=%%i" + +echo [+] Rename folder %FINAL_EXPORT_PATH% to %TARGET_NAME% +ren "%FINAL_EXPORT_PATH%" "%TARGET_NAME%" || exit /b !errorlevel! +set "FINAL_EXPORT_PATH=%TARGET_FULL_PATH%" + +echo [+] Add numpy... +:: We install numpy<2 to be compatible with SuperPoint and SuperGlue scripts +%FINAL_EXPORT_PATH%/installed/%TRIPLET%/tools/python3/python.exe -m ensurepip --upgrade || exit /b %errorlevel% +%FINAL_EXPORT_PATH%/installed/%TRIPLET%/tools/python3/python.exe -m pip install --upgrade pip || exit /b !errorlevel! +%FINAL_EXPORT_PATH%/installed/%TRIPLET%/tools/python3/python.exe -m pip install "numpy<2" || exit /b !errorlevel! + +:: 4. Other dependencies not in vcpkg +:: libnabo +echo [+] Building libnabo... +if not exist libnabo ( + echo [+] Downloading... + git clone https://github.com/ethz-asl/libnabo.git + cd libnabo + :: Jan 27, 2022 + git checkout c925c47 + git apply ../patches/libnabo_c925c47.patch + cd .. +) +cd libnabo +cmake -S . -B build -GNinja ^ + -DVCPKG_MANIFEST_INSTALL=OFF ^ + -DVCPKG_TARGET_TRIPLET=x64-windows-release ^ + -DVCPKG_INSTALLED_DIR="%FINAL_EXPORT_PATH%\installed" ^ + -DCMAKE_TOOLCHAIN_FILE="%FINAL_EXPORT_PATH%\scripts\buildsystems\vcpkg.cmake" ^ + -DCMAKE_INSTALL_PREFIX="%FINAL_EXPORT_PATH%\installed\%TRIPLET%" ^ + -DCMAKE_BUILD_TYPE=Release ^ + -DSHARED_LIBS=FALSE ^ + -DLIBNABO_BUILD_DOXYGEN=OFF ^ + -DLIBNABO_BUILD_EXAMPLES=OFF ^ + -DLIBNABO_BUILD_PYTHON=OFF ^ + -DLIBNABO_BUILD_TESTS=OFF || exit /b !errorlevel! +cmake --build build --config Release --target install || exit /b !errorlevel! +cd .. + +:: libpointmatcher +echo [+] Building libpointmatcher... +if not exist libpointmatcher ( + echo [+] Downloading and applying patch... + git clone https://github.com/ethz-asl/libpointmatcher.git + cd libpointmatcher + :: Mar 17, 2023 + git checkout 7dc58e5 + git apply ../patches/pointmatcher_7dc58e5.patch + cd .. +) +cd libpointmatcher +cmake -S . -B build -GNinja ^ + -DVCPKG_MANIFEST_INSTALL=OFF ^ + -DVCPKG_TARGET_TRIPLET=x64-windows-release ^ + -DVCPKG_INSTALLED_DIR="%FINAL_EXPORT_PATH%\installed" ^ + -DCMAKE_TOOLCHAIN_FILE="%FINAL_EXPORT_PATH%\scripts\buildsystems\vcpkg.cmake" ^ + -DCMAKE_INSTALL_PREFIX="%FINAL_EXPORT_PATH%\installed\%TRIPLET%" ^ + -DCMAKE_BUILD_TYPE=Release ^ + -DBUILD_TESTS=OFF ^ + -DBUILD_SHARED_LIBS=ON ^ + -DPOINTMATCHER_BUILD_EVALUATIONS=OFF ^ + -DPOINTMATCHER_BUILD_EXAMPLES=OFF ^ + -DCMAKE_CXX_FLAGS="-DBOOST_TIMER_ENABLE_DEPRECATED /EHsc -DBOOST_EXCEPTION_DISABLE" || exit /b !errorlevel! +cmake --build build --config Release --target install || exit /b !errorlevel! +cd .. + +:: We remove the files in the top-level CMake directory to force use of share/libpointmatcher/cmake +if exist "%FINAL_EXPORT_PATH%\installed\%TRIPLET%\CMake\" ( + rd /s /q "%FINAL_EXPORT_PATH%\installed\%TRIPLET%\CMake" +) + +:: gtsam +echo [+] Building gtsam... +if not exist gtsam ( + echo [+] Downloading and applying patch... + git clone https://github.com/borglab/gtsam.git + cd gtsam + :: June 18, 2025 + git checkout 4.3a0-ros + git cherry-pick 18af4e6 + git apply ../patches/gtsam_4_3a0-ros.patch + cd .. +) +cd gtsam +cmake -S . -B build -GNinja ^ + -DVCPKG_MANIFEST_INSTALL=OFF ^ + -DVCPKG_TARGET_TRIPLET=x64-windows-release ^ + -DVCPKG_INSTALLED_DIR="%FINAL_EXPORT_PATH%\installed" ^ + -DCMAKE_TOOLCHAIN_FILE="%FINAL_EXPORT_PATH%\scripts\buildsystems\vcpkg.cmake" ^ + -DCMAKE_INSTALL_PREFIX="%FINAL_EXPORT_PATH%\installed\%TRIPLET%" ^ + -DCMAKE_BUILD_TYPE=Release ^ + -DGTSAM_BUILD_EXAMPLES_ALWAYS=OFF ^ + -DGTSAM_BUILD_TESTS=OFF ^ + -DGTSAM_BUILD_UNSTABLE=OFF ^ + -DGTSAM_USE_SYSTEM_EIGEN=ON ^ + -DGTSAM_BUILD_WITH_PRECOMPILED_HEADERS=OFF ^ + -DGTSAM_UNSTABLE_BUILD_PYTHON=OFF ^ + -DGTSAM_WITH_EIGEN_MKL=OFF ^ + -DGTSAM_WITH_EIGEN_MKL_OPENMP=OFF ^ + -DCMAKE_CXX_FLAGS="-DBOOST_TIMER_ENABLE_DEPRECATED -DBOOST_BIND_GLOBAL_PLACEHOLDERS" || exit /b !errorlevel! +cmake --build build --config Release --target install || exit /b !errorlevel! +cd .. + +:: opengv +echo [+] Building opengv... +if not exist opengv ( + echo [+] Downloading and applying patch... + git clone https://github.com/laurentkneip/opengv.git + cd opengv + :: Aug 6, 2020 + git checkout 91f4b19c73450833a40e463ad3648aae80b3a7f3 + git apply ../patches/opengv_91f4b19c.patch + cd .. +) +cd opengv +cmake -S . -B build -GNinja ^ + -DVCPKG_MANIFEST_INSTALL=OFF ^ + -DVCPKG_TARGET_TRIPLET=x64-windows-release ^ + -DVCPKG_INSTALLED_DIR="%FINAL_EXPORT_PATH%\installed" ^ + -DCMAKE_TOOLCHAIN_FILE="%FINAL_EXPORT_PATH%\scripts\buildsystems\vcpkg.cmake" ^ + -DCMAKE_INSTALL_PREFIX="%FINAL_EXPORT_PATH%\installed\%TRIPLET%" ^ + -DBUILD_TESTS=OFF ^ + -DBUILD_SHARED_LIBS=ON || exit /b !errorlevel! +cmake --build build --config Release --target install || exit /b !errorlevel! +cd .. + +:: 5. ZIP the folder +echo [+] Creating final package with 7-Zip... +:: Rip off pdb files +cd /d "%FINAL_EXPORT_PATH%" +del /s /q /f *.pdb >nul 2>&1 + +cd .. + +set "FINAL_ZIP=%TARGET_NAME%.7z" + +:: compress contents without the root folder +"%SEVENZIP_EXE%" u -t7z -mx9 "%FINAL_ZIP%" "%FINAL_EXPORT_PATH%\*" -up0q0 + +if !errorlevel! EQU 0 ( + echo [!] Success! Package created at %FINAL_ZIP% +) else ( + echo [X] 7-Zip failed with error code !errorlevel! +) + + +:: Example building rtabmap afterwards +goto :EndComment + +set VCPKG_UNZIPPED_EXPORT_PATH=%USERPROFILE%\Downloads\vcpkg-export-########-x64-vs2022 +set PATH=%VCPKG_UNZIPPED_EXPORT_PATH%\installed\x64-windows-release\bin;%PATH% + +cmake -B build -GNinja ^ + -DCMAKE_BUILD_TYPE=Release ^ + -DBUILD_AS_BUNDLE=ON -DWITH_PYTHON=ON ^ + -DWITH_ZED=OFF ^ + -DVCPKG_MANIFEST_INSTALL=OFF ^ + -DVCPKG_TARGET_TRIPLET=x64-windows-release ^ + -DVCPKG_INSTALLED_DIR="%VCPKG_UNZIPPED_EXPORT_PATH%/installed" ^ + -DCMAKE_TOOLCHAIN_FILE=%VCPKG_UNZIPPED_EXPORT_PATH%/scripts/buildsystems/vcpkg.cmake ^ + -DGTSAM_DIR=%VCPKG_UNZIPPED_EXPORT_PATH%\installed\x64-windows-release\CMake + +cmake --build build --config Release --target package + +:: To install CPU pytorch inside rtabmap package afterwards. +:: Note that python.exe is the one in the bin directory of the package, not the system one. +python.exe -m pip install torch torchvision opencv-python-headless "numpy<2" + +:EndComment diff --git a/bundle_windows_deps_cuda.bat b/bundle_windows_deps_cuda.bat new file mode 100644 index 00000000..3ba4d2fc --- /dev/null +++ b/bundle_windows_deps_cuda.bat @@ -0,0 +1,239 @@ +@echo off +setlocal enabledelayedexpansion + +IF NOT DEFINED CUDA_PATH ( + echo [ERROR] CUDA_PATH is not set. + exit /b 1 +) + +set PATH=%CUDA_PATH%\bin;%PATH% +set PATH=%CUDA_PATH%\bin\x64;%PATH% +set PATH=%CUDA_PATH%\extras\CUPTI\lib64;%PATH% + +:: CUDA Toolkit should be manually installed on the computer before running this script +:: We assume also that cuDNN is merged into CUDA installed directory. +where nvcc >nul 2>&1 +if !errorlevel! neq 0 ( + echo [ERROR] nvcc was not found in your PATH. + pause + exit /b +) +for /f "tokens=5" %%a in ('nvcc --version ^| findstr "release"') do ( + set "RAW_VER=%%a" + :: This removes the trailing comma + set "CUDA_VER=!RAW_VER:,=!" + set "CUDA_VER_SHORT=!CUDA_VER:.=!" +) +if "!CUDA_VER!"=="" ( + echo [ERROR] Could not parse CUDA version. + pause + exit /b +) +echo Installed CUDA Toolkit: %CUDA_VER% + +:: --- CONFIGURATION --- +set "VCPKG_ROOT=%~dp0vcpkg" +set "EXPORT_DIR=%~dp0vcpkg_binaries" +set "TRIPLET=x64-windows-release" +set "SEVENZIP_EXE=C:\Program Files\7-Zip\7z.exe" + +set "VCPKG_JSON=%~dp0vcpkg.json" +for /f "usebackq tokens=*" %%a in (`powershell -NoProfile -Command "(Get-Content '%VCPKG_JSON%' -Raw | ConvertFrom-Json).'builtin-baseline'"` ) do set "VCPKG_COMMIT=%%a" + +if "%VCPKG_COMMIT%"=="" ( + echo [X] Error: Could not find 'builtin-baseline' in %VCPKG_JSON% + pause + exit /b 1 +) + +set VCPKG_COMMIT_SHORT=%VCPKG_COMMIT:~0,8% +set "VS_LOCATOR=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" +for /f "usebackq tokens=*" %%i in (`"%VS_LOCATOR%" -latest -property catalog_productLineVersion`) do set VS_YEAR=vs%%i +set ORG_TARGET_NAME=vcpkg-export-%VCPKG_COMMIT_SHORT%-x64-%VS_YEAR% +set TARGET_NAME=%ORG_TARGET_NAME%-cuda%CUDA_VER_SHORT% +set VCPKG_EXPORT_PATH=%EXPORT_DIR%\%ORG_TARGET_NAME% +set FINAL_EXPORT_PATH=%EXPORT_DIR%\%TARGET_NAME% + +if not exist "%FINAL_EXPORT_PATH%" ( + if not exist "%VCPKG_EXPORT_PATH%" ( + call bundle_windows_deps.bat || exit /b !errorlevel! + ) + echo [+] Copying %VCPKG_EXPORT_PATH% to %FINAL_EXPORT_PATH% + xcopy "%VCPKG_EXPORT_PATH%" "%FINAL_EXPORT_PATH%\" /E /I /H /Y /Q || exit /b !errorlevel! + echo [+] Remove opencv built by vcpkg + rmdir /s /q "%FINAL_EXPORT_PATH%\installed\%TRIPLET%\include\opencv4" || exit /b !errorlevel! + rmdir /s /q "%FINAL_EXPORT_PATH%\installed\%TRIPLET%\share\opencv4" || exit /b !errorlevel! + rmdir /s /q "%FINAL_EXPORT_PATH%\installed\%TRIPLET%\share\opencv" || exit /b !errorlevel! + del "%FINAL_EXPORT_PATH%\installed\%TRIPLET%\bin\opencv*" || exit /b !errorlevel! + del "%FINAL_EXPORT_PATH%\installed\%TRIPLET%\lib\opencv*" || exit /b !errorlevel! + :: bundle cudnn runtime libraries + xcopy "%CUDA_PATH%\bin\x64\cudnn*.dll" "%FINAL_EXPORT_PATH%\installed\%TRIPLET%\bin\" /Y +) + +:: pytorch deps +%FINAL_EXPORT_PATH%/installed/%TRIPLET%/tools/python3/python.exe -m pip install numpy packaging "setuptools<82" pyyaml typing_extensions + +git config --global core.longpaths true + +:: pytorch, build with local cuda libraries to avoid duplicating them when we install rtabmap +echo [+] Building pytorch with cuda support... +if not exist pytorch ( + echo [+] Downloading pytorch... + git clone https://github.com/pytorch/pytorch || exit /b !errorlevel! + cd pytorch + :: Jan 21, 2026 + git checkout v2.10.0 + git submodule update --init --recursive || exit /b !errorlevel! + cd .. +) + +set "PYTHONHOME=%FINAL_EXPORT_PATH%\installed\%TRIPLET%\tools\python3" +set "Python_ROOT_DIR=%FINAL_EXPORT_PATH%\installed\%TRIPLET%" +set CMAKE_GENERATOR=Ninja +set BUILD_TEST=0 +set ATEN_NO_TEST=1 +set INSTALL_TEST=OFF +set "LIB=%FINAL_EXPORT_PATH%\installed\%TRIPLET%\lib;%LIB%" +set "%FINAL_EXPORT_PATH%\installed\%TRIPLET%\include;INCLUDE=%INCLUDE%" + +:: check if torch is installed +%PYTHONHOME%/python.exe -m pip show torch >nul 2>&1 +if !errorlevel! neq 0 ( + cd pytorch + %PYTHONHOME%/python.exe setup.py install || exit /b !errorlevel! + cd .. +) +if exist "%PYTHONHOME%\Lib\site-packages\torch\test" rd /s /q %PYTHONHOME%\Lib\site-packages\torch\test" +del "%PYTHONHOME%\Lib\site-packages\torch\bin\test_*" || exit /b !errorlevel! + +echo [+] Building torchvision... +if not exist torchvision ( + echo [+] Downloading torchvision... + git clone https://github.com/pytorch/vision.git torchvision || exit /b !errorlevel! + cd torchvision + :: Jan 6, 2026 + git checkout v0.25.0 + git submodule update --init --recursive || exit /b !errorlevel! + cd .. +) +cd torchvision +set PATH=%FINAL_EXPORT_PATH%\installed\%TRIPLET%\bin;%PATH% +set DISTUTILS_USE_SDK=1 +set TORCHVISION_INCLUDE=%FINAL_EXPORT_PATH%\installed\%TRIPLET%\include +set TORCHVISION_LIBRARY=%FINAL_EXPORT_PATH%\installed\%TRIPLET%\lib +%FINAL_EXPORT_PATH%/installed/%TRIPLET%/tools/python3/python.exe -m pip install . -v --no-build-isolation || exit /b !errorlevel! +cd .. + +:: opencv_cuda +echo [+] Building opencv with cuda support... +if not exist opencv ( + echo [+] Downloading opencv... + git clone https://github.com/opencv/opencv.git || exit /b !errorlevel! + cd opencv + :: 4.13.0 minimum required to be compatible with cuda 13 + :: Dec 31, 2025 + git checkout 4.13.0 + cd .. +) +if not exist opencv_contrib ( + echo [+] Downloading opencv_contrib... + git clone https://github.com/opencv/opencv_contrib.git || exit /b !errorlevel! + cd opencv + :: 4.13.0 minimum required to be compatible with cuda 13 + :: Dec 31, 2025 + git checkout 4.13.0 + cd .. +) +cd opencv +cmake -S . -B build -GNinja ^ + -DVCPKG_MANIFEST_INSTALL=OFF ^ + -DVCPKG_TARGET_TRIPLET=%TRIPLET% ^ + -DVCPKG_INSTALLED_DIR="%FINAL_EXPORT_PATH%\installed" ^ + -DCMAKE_TOOLCHAIN_FILE="%FINAL_EXPORT_PATH%\scripts\buildsystems\vcpkg.cmake" ^ + -DCMAKE_INSTALL_PREFIX="%FINAL_EXPORT_PATH%\installed\%TRIPLET%" ^ + -DCMAKE_BUILD_TYPE=Release ^ + -DOPENCV_BIN_INSTALL_PATH="bin" ^ + -DOPENCV_LIB_INSTALL_PATH="lib" ^ + -DOPENCV_CONFIG_INSTALL_PATH="share/opencv" ^ + -DOPENCV_EXTRA_MODULES_PATH=../opencv_contrib/modules ^ + -DBUILD_SHARED_LIBS=ON ^ + -DBUILD_TESTS=OFF ^ + -DBUILD_PERF_TESTS=OFF ^ + -DOPENCV_ENABLE_NONFREE=ON ^ + -DBUILD_opencv_apps=OFF ^ + -DBUILD_opencv_python3=ON ^ + -DPYTHON3_EXECUTABLE=%FINAL_EXPORT_PATH%/installed/%TRIPLET%/tools/python3/python.exe ^ + -DPYTHON3_PACKAGES_PATH=bin/Lib/site-packages ^ + -DBUILD_opencv_java_bindings_generator=OFF ^ + -DWITH_CUDA=ON ^ + -DWITH_VTK=OFF ^ + -DWITH_TBB=ON || exit /b !errorlevel! +cmake --build build --config Release --target install || exit /b !errorlevel! +:: move cv2 package under tools/python3 +robocopy "%FINAL_EXPORT_PATH%\installed\%TRIPLET%\bin\Lib" "%FINAL_EXPORT_PATH%\installed\%TRIPLET%\tools\python3\Lib" /E /MOVE /NFL /NDL /NJH /NC /NS /NP +cd .. + + +:: 5. ZIP the folder +echo [+] Creating final package with 7-Zip... +:: Rip off pdb files +cd /d "%FINAL_EXPORT_PATH%" +del /s /q /f *.pdb >nul 2>&1 +cd .. + +set "FINAL_ZIP=%TARGET_NAME%.7z" + +:: compress contents without the root folder +"%SEVENZIP_EXE%" u -t7z -mx9 "%FINAL_ZIP%" "%FINAL_EXPORT_PATH%\*" -up0q0 || exit /b !errorlevel! + +if !errorlevel! EQU 0 ( + echo [!] Success! Package created at %FINAL_ZIP% +) else ( + echo [X] 7-Zip failed with error code !errorlevel! +) + +:: Example building rtabmap with opencv cuda and libtorch afterwards +goto :EndComment + +:: Set path of unzipped deps +set VCPKG_UNZIPPED_EXPORT_PATH= + +set TRIPLET=x64-windows-release +set PATH=%VCPKG_UNZIPPED_EXPORT_PATH%\installed\%TRIPLET%\bin;%PATH% +set PATH=%VCPKG_UNZIPPED_EXPORT_PATH%\installed\%TRIPLET%\tools\python3;%PATH% +set PATH=%CUDA_PATH%\bin;%PATH% +set PATH=%CUDA_PATH%\bin\x64;%PATH% +set PATH=%CUDA_PATH%\extras\CUPTI\lib64;%PATH% +set PATH=%VCPKG_UNZIPPED_EXPORT_PATH%\installed\%TRIPLET%\tools\python3\Lib\site-packages\torch\lib;%PATH% +set PATH=%VCPKG_UNZIPPED_EXPORT_PATH%\installed\%TRIPLET%\tools\python3\Lib\site-packages\numpy.libs;%PATH% + +:: Other dependencies + +:: For ZED, modify zed-config.cmake and remove all dependencies +set PATH=%PATH%;%ZED_SDK_ROOT_DIR%\bin + +:: For kinect 4 windows SDK v2, move kinect20.dll from system32 to KINECTSDK20_DIR\bin +:: For kinect 4 windows SDK v1, move kinect10.dll and KinectAudio10.dll to KINECTSDK20_DIR\bin +set PATH=%PATH%;%KINECTSDK20_DIR%\bin + +cmake -B build_cuda -GNinja ^ + -DCMAKE_BUILD_TYPE=Release ^ + -DBUILD_AS_BUNDLE=ON ^ + -DWITH_PYTHON=ON ^ + -DWITH_TORCH=ON ^ + -DWITH_ZED=ON ^ + -DVCPKG_MANIFEST_INSTALL=OFF ^ + -DVCPKG_TARGET_TRIPLET=%TRIPLET% ^ + -DVCPKG_INSTALLED_DIR="%VCPKG_UNZIPPED_EXPORT_PATH%/installed" ^ + -DCMAKE_TOOLCHAIN_FILE=%VCPKG_UNZIPPED_EXPORT_PATH%/scripts/buildsystems/vcpkg.cmake ^ + -DGTSAM_DIR=%VCPKG_UNZIPPED_EXPORT_PATH%\installed\%TRIPLET%\CMake ^ + -DTorch_DIR=%VCPKG_UNZIPPED_EXPORT_PATH%\installed\%TRIPLET%\tools\python3\Lib\site-packages\torch\share\cmake\Torch + +cmake --build build_cuda --config Release --target package + +:: Generate superpoint weights (from share directory of the installed package) +curl -L -O "https://raw.githubusercontent.com/magicleap/SuperPointPretrainedNetwork/master/demo_superpoint.py" +curl -L -O "https://github.com/magicleap/SuperPointPretrainedNetwork/raw/refs/heads/master/superpoint_v1.pth" +..\bin\python.exe rtabmap_trace_superpoint.py + +:EndComment \ No newline at end of file diff --git a/corelib/src/CMakeLists.txt b/corelib/src/CMakeLists.txt index 5ff638d2..667b10d7 100644 --- a/corelib/src/CMakeLists.txt +++ b/corelib/src/CMakeLists.txt @@ -791,37 +791,14 @@ IF(CUVSLAM_FOUND) ENDIF(CUVSLAM_FOUND) IF(GTSAM_FOUND) - # Make sure GTSAM is built with system Eigen, not the included one in its package - IF(GTSAM_INCLUDE_DIR) - SET(INCLUDE_DIRS - ${INCLUDE_DIRS} - ${GTSAM_INCLUDE_DIR} - ) - ELSE() - SET(INCLUDE_DIRS - ${INCLUDE_DIRS} - ${GTSAM_INCLUDE_DIRS} - ) - ENDIF() SET(SRC_FILES ${SRC_FILES} optimizer/gtsam/GravityFactor.cpp ) - IF(WIN32) - # GTSAM should be built in STATIC on Windows to avoid "error C2338: THIS_METHOD_IS_ONLY_FOR_1x1_EXPRESSIONS" when building GTSAM - add_definitions("-DGTSAM_IMPORT_STATIC") - ENDIF(WIN32) SET(LIBRARIES ${LIBRARIES} - gtsam # Windows: Place static libs at the end + gtsam ) - IF(WIN32) - #explicitly add metis target on windows (after gtsam target) - SET(LIBRARIES - ${LIBRARIES} - metis - ) - ENDIF(WIN32) ENDIF(GTSAM_FOUND) IF(WITH_MADGWICK) diff --git a/corelib/src/python/PyMatcher.cpp b/corelib/src/python/PyMatcher.cpp index aac482ef..786f0a4f 100644 --- a/corelib/src/python/PyMatcher.cpp +++ b/corelib/src/python/PyMatcher.cpp @@ -228,16 +228,32 @@ std::vector PyMatcher::match( int len2 = PyArray_SHAPE(np_ret)[1]; int type = PyArray_TYPE(np_ret); UDEBUG("Matches array %dx%d (type=%d)", len1, len2, type); - UASSERT_MSG(type == NPY_LONG || type == NPY_INT, uFormat("Returned matches should type INT=5 or LONG=7, received type=%d", type).c_str()); - if(type == NPY_LONG) + UASSERT_MSG(type == NPY_INT32 || type == NPY_UINT32 || type == NPY_INT64 || type == NPY_UINT64, uFormat("Returned matches should type INT32=%d UINT32=%d, INT64=%d or UINT64=%d, received type=%d", NPY_INT, NPY_UINT32, NPY_INT64, NPY_UINT64, type).c_str()); + if(type == NPY_UINT64) { - long* c_out = reinterpret_cast(PyArray_DATA(np_ret)); + long long* c_out = reinterpret_cast(PyArray_DATA(np_ret)); for (int i = 0; i < len1*len2; i+=2) { matches.push_back(cv::DMatch(c_out[i], c_out[i+1], 0)); } } - else // INT + if(type == NPY_INT64) + { + unsigned long long* c_out = reinterpret_cast(PyArray_DATA(np_ret)); + for (int i = 0; i < len1*len2; i+=2) + { + matches.push_back(cv::DMatch(c_out[i], c_out[i+1], 0)); + } + } + else if(type == NPY_UINT32) + { + unsigned int* c_out = reinterpret_cast(PyArray_DATA(np_ret)); + for (int i = 0; i < len1*len2; i+=2) + { + matches.push_back(cv::DMatch(c_out[i], c_out[i+1], 0)); + } + } + else // NPY_INT { int* c_out = reinterpret_cast(PyArray_DATA(np_ret)); for (int i = 0; i < len1*len2; i+=2) diff --git a/corelib/src/python/PythonInterface.cpp b/corelib/src/python/PythonInterface.cpp index 0e2d9879..5fd0b43a 100644 --- a/corelib/src/python/PythonInterface.cpp +++ b/corelib/src/python/PythonInterface.cpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace rtabmap { @@ -16,6 +17,12 @@ PythonInterface::PythonInterface() { UINFO("Initialize python interpreter"); guard_ = new pybind11::scoped_interpreter(); + + // Tell Python to look in this directory for DLLs + std::string exe_dir = std::filesystem::current_path().string(); + pybind11::module_ os = pybind11::module_::import("os"); + os.attr("add_dll_directory")(exe_dir); + pybind11::module::import("threading"); release_ = new pybind11::gil_scoped_release(); } diff --git a/corelib/src/python/rtabmap_superglue.py b/corelib/src/python/rtabmap_superglue.py index d9758d72..c3dafe2e 100644 --- a/corelib/src/python/rtabmap_superglue.py +++ b/corelib/src/python/rtabmap_superglue.py @@ -38,7 +38,6 @@ def init(descriptorDim, matchThreshold, iterations, cuda, model): global superglue superglue = SuperGlue(config.get('superglue', {})).eval().to(device) - def match(kptsFrom, kptsTo, scoresFrom, scoresTo, descriptorsFrom, descriptorsTo, imageWidth, imageHeight): #print("SuperGlue python match()") global device @@ -77,6 +76,8 @@ def match(kptsFrom, kptsTo, scoresFrom, scoresTo, descriptorsFrom, descriptorsTo matchesArray = np.stack((matchesFrom, matchesTo), axis=1); + # rtabmap expects format: + # matches: array Nx2 (type=9 or uint64) return matchesArray diff --git a/corelib/src/python/rtabmap_superpoint.py b/corelib/src/python/rtabmap_superpoint.py index 51fce24d..79f4ce2d 100644 --- a/corelib/src/python/rtabmap_superpoint.py +++ b/corelib/src/python/rtabmap_superpoint.py @@ -8,6 +8,7 @@ import random import numpy as np import torch +import os #import sys #import os @@ -21,15 +22,19 @@ torch.set_grad_enabled(False) device = 'cpu' superpoint = [] +script_dir = os.path.dirname(os.path.abspath(__file__)) + def init(cuda): #print("SuperPoint python init()") global device device = 'cuda' if torch.cuda.is_available() and cuda else 'cpu' + weights_abs_path = os.path.join(script_dir, "superpoint_v1.pth") + # This class runs the SuperPoint network and processes its outputs. global superpoint - superpoint = SuperPointFrontend(weights_path="superpoint_v1.pth", + superpoint = SuperPointFrontend(weights_path=weights_abs_path, nms_dist=4, conf_thresh=0.015, nn_thresh=1, @@ -47,10 +52,14 @@ def detect(imageBuffer): # use copy to make sure memory is correctly re-ordered pts = np.float32(np.transpose(pts)).copy() desc = np.float32(np.transpose(desc)).copy() + + # rtabmap expects format: + # pts: array Nx3 (type=11 or float) + # descriptors: array NxDIM 35x256 (type=11 or float) return pts, desc if __name__ == '__main__': #test - init(True) + init(False) detect(np.random.rand(640,480)*255) diff --git a/corelib/src/python/rtabmap_trace_superpoint.py b/corelib/src/python/rtabmap_trace_superpoint.py new file mode 100644 index 00000000..b74db6c1 --- /dev/null +++ b/corelib/src/python/rtabmap_trace_superpoint.py @@ -0,0 +1,13 @@ +import os +import sys +from pathlib import Path + +import torch +import torchvision +from demo_superpoint import SuperPointNet +model = SuperPointNet() +model.load_state_dict(torch.load("superpoint_v1.pth")) +model.eval() +example = torch.rand(1, 1, 640, 480) +traced_script_module = torch.jit.trace(model, example, check_trace=False) +traced_script_module.save("superpoint_v1.pt") diff --git a/docker/noble/android/rtabmap_apiXX/rtabmap.bash b/docker/noble/android/rtabmap_apiXX/rtabmap.bash index 67154dfe..2d1bf32d 100755 --- a/docker/noble/android/rtabmap_apiXX/rtabmap.bash +++ b/docker/noble/android/rtabmap_apiXX/rtabmap.bash @@ -21,7 +21,7 @@ make # rtabmap mkdir arm64-v8a cd arm64-v8a -cmake -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake -DANDROID_ABI=arm64-v8a -DANDROID_NDK=$ANDROID_NDK -DANDROID_NATIVE_API_LEVEL=$api -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=$prefix/arm64-v8a -DCMAKE_FIND_ROOT_PATH="$prefix/arm64-v8a/bin;$prefix/arm64-v8a;$prefix/arm64-v8a/share" -DBUILD_EXAMPLES=OFF -DBUILD_TOOLS=OFF -DOpenCV_DIR=$prefix/arm64-v8a/sdk/native/jni ../.. +cmake -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake -DANDROID_ABI=arm64-v8a -DANDROID_NDK=$ANDROID_NDK -DANDROID_NATIVE_API_LEVEL=$api -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=$prefix/arm64-v8a -DCMAKE_FIND_ROOT_PATH="$prefix/arm64-v8a/bin;$prefix/arm64-v8a;$prefix/arm64-v8a/share" -DBUILD_EXAMPLES=OFF -DBUILD_TOOLS=OFF -DWITH_OPENGV=OFF -DOpenCV_DIR=$prefix/arm64-v8a/sdk/native/jni ../.. make make clean diff --git a/guilib/src/3rdParty/QMultiComboBox.cpp b/guilib/src/3rdParty/QMultiComboBox.cpp index 64e19c68..f3df7939 100644 --- a/guilib/src/3rdParty/QMultiComboBox.cpp +++ b/guilib/src/3rdParty/QMultiComboBox.cpp @@ -40,7 +40,7 @@ QMultiComboBox::QMultiComboBox(QWidget *widget ) : QMultiComboBox::~QMultiComboBox() { - disconnect(&vlist_,0,0,0); + vlist_.disconnect(SIGNAL(itemChanged(QListWidgetItem*))); } diff --git a/guilib/src/AboutDialog.cpp b/guilib/src/AboutDialog.cpp index 5cdf8c34..40a605b5 100644 --- a/guilib/src/AboutDialog.cpp +++ b/guilib/src/AboutDialog.cpp @@ -121,6 +121,13 @@ AboutDialog::AboutDialog(QWidget * parent) : _ui->label_liblas->setText("No"); _ui->label_liblas_license->setEnabled(false); #endif +#ifdef RTABMAP_OPENGV + _ui->label_opengv->setText("Yes"); + _ui->label_opengv_license->setEnabled(true); +#else + _ui->label_opengv->setText("No"); + _ui->label_opengv_license->setEnabled(false); +#endif #ifdef RTABMAP_CUDASIFT _ui->label_cudasift->setText("Yes"); _ui->label_cudasift_license->setEnabled(true); diff --git a/guilib/src/ui/aboutDialog.ui b/guilib/src/ui/aboutDialog.ui index ac562b74..ca0e087d 100644 --- a/guilib/src/ui/aboutDialog.ui +++ b/guilib/src/ui/aboutDialog.ui @@ -164,7 +164,7 @@ p, li { white-space: pre-wrap; } 0 0 596 - 1234 + 1257 @@ -185,78 +185,8 @@ p, li { white-space: pre-wrap; } - - - - With GridMap : - - - true - - - - - - - Apache v2 - - - true - - - - - - - With CCCoreLib : - - - true - - - - - - - With Zed Open Capture : - - - true - - - - - - - With Python3 : - - - true - - - - - - - MIT - - - true - - - - - - - GPLv3 - - - true - - - - - + + @@ -268,40 +198,20 @@ p, li { white-space: pre-wrap; } - - + + - With MYNTEYE S : + BSD true - - + + - With FastCV : - - - true - - - - - - - With VINS-Fusion : - - - true - - - - - - - With PDAL : + BSD true @@ -309,7 +219,7 @@ p, li { white-space: pre-wrap; } - + @@ -321,7 +231,7 @@ p, li { white-space: pre-wrap; } - + GPLv2 @@ -331,49 +241,6 @@ p, li { white-space: pre-wrap; } - - - - GPLv3 - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With OKVIS : - - - true - - - - - - - MIT - - - true - - - @@ -387,8 +254,8 @@ p, li { white-space: pre-wrap; } - - + + @@ -400,68 +267,8 @@ p, li { white-space: pre-wrap; } - - - - BSD - - - true - - - - - - - Apache v2 and/or GPLv2 - - - true - - - - - - - With Octomap : - - - true - - - - - - - With Open3D : - - - true - - - - - - - With Freenect : - - - true - - - - - - - MIT - - - true - - - - - + + @@ -473,146 +280,7 @@ p, li { white-space: pre-wrap; } - - - - GPLv3 - - - true - - - - - - - With cvsba : - - - true - - - - - - - With SuperPoint Torch : - - - true - - - - - - - BSD - - - true - - - - - - - With FOVIS : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With AliceVision : - - - true - - - - - - - BSD - - - true - - - - - - - With DVO : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With OpenChisel : - - - true - - - - - - - With Freenect2 : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - + MIT @@ -622,175 +290,50 @@ p, li { white-space: pre-wrap; } - - - - Penn Software License - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With DepthAI : - - - true - - - - - - - With OpenNI : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - BSD - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - Apache-2 - - - true - - - - - - - BSD - - - true - - - - + - With Ceres : + With cvsba : true - - + + - With XVisio SDK : + MIT true - - + + - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + BSD true - - + + - With OpenNI2 : + BSD true - - + + - With cuVSLAM : - - - true - - - - - - - With TORO : + With Viso2 : true @@ -807,254 +350,17 @@ p, li { white-space: pre-wrap; } - - + + - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + With K4A : true - - - - MIT - - - true - - - - - - - <html><head/><body><p><span style=" font-weight:600;">License</span></p></body></html> - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - GPLv3 - - - true - - - - - - - GPLv2 - - - true - - - - - - - BSD - - - true - - - - - - - With Orbbec SDK : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With MSCKF : - - - true - - - - - - - Open Source or Commercial - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - MPL2 - - - true - - - - - - - With loam_velodyne : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - BSD - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With CPU-TSDF : - - - true - - - - - - - With GTSAM : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - + Creative Commons [Attribution-NonCommercial-ShareAlike] @@ -1064,28 +370,8 @@ p, li { white-space: pre-wrap; } - - - - Apache-2 - - - true - - - - - - - With RealSense : - - - true - - - - - + + @@ -1097,51 +383,68 @@ p, li { white-space: pre-wrap; } - - + + - BSD + With CCCoreLib : true - - + + - With OpenVINS : + With RealSense2 : true - - + + - With Viso2 : + With stereo dc1394 : true - - + + - <html><head/><body><p><span style=" font-weight:600;">Third Party Libraries</span></p></body></html> - - - Qt::RichText + With OpenChisel : true - - + + + + MIT + + + true + + + + + + + GPLv3 + + + true + + + + + @@ -1153,85 +456,10 @@ p, li { white-space: pre-wrap; } - - + + - LGPL - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With ORB SLAM 2 : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + With g2o : true @@ -1248,28 +476,8 @@ p, li { white-space: pre-wrap; } - - - - With ORB Octree : - - - true - - - - - - - Qt version : - - - true - - - - - + + @@ -1281,20 +489,37 @@ p, li { white-space: pre-wrap; } - - + + - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + BSD true - + + + + With FastCV : + + + true + + + + + + + BSD + + + true + + + + @@ -1307,8 +532,68 @@ p, li { white-space: pre-wrap; } - - + + + + MPL2 + + + true + + + + + + + MIT + + + true + + + + + + + With stereo FlyCapture2 : + + + true + + + + + + + GPLv3 + + + true + + + + + + + MIT + + + true + + + + + + + With VINS-Fusion : + + + true + + + + + @@ -1320,6 +605,116 @@ p, li { white-space: pre-wrap; } + + + + BSD + + + true + + + + + + + With Zed Open Capture : + + + true + + + + + + + With ORB SLAM 2 : + + + true + + + + + + + BSD + + + true + + + + + + + BSD + + + true + + + + + + + With MSCKF : + + + true + + + + + + + With CPU-TSDF : + + + true + + + + + + + With Zed SDK : + + + true + + + + + + + With CudaSift : + + + true + + + + + + + Apache v2 + + + true + + + + + + + With FOVIS : + + + true + + + @@ -1330,118 +725,6 @@ p, li { white-space: pre-wrap; } - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With stereo FlyCapture2 : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With K4A : - - - true - - - - - - - Apache v2 - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With MRPT : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - GPLv3 - - - true - - - - - - - Apache v2 and/or GPLv2 - - - true - - - @@ -1455,72 +738,20 @@ p, li { white-space: pre-wrap; } - - + + - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + BSD true - - + + - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - MIT - - - true - - - - - - - OpenCV version : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + Apache v2 and/or GPLv2 true @@ -1540,10 +771,893 @@ p, li { white-space: pre-wrap; } - - + + - VTK version : + BSD + + + true + + + + + + + With OKVIS : + + + true + + + + + + + BSD + + + true + + + + + + + With libpointmatcher : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With Octomap : + + + true + + + + + + + GPLv2 + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With Ceres : + + + true + + + + + + + LGPL + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With MYNTEYE S : + + + true + + + + + + + GPLv2 + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + BSD + + + true + + + + + + + With OpenNI2 : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With XVisio SDK : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + Apache-2 + + + true + + + + + + + GPLv3 + + + true + + + + + + + NVIDIA ISAAC ROS SOFTWARE LICENSE + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + Apache-2 + + + true + + + + + + + With Freenect2 : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With PDAL : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + MIT + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With Python3 : + + + true + + + + + + + With ORB Octree : + + + true + + + + + + + With DVO : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With GridMap : + + + true + + + + + + + With cuVSLAM : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + Penn Software License + + + true + + + + + + + Open Source or Commercial + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With OpenVINS : + + + true + + + + + + + GPLv3 + + + true + + + + + + + GPLv3 + + + true + + + + + + + With Open3D : + + + true + + + + + + + + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With TORO : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With GTSAM : + + + true + + + + + + + Apache v2 + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + Apache v2 + + + true + + + + + + + With OpenNI : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With Orbbec SDK : + + + true + + + + + + + With Freenect : + + + true + + + + + + + With RealSense : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With DepthAI : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + <html><head/><body><p><span style=" font-weight:600;">License</span></p></body></html> + + + true + + + + + + + With AliceVision : + + + true + + + + + + + With MRPT : + + + true + + + + + + + MIT + + + + + + + With SuperPoint Rpautrat : + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter true @@ -1560,8 +1674,34 @@ p, li { white-space: pre-wrap; } - - + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + <html><head/><body><p><span style=" font-weight:600;">Third Party Libraries</span></p></body></html> + + + Qt::RichText + + + true + + + + + BSD @@ -1570,74 +1710,8 @@ p, li { white-space: pre-wrap; } - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With stereo dc1394 : - - - true - - - - - - - With CudaSift : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With K4W2 : - - - true - - - - - - - With RealSense2 : - - - true - - - - - + + GPLv3 @@ -1646,20 +1720,23 @@ p, li { white-space: pre-wrap; } - - + + - BSD + GPLv3 true - - + + - With libpointmatcher : + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter true @@ -1679,8 +1756,28 @@ p, li { white-space: pre-wrap; } - - + + + + With loam_velodyne : + + + true + + + + + + + Qt version : + + + true + + + + + @@ -1692,48 +1789,38 @@ p, li { white-space: pre-wrap; } - - + + - NVIDIA ISAAC ROS SOFTWARE LICENSE + Apache v2 and/or GPLv2 true - - + + - GPLv2 + With SuperPoint Torch : true - - + + - BSD + VTK version : true - - - - BSD - - - true - - - - - + + @@ -1745,21 +1832,38 @@ p, li { white-space: pre-wrap; } - - + + - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + With K4W2 : true - - + + + + OpenCV version : + + + true + + + + + + + With OpenGV : + + + true + + + + + BSD @@ -1768,28 +1872,8 @@ p, li { white-space: pre-wrap; } - - - - With Zed SDK : - - - true - - - - - - - GPLv3 - - - true - - - - - + + @@ -1801,57 +1885,6 @@ p, li { white-space: pre-wrap; } - - - - Apache v2 - - - true - - - - - - - With g2o : - - - true - - - - - - - BSD - - - true - - - - - - - With SuperPoint Rpautrat : - - - - - - - - - - - - - - MIT - - - @@ -1887,7 +1920,7 @@ p, li { white-space: pre-wrap; } - Copyright (C) 2010-2025 IntRoLab - Université de Sherbrooke + Copyright (C) 2010-2026 IntRoLab - Université de Sherbrooke Qt::AlignCenter diff --git a/guilib/src/ui/mainWindow.ui b/guilib/src/ui/mainWindow.ui index bbfee4ed..fc4519a0 100644 --- a/guilib/src/ui/mainWindow.ui +++ b/guilib/src/ui/mainWindow.ui @@ -18,7 +18,7 @@ :/images/RTAB-Map.ico:/images/RTAB-Map.ico - QMainWindow::AllowNestedDocks|QMainWindow::AllowTabbedDocks|QMainWindow::AnimatedDocks|QMainWindow::VerticalTabs + QMainWindow::DockOption::AllowNestedDocks|QMainWindow::DockOption::AllowTabbedDocks|QMainWindow::DockOption::AnimatedDocks|QMainWindow::DockOption::VerticalTabs @@ -27,7 +27,7 @@ 0 0 1012 - 22 + 21 @@ -494,7 +494,7 @@ - true + false Statistics @@ -522,7 +522,7 @@ - QLayout::SetMinimumSize + QLayout::SizeConstraint::SetMinimumSize 2 @@ -530,7 +530,7 @@ - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter ms @@ -562,14 +562,14 @@ Unknown - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter Hz @@ -604,7 +604,7 @@ Unknown - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter @@ -621,7 +621,7 @@ Unknown - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter @@ -638,7 +638,7 @@ 0 - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter @@ -656,7 +656,7 @@ 0 - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter @@ -673,7 +673,7 @@ 0 - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter @@ -697,7 +697,7 @@ - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter Hz @@ -731,7 +731,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -893,7 +893,7 @@ - true + false Map visibility @@ -956,7 +956,7 @@ - Qt::AllDockWidgetAreas + Qt::DockWidgetArea::AllDockWidgetAreas Loop closure detection @@ -998,7 +998,7 @@ - Qt::AlignCenter + Qt::AlignmentFlag::AlignCenter @@ -1024,7 +1024,7 @@ - Qt::AlignCenter + Qt::AlignmentFlag::AlignCenter @@ -1422,6 +1422,9 @@ + + true + :/images/document-save.png:/images/document-save.png diff --git a/patches/apply_patch.cmake b/patches/apply_patch.cmake new file mode 100644 index 00000000..d8356ff0 --- /dev/null +++ b/patches/apply_patch.cmake @@ -0,0 +1,18 @@ +# Try to apply the patch in reverse first to see if it's already there +execute_process( + COMMAND git apply --reverse --check "${PATCH_FILE}" + RESULT_VARIABLE PATCH_APPLIED + OUTPUT_QUIET + ERROR_QUIET +) + +if(NOT PATCH_APPLIED EQUAL 0) + # If not applied, apply it now with whitespace fixes for Windows + execute_process( + COMMAND git apply --ignore-whitespace --whitespace=fix "${PATCH_FILE}" + RESULT_VARIABLE RESULT + ) + if(NOT RESULT EQUAL 0) + message(FATAL_ERROR "Failed to apply patch: ${PATCH_FILE}") + endif() +endif() \ No newline at end of file diff --git a/patches/gtsam_4_3a0-ros.patch b/patches/gtsam_4_3a0-ros.patch new file mode 100644 index 00000000..e2b2ab54 --- /dev/null +++ b/patches/gtsam_4_3a0-ros.patch @@ -0,0 +1,13 @@ +diff --git a/cmake/Config.cmake.in b/cmake/Config.cmake.in +index 338ff8500..0b738ea3c 100644 +--- a/cmake/Config.cmake.in ++++ b/cmake/Config.cmake.in +@@ -28,7 +28,7 @@ if(@GTSAM_USE_TBB@) + endif() + + if(@GTSAM_USE_SYSTEM_EIGEN@) +-find_dependency(Eigen3 REQUIRED) ++find_dependency(Eigen3 REQUIRED CONFIG) + endif() + + # Load exports diff --git a/patches/libnabo_c925c47.patch b/patches/libnabo_c925c47.patch new file mode 100644 index 00000000..65f87e60 --- /dev/null +++ b/patches/libnabo_c925c47.patch @@ -0,0 +1,58 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 2e9de0c..6a031c7 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -103,16 +103,7 @@ endif () + include(GNUInstallDirs) + + # eigen 2 or 3 +-find_path(EIGEN_INCLUDE_DIR NAMES signature_of_eigen3_matrix_library +- HINTS ENV EIGEN3_INC_DIR +- ENV EIGEN3_DIR +- PATHS Eigen/Core +- /usr/local/include +- /usr/include +- /opt/local/include +- PATH_SUFFIXES include eigen3 eigen2 eigen +- DOC "Directory containing the Eigen3 header files" +-) ++find_package(Eigen3 REQUIRED) + + # optionally, opencl + # OpenCL disabled as its code is not up-to-date with API +@@ -165,10 +156,10 @@ endif () + set_target_properties(${LIB_NAME} PROPERTIES VERSION "${PROJECT_VERSION}" SOVERSION 1) + + target_include_directories(${LIB_NAME} PUBLIC +- ${EIGEN_INCLUDE_DIR} + $ + $ + ) ++target_link_libraries(${LIB_NAME} PUBLIC Eigen3::Eigen) + + # openmp + set(USE_OPEN_MP TRUE CACHE BOOL "Set to FALSE to not use OpenMP") +diff --git a/libnaboConfig.cmake.in b/libnaboConfig.cmake.in +index 0de01c3..8bb8194 100644 +--- a/libnaboConfig.cmake.in ++++ b/libnaboConfig.cmake.in +@@ -1,5 +1,7 @@ + # - Config file for the libnabo package + ++find_dependency(Eigen3 CONFIG) ++ + include(${CMAKE_CURRENT_LIST_DIR}/libnabo-targets.cmake) + + # This causes catkin_simple to link against these libraries +diff --git a/nabo/kdtree_cpu.cpp b/nabo/kdtree_cpu.cpp +index cb1f8d1..52a444f 100644 +--- a/nabo/kdtree_cpu.cpp ++++ b/nabo/kdtree_cpu.cpp +@@ -37,6 +37,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + #include + #include + #include ++#include + #ifdef HAVE_OPENMP + #include + #endif diff --git a/patches/opengv_91f4b19c.patch b/patches/opengv_91f4b19c.patch new file mode 100644 index 00000000..e15b212e --- /dev/null +++ b/patches/opengv_91f4b19c.patch @@ -0,0 +1,314 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 9660f55..c4a076f 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -18,6 +18,7 @@ set(LIBRARY_OUTPUT_PATH ${CMAKE_BINARY_DIR}/lib) + + OPTION(BUILD_TESTS "Build tests" ON) + OPTION(BUILD_PYTHON "Build Python extension" OFF) ++OPTION(BUILD_WITH_MARCHNATIVE "Build with -march=native" ON) + + IF(MSVC) + set(BUILD_SHARED_LIBS OFF) +@@ -35,7 +36,7 @@ ELSE() + ELSEIF (CMAKE_SYSTEM_PROCESSOR MATCHES + "(arm)|(ARM)|(armhf)|(ARMHF)|(armel)|(ARMEL)") + add_definitions (-march=armv7-a) +- ELSE () ++ ELSEIF (BUILD_WITH_MARCHNATIVE) + add_definitions (-march=native) #TODO use correct c++11 def once everybody has moved to gcc 4.7 # for now I even removed std=gnu++0x + ENDIF() + add_definitions ( +@@ -54,8 +55,9 @@ IF(BUILD_POSITION_INDEPENDENT_CODE) + ENDIF() + + set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${PROJECT_SOURCE_DIR}/modules/") +-find_package(Eigen REQUIRED) +-set(ADDITIONAL_INCLUDE_DIRS ${EIGEN_INCLUDE_DIRS} ${EIGEN_INCLUDE_DIR}/unsupported) ++find_package(Eigen3 REQUIRED) ++get_target_property(EIGEN3_INCLUDE_DIR Eigen3::Eigen INTERFACE_INCLUDE_DIRECTORIES) ++set(ADDITIONAL_INCLUDE_DIRS ${EIGEN3_INCLUDE_DIR}/unsupported) + + set( OPENGV_SOURCE_FILES + src/absolute_pose/modules/main.cpp +@@ -187,13 +189,10 @@ set_target_properties( opengv random_generators PROPERTIES + + target_include_directories( opengv PUBLIC + # only when building from the source tree +- $ ++ "$" + # only when using the lib from the install path +- $ +- ${ADDITIONAL_INCLUDE_DIRS} ) +- +-target_include_directories( random_generators PRIVATE ${ADDITIONAL_INCLUDE_DIRS} ) +- ++ "$" ) ++target_link_libraries(opengv PUBLIC Eigen3::Eigen) + target_link_libraries( random_generators opengv ) + + IF (BUILD_TESTS) +diff --git a/modules/Config.cmake.in b/modules/Config.cmake.in +index 9b4c9ee..6963356 100644 +--- a/modules/Config.cmake.in ++++ b/modules/Config.cmake.in +@@ -1,4 +1,4 @@ + @PACKAGE_INIT@ +- ++find_dependency(Eigen3 CONFIG) + include("${CMAKE_CURRENT_LIST_DIR}/@targets_export_name@.cmake") + check_required_components("@PROJECT_NAME@") +diff --git a/src/absolute_pose/CentralAbsoluteAdapter.cpp b/src/absolute_pose/CentralAbsoluteAdapter.cpp +index 684fa7e..ead54ac 100644 +--- a/src/absolute_pose/CentralAbsoluteAdapter.cpp ++++ b/src/absolute_pose/CentralAbsoluteAdapter.cpp +@@ -30,6 +30,7 @@ + + + #include ++#include + + + opengv::absolute_pose::CentralAbsoluteAdapter::CentralAbsoluteAdapter( +diff --git a/src/absolute_pose/MACentralAbsolute.cpp b/src/absolute_pose/MACentralAbsolute.cpp +index 6edbabc..1164b30 100644 +--- a/src/absolute_pose/MACentralAbsolute.cpp ++++ b/src/absolute_pose/MACentralAbsolute.cpp +@@ -30,6 +30,7 @@ + + + #include ++#include + + + opengv::absolute_pose::MACentralAbsolute::MACentralAbsolute( +diff --git a/src/absolute_pose/MANoncentralAbsolute.cpp b/src/absolute_pose/MANoncentralAbsolute.cpp +index d9b5b09..1d2041c 100644 +--- a/src/absolute_pose/MANoncentralAbsolute.cpp ++++ b/src/absolute_pose/MANoncentralAbsolute.cpp +@@ -30,6 +30,7 @@ + + + #include ++#include + + opengv::absolute_pose::MANoncentralAbsolute::MANoncentralAbsolute( + const double * points, +diff --git a/src/absolute_pose/NoncentralAbsoluteAdapter.cpp b/src/absolute_pose/NoncentralAbsoluteAdapter.cpp +index 30176aa..399699b 100644 +--- a/src/absolute_pose/NoncentralAbsoluteAdapter.cpp ++++ b/src/absolute_pose/NoncentralAbsoluteAdapter.cpp +@@ -30,6 +30,7 @@ + + + #include ++#include + + opengv::absolute_pose::NoncentralAbsoluteAdapter::NoncentralAbsoluteAdapter( + const bearingVectors_t & bearingVectors, +diff --git a/src/absolute_pose/NoncentralAbsoluteMultiAdapter.cpp b/src/absolute_pose/NoncentralAbsoluteMultiAdapter.cpp +index 88c237a..b88bbb2 100644 +--- a/src/absolute_pose/NoncentralAbsoluteMultiAdapter.cpp ++++ b/src/absolute_pose/NoncentralAbsoluteMultiAdapter.cpp +@@ -30,6 +30,7 @@ + + + #include ++#include + + opengv::absolute_pose::NoncentralAbsoluteMultiAdapter::NoncentralAbsoluteMultiAdapter( + std::vector > bearingVectors, +diff --git a/src/absolute_pose/methods.cpp b/src/absolute_pose/methods.cpp +index b1f0889..6d7a73c 100644 +--- a/src/absolute_pose/methods.cpp ++++ b/src/absolute_pose/methods.cpp +@@ -34,6 +34,7 @@ + + #include + #include ++#include + + #include + #include +diff --git a/src/absolute_pose/modules/main.cpp b/src/absolute_pose/modules/main.cpp +index ed0c271..011dbcc 100644 +--- a/src/absolute_pose/modules/main.cpp ++++ b/src/absolute_pose/modules/main.cpp +@@ -46,6 +46,8 @@ + #include + #include + ++#include ++ + void + opengv::absolute_pose::modules::p3p_kneip_main( + const bearingVectors_t & f, +diff --git a/src/math/arun.cpp b/src/math/arun.cpp +index a0d6296..bc729f6 100644 +--- a/src/math/arun.cpp ++++ b/src/math/arun.cpp +@@ -30,6 +30,7 @@ + + + #include ++#include + + opengv::rotation_t + opengv::math::arun( const Eigen::MatrixXd & Hcross ) +diff --git a/src/point_cloud/MAPointCloud.cpp b/src/point_cloud/MAPointCloud.cpp +index 81fd5dd..a216857 100644 +--- a/src/point_cloud/MAPointCloud.cpp ++++ b/src/point_cloud/MAPointCloud.cpp +@@ -30,6 +30,7 @@ + + + #include ++#include + + opengv::point_cloud::MAPointCloud::MAPointCloud( + const double * points1, +diff --git a/src/point_cloud/PointCloudAdapter.cpp b/src/point_cloud/PointCloudAdapter.cpp +index f9faaeb..1f8e951 100644 +--- a/src/point_cloud/PointCloudAdapter.cpp ++++ b/src/point_cloud/PointCloudAdapter.cpp +@@ -30,6 +30,7 @@ + + + #include ++#include + + opengv::point_cloud::PointCloudAdapter::PointCloudAdapter( + const points_t & points1, +diff --git a/src/point_cloud/methods.cpp b/src/point_cloud/methods.cpp +index 5409eeb..f098a1d 100644 +--- a/src/point_cloud/methods.cpp ++++ b/src/point_cloud/methods.cpp +@@ -39,6 +39,8 @@ + #include + #include + ++#include ++ + namespace opengv + { + namespace point_cloud +diff --git a/src/relative_pose/CentralRelativeAdapter.cpp b/src/relative_pose/CentralRelativeAdapter.cpp +index 38e9a62..b2e9d98 100644 +--- a/src/relative_pose/CentralRelativeAdapter.cpp ++++ b/src/relative_pose/CentralRelativeAdapter.cpp +@@ -30,6 +30,7 @@ + + + #include ++#include + + opengv::relative_pose::CentralRelativeAdapter::CentralRelativeAdapter( + const bearingVectors_t & bearingVectors1, +diff --git a/src/relative_pose/CentralRelativeMultiAdapter.cpp b/src/relative_pose/CentralRelativeMultiAdapter.cpp +index 2ab7476..e522205 100644 +--- a/src/relative_pose/CentralRelativeMultiAdapter.cpp ++++ b/src/relative_pose/CentralRelativeMultiAdapter.cpp +@@ -30,6 +30,7 @@ + + + #include ++#include + + opengv::relative_pose::CentralRelativeMultiAdapter::CentralRelativeMultiAdapter( + std::vector > bearingVectors1, +diff --git a/src/relative_pose/CentralRelativeWeightingAdapter.cpp b/src/relative_pose/CentralRelativeWeightingAdapter.cpp +index a6ab478..9401dc2 100644 +--- a/src/relative_pose/CentralRelativeWeightingAdapter.cpp ++++ b/src/relative_pose/CentralRelativeWeightingAdapter.cpp +@@ -30,6 +30,7 @@ + + + #include ++#include + + opengv::relative_pose::CentralRelativeWeightingAdapter::CentralRelativeWeightingAdapter( + const bearingVectors_t & bearingVectors1, +diff --git a/src/relative_pose/MACentralRelative.cpp b/src/relative_pose/MACentralRelative.cpp +index ec2959f..c1feb7c 100644 +--- a/src/relative_pose/MACentralRelative.cpp ++++ b/src/relative_pose/MACentralRelative.cpp +@@ -30,6 +30,7 @@ + + + #include ++#include + + opengv::relative_pose::MACentralRelative::MACentralRelative( + const double * bearingVectors1, +diff --git a/src/relative_pose/MANoncentralRelative.cpp b/src/relative_pose/MANoncentralRelative.cpp +index cea9c14..fc65c64 100644 +--- a/src/relative_pose/MANoncentralRelative.cpp ++++ b/src/relative_pose/MANoncentralRelative.cpp +@@ -30,6 +30,7 @@ + + + #include ++#include + + opengv::relative_pose::MANoncentralRelative::MANoncentralRelative( + const double * bearingVectors1, +diff --git a/src/relative_pose/MANoncentralRelativeMulti.cpp b/src/relative_pose/MANoncentralRelativeMulti.cpp +index 49f8ecf..1e58fde 100644 +--- a/src/relative_pose/MANoncentralRelativeMulti.cpp ++++ b/src/relative_pose/MANoncentralRelativeMulti.cpp +@@ -30,6 +30,7 @@ + + + #include ++#include + + opengv::relative_pose::MANoncentralRelativeMulti::MANoncentralRelativeMulti( + const std::vector & bearingVectors1, +diff --git a/src/relative_pose/NoncentralRelativeAdapter.cpp b/src/relative_pose/NoncentralRelativeAdapter.cpp +index 552f180..9edf294 100644 +--- a/src/relative_pose/NoncentralRelativeAdapter.cpp ++++ b/src/relative_pose/NoncentralRelativeAdapter.cpp +@@ -30,6 +30,7 @@ + + + #include ++#include + + opengv::relative_pose::NoncentralRelativeAdapter::NoncentralRelativeAdapter( + const bearingVectors_t & bearingVectors1, +diff --git a/src/relative_pose/NoncentralRelativeMultiAdapter.cpp b/src/relative_pose/NoncentralRelativeMultiAdapter.cpp +index f41edbe..c720a5a 100644 +--- a/src/relative_pose/NoncentralRelativeMultiAdapter.cpp ++++ b/src/relative_pose/NoncentralRelativeMultiAdapter.cpp +@@ -30,6 +30,7 @@ + + + #include ++#include + + opengv::relative_pose::NoncentralRelativeMultiAdapter::NoncentralRelativeMultiAdapter( + std::vector > bearingVectors1, +diff --git a/src/relative_pose/methods.cpp b/src/relative_pose/methods.cpp +index 0027dae..e2e26b1 100644 +--- a/src/relative_pose/methods.cpp ++++ b/src/relative_pose/methods.cpp +@@ -42,6 +42,7 @@ + #include + + #include ++#include + + opengv::translation_t + opengv::relative_pose::twopt( +diff --git a/src/relative_pose/modules/fivept_nister/modules.cpp b/src/relative_pose/modules/fivept_nister/modules.cpp +index 4b134c5..f24e3f1 100644 +--- a/src/relative_pose/modules/fivept_nister/modules.cpp ++++ b/src/relative_pose/modules/fivept_nister/modules.cpp +@@ -34,6 +34,7 @@ + #include + + #include ++#include + + void + opengv::relative_pose::modules::fivept_nister::composeA( diff --git a/patches/pointmatcher_7dc58e5.patch b/patches/pointmatcher_7dc58e5.patch new file mode 100644 index 00000000..1b0a911b --- /dev/null +++ b/patches/pointmatcher_7dc58e5.patch @@ -0,0 +1,589 @@ +diff --git a/.gitignore b/.gitignore +index 85152fb..da016a7 100644 +--- a/.gitignore ++++ b/.gitignore +@@ -5,3 +5,4 @@ + *.cur_trans + build + .ipynb_checkpoints/ ++pointmatcher/pm_export.h +\ No newline at end of file +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 819a834..d72850a 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -233,7 +233,7 @@ else() + #get_property(yaml-cpp-pm_INCLUDE TARGET yaml-cpp-pm PROPERTY INCLUDE_DIRECTORIES) + #include_directories(${yaml-cpp-pm_INCLUDE}) + +- list(APPEND EXTERNAL_LIBS $) ++ list(APPEND EXTERNAL_LIBS $) + list(APPEND EXTRA_DEPS yaml-cpp-pm) + set(yamlcpp_FOUND) + +@@ -291,6 +291,8 @@ else () + set (CMAKE_CXX_STANDARD 11) + endif () + ++option(BUILD_SHARED_LIBS "Set to ON to build shared libraries" OFF) ++ + # SOURCE + + # Pointmatcher lib and install +@@ -363,12 +365,18 @@ set(POINTMATCHER_SRC + + file(GLOB_RECURSE POINTMATCHER_HEADERS "pointmatcher/*.h") + +- + # In CMake >=3.4 we can easily build shared libraries in Mac and Windows. + # No need to distinguish between operating systems while building targets + + add_library(pointmatcher ${POINTMATCHER_SRC} ${POINTMATCHER_HEADERS} ) + ++include(GenerateExportHeader) ++generate_export_header(pointmatcher ++ BASE_NAME PM ++ EXPORT_FILE_NAME "${CMAKE_CURRENT_SOURCE_DIR}/pointmatcher/pm_export.h" ++ DEFINE_NO_DEPRECATED ++) ++ + target_include_directories(pointmatcher PUBLIC + $ + $ +@@ -417,6 +425,7 @@ install(FILES + pointmatcher/Timer.h + pointmatcher/Functions.h + pointmatcher/IO.h ++ pointmatcher/pm_export.h + DESTINATION ${INSTALL_INCLUDE_DIR}/pointmatcher + ) + +@@ -515,7 +524,7 @@ add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS pointmatcher) + get_property(CONF_INCLUDE_DIRS DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY INCLUDE_DIRECTORIES) + + # Create variable with the library location +-set(POINTMATCHER_LIB $) ++set(POINTMATCHER_LIB $) + + # Configure config file for local build tree + configure_file(libpointmatcherConfig.cmake.in +diff --git a/pointmatcher/Bibliography.h b/pointmatcher/Bibliography.h +index 17a353d..c86def5 100644 +--- a/pointmatcher/Bibliography.h ++++ b/pointmatcher/Bibliography.h +@@ -40,6 +40,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + #include + #include + ++#include "pm_export.h" ++ + namespace PointMatcherSupport + { + typedef std::vector StringVector; +@@ -48,7 +50,7 @@ namespace PointMatcherSupport + typedef StringMapMap Bibliography; + typedef std::map BibIndices; + +- struct CurrentBibliography ++ struct PM_EXPORT CurrentBibliography + { + enum Mode + { +@@ -68,7 +70,7 @@ namespace PointMatcherSupport + void dumpBibtex(std::ostream& os) const; + }; + +- std::string getAndReplaceBibEntries(const std::string&, CurrentBibliography& curBib); ++ PM_EXPORT std::string getAndReplaceBibEntries(const std::string&, CurrentBibliography& curBib); + + }; // PointMatcherSupport + +diff --git a/pointmatcher/DeprecationWarnings.h b/pointmatcher/DeprecationWarnings.h +index b1ca8ec..dc84582 100644 +--- a/pointmatcher/DeprecationWarnings.h ++++ b/pointmatcher/DeprecationWarnings.h +@@ -31,7 +31,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + #ifndef __POINTMATCHER_DEPRECATION_WARNINGS_H + #define __POINTMATCHER_DEPRECATION_WARNINGS_H + +- + #if __cplusplus >= 201402L + #define PM_DEPRECATED(msg) [[deprecated(msg)]] + #define PM_DEPRECATION_SUPPORTED +diff --git a/pointmatcher/IO.cpp b/pointmatcher/IO.cpp +index c4288c9..8722747 100644 +--- a/pointmatcher/IO.cpp ++++ b/pointmatcher/IO.cpp +@@ -51,7 +51,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + #include "boost/lexical_cast.hpp" + #include "boost/foreach.hpp" + +-#ifdef WIN32 ++#ifdef _WIN32 + #define strtok_r strtok_s + #endif // WIN32 + +@@ -359,7 +359,9 @@ void PointMatcherSupport::validateFile(const std::string& fileName) + ifstream ifs(fileName.c_str()); + if (!ifs.good() || !boost::filesystem::is_regular_file(fullPath)) + #if BOOST_FILESYSTEM_VERSION >= 3 +- #if BOOST_VERSION >= 105000 ++ #if BOOST_VERSION >= 109000 ++ throw runtime_error(string("Cannot open file ") + boost::filesystem::absolute(fullPath).generic_string()); ++ #elif BOOST_VERSION >= 105000 + throw runtime_error(string("Cannot open file ") + boost::filesystem::complete(fullPath).generic_string()); + #else + throw runtime_error(string("Cannot open file ") + boost::filesystem3::complete(fullPath).generic_string()); +@@ -375,7 +377,11 @@ template + typename PointMatcher::DataPoints PointMatcher::DataPoints::load(const std::string& fileName) + { + const boost::filesystem::path path(fileName); ++#if BOOST_VERSION >= 109000 ++ const string ext = path.extension().string(); ++#else + const string& ext(boost::filesystem::extension(path)); ++#endif + if (boost::iequals(ext, ".vtk")) + return PointMatcherIO::loadVTK(fileName); + else if (boost::iequals(ext, ".csv")) +@@ -809,7 +815,11 @@ template + void PointMatcher::DataPoints::save(const std::string& fileName, bool binary) const + { + const boost::filesystem::path path(fileName); ++#if BOOST_VERSION >= 109000 ++ const string ext = path.extension().string(); ++#else + const string& ext(boost::filesystem::extension(path)); ++#endif + if (boost::iequals(ext, ".vtk")) + return PointMatcherIO::saveVTK(*this, fileName, binary); + +diff --git a/pointmatcher/IO.h b/pointmatcher/IO.h +index 162dc21..8a0cefa 100644 +--- a/pointmatcher/IO.h ++++ b/pointmatcher/IO.h +@@ -58,7 +58,7 @@ struct PointMatcherIO + //! ex: nx, ny, nz are associated with (0,normals) (1,normals) (2,normals) respectively + typedef std::map SublabelAssociationMap; + +- static std::string getColLabel(const Label& label, const int row); //!< convert a descriptor label to an appropriate sub-label ++ PM_EXPORT static std::string getColLabel(const Label& label, const int row); //!< convert a descriptor label to an appropriate sub-label + + //! Type of information in a DataPoints. Each type is stored in its own dense matrix. + enum PMPropTypes +@@ -70,7 +70,7 @@ struct PointMatcherIO + }; + + //! Structure containing all information required to map external information to PointMatcher internal representation +- struct SupportedLabel ++ struct PM_EXPORT SupportedLabel + { + std::string internalName; //!< name used in PointMatcher + std::string externalName; //!< name used in external format +@@ -84,7 +84,7 @@ struct PointMatcherIO + typedef std::vector SupportedLabels; + + //! Helper structure designed to parse file headers +- struct GenericInputHeader ++ struct PM_EXPORT GenericInputHeader + { + std::string name; //!< name found in the file + unsigned int matrixRowId; //!< on which row the information will be loaded +@@ -159,7 +159,7 @@ struct PointMatcherIO + } + + //! Generate a vector of Labels by checking for collision is the same name is reused. +- class LabelGenerator ++ class PM_EXPORT LabelGenerator + { + Labels labels; //!< vector of labels used to cumulat information + +@@ -180,11 +180,11 @@ struct PointMatcherIO + //static PMPropTypes getPMType(const std::string& externalName); //! Return the type of information specific to a DataPoints based on a sulabel name + + // CSV +- static DataPoints loadCSV(const std::string& fileName); +- static DataPoints loadCSV(std::istream& is); ++ PM_EXPORT static DataPoints loadCSV(const std::string& fileName); ++ PM_EXPORT static DataPoints loadCSV(std::istream& is); + +- static void saveCSV(const DataPoints& data, const std::string& fileName); +- static void saveCSV(const DataPoints& data, std::ostream& os); ++ PM_EXPORT static void saveCSV(const DataPoints& data, const std::string& fileName); ++ PM_EXPORT static void saveCSV(const DataPoints& data, std::ostream& os); + + // VTK + //! Enumeration of legacy VTK data types that can be parsed +@@ -209,25 +209,25 @@ struct PointMatcherIO + + }; + +- static DataPoints loadVTK(const std::string& fileName); +- static DataPoints loadVTK(std::istream& is); ++ PM_EXPORT static DataPoints loadVTK(const std::string& fileName); ++ PM_EXPORT static DataPoints loadVTK(std::istream& is); + +- static void saveVTK(const DataPoints& data, const std::string& fileName, bool binary = false); ++ PM_EXPORT static void saveVTK(const DataPoints& data, const std::string& fileName, bool binary = false); + + // PLY +- static DataPoints loadPLY(const std::string& fileName); +- static DataPoints loadPLY(std::istream& is); ++ PM_EXPORT static DataPoints loadPLY(const std::string& fileName); ++ PM_EXPORT static DataPoints loadPLY(std::istream& is); + +- static void savePLY(const DataPoints& data, const std::string& fileName); //!< save datapoints to PLY point cloud format ++ PM_EXPORT static void savePLY(const DataPoints& data, const std::string& fileName); //!< save datapoints to PLY point cloud format + + // PCD +- static DataPoints loadPCD(const std::string& fileName); +- static DataPoints loadPCD(std::istream& is); ++ PM_EXPORT static DataPoints loadPCD(const std::string& fileName); ++ PM_EXPORT static DataPoints loadPCD(std::istream& is); + +- static void savePCD(const DataPoints& data, const std::string& fileName); //!< save datapoints to PCD point cloud format ++ PM_EXPORT static void savePCD(const DataPoints& data, const std::string& fileName); //!< save datapoints to PCD point cloud format + + //! Information to exploit a reading from a file using this library. Fields might be left blank if unused. +- struct FileInfo ++ struct PM_EXPORT FileInfo + { + typedef Eigen::Matrix Vector3; //!< alias + +@@ -242,7 +242,7 @@ struct PointMatcherIO + }; + + //! A vector of file info, to be used in batch processing +- struct FileInfoVector: public std::vector ++ struct PM_EXPORT FileInfoVector: public std::vector + { + FileInfoVector(); + FileInfoVector(const std::string& fileName, std::string dataPath = "", std::string configPath = ""); +@@ -264,7 +264,7 @@ struct PointMatcherIO + static bool plyPropTypeValid (const std::string& type); + + //! Interface for PLY property +- struct PLYProperty ++ struct PM_EXPORT PLYProperty + { + //PLY information: + std::string name; //!< name of PLY property +@@ -299,7 +299,7 @@ struct PointMatcherIO + typedef typename PLYProperties::iterator it_PLYProp; + + //! Interface for all PLY elements. +- class PLYElement ++ class PM_EXPORT PLYElement + { + public: + std::string name; //!< name identifying the PLY element +@@ -331,7 +331,7 @@ struct PointMatcherIO + + + //! Implementation of PLY vertex element +- class PLYVertex : public PLYElement ++ class PM_EXPORT PLYVertex : public PLYElement + { + public: + //! Constructor +@@ -346,7 +346,7 @@ struct PointMatcherIO + }; + + //! Factory for PLY elements +- class PLYElementF ++ class PM_EXPORT PLYElementF + { + enum ElementTypes + { +diff --git a/pointmatcher/Parametrizable.h b/pointmatcher/Parametrizable.h +index 318e7f1..edc1355 100644 +--- a/pointmatcher/Parametrizable.h ++++ b/pointmatcher/Parametrizable.h +@@ -46,6 +46,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + #define BOOST_ASSIGN_MAX_PARAMS 6 + #include + ++#include "pm_export.h" + + namespace PointMatcherSupport + { +@@ -95,7 +96,7 @@ namespace PointMatcherSupport + } + + //! The superclass of classes that are constructed using generic parameters. This class provides the parameter storage and fetching mechanism +- struct Parametrizable ++ struct PM_EXPORT Parametrizable + { + //! An exception thrown when one tries to fetch the value of an unexisting parameter + struct InvalidParameter: std::runtime_error +@@ -114,7 +115,7 @@ namespace PointMatcherSupport + } + + //! The documentation of a parameter +- struct ParameterDoc ++ struct PM_EXPORT ParameterDoc + { + std::string name; //!< name + std::string doc; //!< short documentation +@@ -173,7 +174,7 @@ namespace PointMatcherSupport + + friend std::ostream& operator<< (std::ostream& o, const Parametrizable& p); + }; +- std::ostream& operator<< (std::ostream& o, const Parametrizable::ParametersDoc& p); ++ PM_EXPORT std::ostream& operator<< (std::ostream& o, const Parametrizable::ParametersDoc& p); + } // namespace PointMatcherSupport + + #endif // __POINTMATCHER_PARAMETRIZABLE_H +diff --git a/pointmatcher/PointMatcher.h b/pointmatcher/PointMatcher.h +index f26aa2c..435a464 100644 +--- a/pointmatcher/PointMatcher.h ++++ b/pointmatcher/PointMatcher.h +@@ -101,7 +101,7 @@ namespace PointMatcherSupport + + + //! The logger interface, used to output warnings and informations +- struct Logger: public Parametrizable ++ struct PM_EXPORT Logger: public Parametrizable + { + Logger(); + Logger(const std::string& className, const ParametersDoc paramsDoc, const Parameters& params); +@@ -127,7 +127,7 @@ namespace PointMatcherSupport + + //! Functions and classes that are dependant on scalar type are defined in this templatized class + template +-struct PointMatcher ++struct PM_EXPORT PointMatcher + { + // --------------------------------- + // macros for constants +@@ -145,7 +145,7 @@ struct PointMatcher + //TODO: gather exceptions here and in Exceptions.cpp + + //! Point matcher did not converge +- struct ConvergenceError: std::runtime_error ++ struct PM_EXPORT ConvergenceError: std::runtime_error + { + ConvergenceError(const std::string& reason); + }; +@@ -204,7 +204,7 @@ struct PointMatcher + Moreover, the position of the points is in homogeneous coordinates because they need both translation and rotation, while the normals need only rotation. + All channels contain scalar values of type ScalarType. + */ +- struct DataPoints ++ struct PM_EXPORT DataPoints + { + //! A view on a feature or descriptor + typedef Eigen::Block View; +@@ -218,7 +218,7 @@ struct PointMatcher + typedef typename Matrix::Index Index; + + //! The name for a certain number of dim +- struct Label ++ struct PM_EXPORT Label + { + std::string text; //!< name of the label + size_t span; //!< number of data dimensions the label spans +@@ -226,7 +226,7 @@ struct PointMatcher + bool operator ==(const Label& that) const; + }; + //! A vector of Label +- struct Labels: std::vector - 10 + 24 @@ -16425,7 +16425,7 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - 13 + 1 @@ -16965,6 +16965,22 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag [Visual] Correspondences computation: Optical Flow + + + + 1 + + + 999999 + + + 1 + + + 21 + + + @@ -16978,19 +16994,6 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - - - - Max level. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - @@ -17007,19 +17010,16 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - - - - 1 + + + + Iterations. - - 999999 + + true - - 1 - - - 21 + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse @@ -17042,10 +17042,10 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - - + + - Iterations. + Epsilon. true @@ -17071,23 +17071,10 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - - + + - Epsilon. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - Use GPU. OpenCV built with CUDA required. + Max level. true @@ -17098,12 +17085,109 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag + + + + + + + + + + Use minimum eigen values as an error measure, otherwise L1 distance between patches is used as an error measure. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + 6 + + + 0.000000000000000 + + + 1.000000000000000 + + + 0.000100000000000 + + + 0.000100000000000 + + + + + + + [Eigen error measure] If the minimum eigenvalue of a feature's spatial gradient matrix is less than this threshold, then the feature is filtered out. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + 6 + + + 0.000000000000000 + + + 9999.000000000000000 + + + 1.000000000000000 + + + 50.000000000000000 + + + + + + + [L1 error measure] Filter out features with error greater than this threshold. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + Use GPU. OpenCV built with CUDA required. Note that the error measure parameter is not used with the GPU implementation. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + @@ -23020,6 +23104,35 @@ Lower the ratio -> higher the precision. Motion estimation: 3D to 3D + + + + Refine iterations of the resulting transformation computed by RANSAC. 0 means no refining. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + 0 + + + 10000 + + + 1 + + + 10 + + + @@ -23052,34 +23165,18 @@ Lower the ratio -> higher the precision. - - - - 0 + + + + Qt::Vertical - - 10000 + + + 20 + 40 + - - 1 - - - 10 - - - - - - - Refine iterations of the resulting transformation computed by RANSAC. 0 means no refining. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - + @@ -24566,7 +24663,7 @@ Lower the ratio -> higher the precision. - 2 + 0 @@ -25532,10 +25629,17 @@ Lower the ratio -> higher the precision. - - + + - [OpticalFlow = true] Do optical flow on GPU. RTAB-Map should be built with OpenCV CUDA. + + + + + + + + [OpticalFlow = true] Use minimum eigen values as an error measure, otherwise L1 distance between patches is used as an error measure. true @@ -25545,13 +25649,90 @@ Lower the ratio -> higher the precision. - + + + + 6 + + + 0.000000000000000 + + + 1.000000000000000 + + + 0.000100000000000 + + + 0.000100000000000 + + + + + + + [OpticalFlow = true / Eigen error measure] If the minimum eigenvalue of a feature's spatial gradient matrix is less than this threshold, then the feature is filtered out. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + 6 + + + 0.000000000000000 + + + 9999.000000000000000 + + + 1.000000000000000 + + + 50.000000000000000 + + + + + + + [OpticalFlow = true / L1 error measure] Filter out features with error greater than this threshold. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + [OpticalFlow = true] Do optical flow on GPU. RTAB-Map should be built with OpenCV CUDA. Note that the error measure parameter is not used with the GPU implementation. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + From f94a9634636cc47abcb3081f631bf837c0599ed7 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sat, 28 Mar 2026 14:33:26 -0700 Subject: [PATCH 089/126] CID-SIMS support (#1676) * cid-sims dataset support * Added cli tool to test CID-SIMS dataset * removed debug log --- .../rtabmap/core/camera/CameraImages.h | 4 +- corelib/src/CameraModel.cpp | 16 + corelib/src/IMUThread.cpp | 17 +- corelib/src/OdometryThread.cpp | 4 +- corelib/src/Transform.cpp | 25 + corelib/src/camera/CameraImages.cpp | 81 ++- .../include/rtabmap/gui/PreferencesDialog.h | 1 + guilib/src/MainWindow.cpp | 2 +- guilib/src/PreferencesDialog.cpp | 20 +- guilib/src/ui/preferencesDialog.ui | 546 +++++++------- tools/CMakeLists.txt | 1 + tools/CidSimsDataset/CMakeLists.txt | 11 + tools/CidSimsDataset/main.cpp | 688 ++++++++++++++++++ 13 files changed, 1115 insertions(+), 301 deletions(-) create mode 100644 tools/CidSimsDataset/CMakeLists.txt create mode 100644 tools/CidSimsDataset/main.cpp diff --git a/corelib/include/rtabmap/core/camera/CameraImages.h b/corelib/include/rtabmap/core/camera/CameraImages.h index cbdbcd78..2e234b9a 100644 --- a/corelib/include/rtabmap/core/camera/CameraImages.h +++ b/corelib/include/rtabmap/core/camera/CameraImages.h @@ -102,10 +102,11 @@ public: } // 0=Raw, 1=RGBD-SLAM motion capture (10=without change of coordinate frame, 11=10+ID), 2=KITTI, 3=TORO, 4=g2o, 5=NewCollege(t,x,y), 6=Malaga Urban GPS, 7=St Lucia INS, 8=Karlsruhe, 9=EuRoC MAV, 12=rgbd_bonn - void setGroundTruthPath(const std::string & filePath, int format = 0) + void setGroundTruthPath(const std::string & filePath, int format = 0, const Transform & localTransform = Transform::getIdentity()) { _groundTruthPath = filePath; _groundTruthFormat = format; + _groundTruthLocalTransform = localTransform; } void setMaxPoseTimeDiff(double diff) {_maxPoseTimeDiff = diff;} @@ -164,6 +165,7 @@ private: int _odometryFormat; std::string _groundTruthPath; int _groundTruthFormat; + Transform _groundTruthLocalTransform; double _maxPoseTimeDiff; std::list _stamps; diff --git a/corelib/src/CameraModel.cpp b/corelib/src/CameraModel.cpp index 7c1980c5..04b77329 100644 --- a/corelib/src/CameraModel.cpp +++ b/corelib/src/CameraModel.cpp @@ -353,6 +353,22 @@ bool CameraModel::load(const std::string & filePath) data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], data[8], data[9], data[10], data[11]); + Transform detCheck = localTransform_.clone(); + localTransform_.normalizeRotation(); /// Normalize by default + float det = detCheck.toEigen3f().linear().determinant(); + if(fabs(det - 1.0f) > 0.0001) + { + std::stringstream streamBefore, streamAfter; + streamBefore << detCheck << std::endl; + streamAfter << localTransform_ << std::endl; + UWARN("The camera model's local_transform from \"%s\" doesn't " + "have a normalized rotation matrix (dertminant=%f). We will normalize " + "it for convenience.\nWas:\n%sNow\n%s", + filePath.c_str(), + det, + streamBefore.str().c_str(), + streamAfter.str().c_str()); + } } else { diff --git a/corelib/src/IMUThread.cpp b/corelib/src/IMUThread.cpp index 58072ed8..778c5e42 100644 --- a/corelib/src/IMUThread.cpp +++ b/corelib/src/IMUThread.cpp @@ -135,8 +135,20 @@ void IMUThread::mainLoop() std::stringstream stream(line); std::string s; std::getline(stream, s, ','); - std::string nanoseconds = s.substr(s.size() - 9, 9); - std::string seconds = s.substr(0, s.size() - 9); + + double stamp = 0.0; + if(s.find('.') != std::string::npos) + { + // Normal [epoch] timestamp + stamp = uStr2Double(s); + } + else + { + // Assume EuRoC format + std::string nanoseconds = s.substr(s.size() - 9, 9); + std::string seconds = s.substr(0, s.size() - 9); + stamp = double(uStr2Int(seconds)) + double(uStr2Int(nanoseconds))*1e-9; + } cv::Vec3d gyr; for (int j = 0; j < 3; ++j) { @@ -150,7 +162,6 @@ void IMUThread::mainLoop() acc[j] = uStr2Double(s); } - double stamp = double(uStr2Int(seconds)) + double(uStr2Int(nanoseconds))*1e-9; if(previousStamp_>0 && stamp > previousStamp_) { captureDelay_ = stamp - previousStamp_; diff --git a/corelib/src/OdometryThread.cpp b/corelib/src/OdometryThread.cpp index 0adb3160..3c7b079a 100644 --- a/corelib/src/OdometryThread.cpp +++ b/corelib/src/OdometryThread.cpp @@ -188,7 +188,7 @@ void OdometryThread::addData(const SensorEvent & event) "(%f), skipping that frame (imu buffer size=%ld). " "When using async IMU, make sure IMU is published faster " "than camera/lidar (assuming IMU latency is very small compared to camera/lidar)." - "Current camera/lidar delay is %fs.", + "Current camera/lidar delay with system time is %fs.", event.data().stamp(), _oldestAsyncImuStamp, _imuBuffer.size(), UTimer::now() - event.data().stamp()); notify = false; } @@ -197,7 +197,7 @@ void OdometryThread::addData(const SensorEvent & event) "(%f), skipping that frame (imu buffer size=%ld). " "When using async IMU, make sure IMU is published faster " "than camera/lidar (assuming IMU latency is very small compared to camera/lidar). " - "Current camera/lidar delay is %fs.", + "Current camera/lidar delay with system time is %fs.", event.data().stamp(), _newestAsyncImuStamp, _imuBuffer.size(), UTimer::now() - event.data().stamp()); notify = false; } diff --git a/corelib/src/Transform.cpp b/corelib/src/Transform.cpp index 08c8d71a..3e511b7e 100644 --- a/corelib/src/Transform.cpp +++ b/corelib/src/Transform.cpp @@ -52,6 +52,26 @@ Transform::Transform( r11, r12, r13, o14, r21, r22, r23, o24, r31, r32, r33, o34); + + if( r11>0.0f || r12>0.0f || r13>0.0f || + r21>0.0f || r22>0.0f || r23>0.0f || + r31>0.0f || r32>0.0f || r33>0.0f) + { + Eigen::Matrix3f m; + m << r11, r12, r13, + r21, r22, r23, + r31, r32, r33; + float d = m.determinant(); + if(fabs(d-1.0f) > 0.0001) + { + UWARN("Created transform doesn't have normalized rotation. Any transformation with this transform can cause unexpected results!" + " Determinant([%f %f %f;%f %f %f;%f %f %f])=%f", + r11, r12, r13, + r21, r22, r23, + r31, r32, r33, + d); + } + } } Transform::Transform(const cv::Mat & transformationMatrix) @@ -509,6 +529,11 @@ Transform Transform::fromString(const std::string & string) numbers[4], numbers[5], numbers[6], numbers[7], numbers[8], numbers[9], numbers[10], numbers[11]); } + // Always normalize + if(!t.isNull()) + { + t.normalizeRotation(); + } return t; } diff --git a/corelib/src/camera/CameraImages.cpp b/corelib/src/camera/CameraImages.cpp index 3b192778..97674121 100644 --- a/corelib/src/camera/CameraImages.cpp +++ b/corelib/src/camera/CameraImages.cpp @@ -63,6 +63,7 @@ CameraImages::CameraImages() : _syncImageRateWithStamps(true), _odometryFormat(0), _groundTruthFormat(0), + _groundTruthLocalTransform(Transform::getIdentity()), _maxPoseTimeDiff(0.02), _captureDelay(0.0) {} @@ -93,6 +94,7 @@ CameraImages::CameraImages(const std::string & path, _syncImageRateWithStamps(true), _odometryFormat(0), _groundTruthFormat(0), + _groundTruthLocalTransform(Transform::getIdentity()), _maxPoseTimeDiff(0.02), _captureDelay(0.0) { @@ -478,27 +480,43 @@ bool CameraImages::init(const std::string & calibrationFolder, const std::string if(success && _odometryPath.size() && odometry_.empty()) { success = readPoses(odometry_, _stamps, _odometryPath, _odometryFormat, _maxPoseTimeDiff); + if(!success) + { + UERROR("Failed to read odometry poses."); + } + + if(success) + { + for(size_t i=0; i(3,3) *= 0.01; + covariance.at(4,4) *= 0.01; + covariance.at(5,5) *= 0.01; + } + covariances_.push_back(covariance); + } + } } if(success && _groundTruthPath.size()) { success = readPoses(groundTruth_, _stamps, _groundTruthPath, _groundTruthFormat, _maxPoseTimeDiff); - } - - if(!odometry_.empty()) - { - for(size_t i=0; i(3,3) *= 0.01; - covariance.at(4,4) *= 0.01; - covariance.at(5,5) *= 0.01; + pose = pose*gtInv; // pose of base_link, assuming ground truth frame and base frame are rigidly fixed } - covariances_.push_back(covariance); } } } @@ -607,7 +625,7 @@ bool CameraImages::readPoses( } if(validPoses != (int)inOutStamps.size()) { - UWARN("%d valid poses of %d stamps", validPoses, (int)inOutStamps.size()); + UWARN("%d/%ld valid poses of %ld stamps", validPoses, outputPoses.size(), inOutStamps.size()); } } else @@ -756,32 +774,19 @@ SensorData CameraImages::captureImage(SensorCaptureInfo * info) if(_stamps.size()) { - stamp = _stamps.front(); - _stamps.pop_front(); - if(_stamps.size()) - { - _captureDelay = _stamps.front() - stamp; - } + UERROR("stamps cannot be used when startAt < 0"); } if(odometry_.size()) { - odometryPose = odometry_.front(); - odometry_.pop_front(); - if(covariances_.size()) - { - covariance = covariances_.front(); - covariances_.pop_front(); - } + UERROR("odometry cannot be used when startAt < 0"); } if(groundTruth_.size()) { - groundTruthPose = groundTruth_.front(); - groundTruth_.pop_front(); + UERROR("groundTruth cannot be used when startAt < 0"); } if(_models.size() && !model.isValidForProjection()) { - model = _models.front(); - _models.pop_front(); + UERROR("models cannot be used when startAt < 0"); } } else @@ -792,6 +797,7 @@ SensorData CameraImages::captureImage(SensorCaptureInfo * info) { imageFilePath = _path + imageFileName; scanFilePath = _scanPath + scanFileName; + size_t stampsSize = _stamps.size(); if(_stamps.size()) { stamp = _stamps.front(); @@ -803,6 +809,8 @@ SensorData CameraImages::captureImage(SensorCaptureInfo * info) } if(odometry_.size()) { + UASSERT_MSG(stampsSize==0 || stampsSize == odometry_.size(), + uFormat("Stamps=%ld odometry=%ld", _stamps.size(), odometry_.size()).c_str()); odometryPose = odometry_.front(); odometry_.pop_front(); if(covariances_.size()) @@ -813,11 +821,15 @@ SensorData CameraImages::captureImage(SensorCaptureInfo * info) } if(groundTruth_.size()) { + UASSERT_MSG(stampsSize==0 || stampsSize == groundTruth_.size(), + uFormat("Stamps=%ld groundTruth=%ld", _stamps.size(), groundTruth_.size()).c_str()); groundTruthPose = groundTruth_.front(); groundTruth_.pop_front(); } if(_models.size() && !model.isValidForProjection()) { + UASSERT_MSG(stampsSize==0 || stampsSize == _models.size(), + uFormat("Stamps=%ld models=%ld", _stamps.size(), _models.size()).c_str()); model = _models.front(); _models.pop_front(); } @@ -834,6 +846,7 @@ SensorData CameraImages::captureImage(SensorCaptureInfo * info) imageFilePath = _path + imageFileName; scanFilePath = _scanPath + scanFileName; + size_t stampsSize = _stamps.size(); if(_stamps.size()) { stamp = _stamps.front(); @@ -845,6 +858,8 @@ SensorData CameraImages::captureImage(SensorCaptureInfo * info) } if(odometry_.size()) { + UASSERT_MSG(stampsSize==0 || stampsSize == odometry_.size(), + uFormat("Stamps=%ld odometry=%ld", stampsSize, odometry_.size()).c_str()); odometryPose = odometry_.front(); odometry_.pop_front(); if(covariances_.size()) @@ -855,11 +870,15 @@ SensorData CameraImages::captureImage(SensorCaptureInfo * info) } if(groundTruth_.size()) { + UASSERT_MSG(stampsSize==0 || stampsSize == groundTruth_.size(), + uFormat("Stamps=%ld groundTruth=%ld", _stamps.size(), groundTruth_.size()).c_str()); groundTruthPose = groundTruth_.front(); groundTruth_.pop_front(); } if(_models.size() && !model.isValidForProjection()) { + UASSERT_MSG(stampsSize==0 || stampsSize == _models.size(), + uFormat("Stamps=%ld models=%ld", _stamps.size(), _models.size()).c_str()); model = _models.front(); _models.pop_front(); } diff --git a/guilib/include/rtabmap/gui/PreferencesDialog.h b/guilib/include/rtabmap/gui/PreferencesDialog.h index 0df6bd43..91845101 100644 --- a/guilib/include/rtabmap/gui/PreferencesDialog.h +++ b/guilib/include/rtabmap/gui/PreferencesDialog.h @@ -293,6 +293,7 @@ public: double getSourceScanForceGroundNormalsUp() const; Transform getSourceLocalTransform() const; //Openni group Transform getLaserLocalTransform() const; // directory images + Transform getGroundTruthLocalTransform() const; // directory images Transform getIMULocalTransform() const; // directory images QString getIMUPath() const; int getIMURate() const; diff --git a/guilib/src/MainWindow.cpp b/guilib/src/MainWindow.cpp index 27ec51ac..66108bec 100644 --- a/guilib/src/MainWindow.cpp +++ b/guilib/src/MainWindow.cpp @@ -3313,7 +3313,7 @@ void MainWindow::updateMapCloud( { std::string gtFrustumId = uFormat("f_gt_%d", iter->first); color = Qt::gray; - _cloudViewer->addOrUpdateFrustum(gtFrustumId, _currentGTPosesMap.at(iter->first), t, _cloudViewer->getFrustumScale(), color, model.fovX(), model.fovY()); + _cloudViewer->addOrUpdateFrustum(gtFrustumId, mapToGt*_currentGTPosesMap.at(iter->first), t, _cloudViewer->getFrustumScale(), color, model.fovX(), model.fovY()); } } } diff --git a/guilib/src/PreferencesDialog.cpp b/guilib/src/PreferencesDialog.cpp index f4339fba..0822cd76 100644 --- a/guilib/src/PreferencesDialog.cpp +++ b/guilib/src/PreferencesDialog.cpp @@ -850,6 +850,7 @@ PreferencesDialog::PreferencesDialog(QWidget * parent) : connect(_ui->comboBox_cameraImages_odomFormat, SIGNAL(currentIndexChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->lineEdit_cameraImages_gt, SIGNAL(textChanged(const QString &)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->comboBox_cameraImages_gtFormat, SIGNAL(currentIndexChanged(int)), this, SLOT(makeObsoleteSourcePanel())); + connect(_ui->lineEdit_cameraImages_gt_transform, SIGNAL(textChanged(const QString &)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->doubleSpinBox_maxPoseTimeDiff, SIGNAL(valueChanged(double)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->lineEdit_cameraImages_path_imu, SIGNAL(textChanged(const QString &)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->lineEdit_cameraImages_imu_transform, SIGNAL(textChanged(const QString &)), this, SLOT(makeObsoleteSourcePanel())); @@ -2360,6 +2361,7 @@ void PreferencesDialog::resetSettings(QGroupBox * groupBox) _ui->comboBox_cameraImages_odomFormat->setCurrentIndex(0); _ui->lineEdit_cameraImages_gt->setText(""); _ui->comboBox_cameraImages_gtFormat->setCurrentIndex(0); + _ui->lineEdit_cameraImages_gt_transform->setText("0 0 0 0 0 0"); _ui->doubleSpinBox_maxPoseTimeDiff->setValue(0.02); _ui->lineEdit_cameraImages_path_imu->setText(""); _ui->lineEdit_cameraImages_imu_transform->setText("0 0 1 0 -1 0 1 0 0"); @@ -2894,6 +2896,7 @@ void PreferencesDialog::readCameraSettings(const QString & filePath) _ui->comboBox_cameraImages_odomFormat->setCurrentIndex(settings.value("odom_format", _ui->comboBox_cameraImages_odomFormat->currentIndex()).toInt()); _ui->lineEdit_cameraImages_gt->setText(settings.value("gt_path", _ui->lineEdit_cameraImages_gt->text()).toString()); _ui->comboBox_cameraImages_gtFormat->setCurrentIndex(settings.value("gt_format", _ui->comboBox_cameraImages_gtFormat->currentIndex()).toInt()); + _ui->lineEdit_cameraImages_gt_transform->setText(settings.value("gt_transform", _ui->lineEdit_cameraImages_gt_transform->text()).toString()); _ui->doubleSpinBox_maxPoseTimeDiff->setValue(settings.value("max_pose_time_diff", _ui->doubleSpinBox_maxPoseTimeDiff->value()).toDouble()); _ui->lineEdit_cameraImages_path_imu->setText(settings.value("imu_path", _ui->lineEdit_cameraImages_path_imu->text()).toString()); @@ -3514,6 +3517,7 @@ void PreferencesDialog::writeCameraSettings(const QString & filePath) const settings.setValue("odom_format", _ui->comboBox_cameraImages_odomFormat->currentIndex()); settings.setValue("gt_path", _ui->lineEdit_cameraImages_gt->text()); settings.setValue("gt_format", _ui->comboBox_cameraImages_gtFormat->currentIndex()); + settings.setValue("gt_transform", _ui->lineEdit_cameraImages_gt_transform->text()); settings.setValue("max_pose_time_diff", _ui->doubleSpinBox_maxPoseTimeDiff->value()); settings.setValue("imu_path", _ui->lineEdit_cameraImages_path_imu->text()); settings.setValue("imu_local_transform", _ui->lineEdit_cameraImages_imu_transform->text()); @@ -6525,6 +6529,15 @@ Transform PreferencesDialog::getLaserLocalTransform() const } return t; } +Transform PreferencesDialog::getGroundTruthLocalTransform() const +{ + Transform t = Transform::fromString(_ui->lineEdit_cameraImages_gt_transform->text().replace("PI_2", QString::number(3.141592/2.0)).toStdString()); + if(t.isNull()) + { + return Transform::getIdentity(); + } + return t; +} QString PreferencesDialog::getIMUPath() const { @@ -6891,7 +6904,7 @@ Camera * PreferencesDialog::createCamera( ((CameraRGBDImages*)camera)->setMaxFrames(_ui->spinBox_cameraRGBDImages_maxFrames->value()); ((CameraRGBDImages*)camera)->setBayerMode(_ui->comboBox_cameraImages_bayerMode->currentIndex()-1); ((CameraRGBDImages*)camera)->setOdometryPath(_ui->lineEdit_cameraImages_odom->text().toStdString(), _ui->comboBox_cameraImages_odomFormat->currentIndex()); - ((CameraRGBDImages*)camera)->setGroundTruthPath(_ui->lineEdit_cameraImages_gt->text().toStdString(), _ui->comboBox_cameraImages_gtFormat->currentIndex()); + ((CameraRGBDImages*)camera)->setGroundTruthPath(_ui->lineEdit_cameraImages_gt->text().toStdString(), _ui->comboBox_cameraImages_gtFormat->currentIndex(), this->getGroundTruthLocalTransform()); ((CameraRGBDImages*)camera)->setMaxPoseTimeDiff(_ui->doubleSpinBox_maxPoseTimeDiff->value()); ((CameraRGBDImages*)camera)->setScanPath( _ui->lineEdit_cameraImages_path_scans->text().isEmpty()?"":_ui->lineEdit_cameraImages_path_scans->text().append(QDir::separator()).toStdString(), @@ -6937,7 +6950,7 @@ Camera * PreferencesDialog::createCamera( ((CameraStereoImages*)camera)->setMaxFrames(_ui->spinBox_cameraStereoImages_maxFrames->value()); ((CameraStereoImages*)camera)->setBayerMode(_ui->comboBox_cameraImages_bayerMode->currentIndex()-1); ((CameraStereoImages*)camera)->setOdometryPath(_ui->lineEdit_cameraImages_odom->text().toStdString(), _ui->comboBox_cameraImages_odomFormat->currentIndex()); - ((CameraStereoImages*)camera)->setGroundTruthPath(_ui->lineEdit_cameraImages_gt->text().toStdString(), _ui->comboBox_cameraImages_gtFormat->currentIndex()); + ((CameraStereoImages*)camera)->setGroundTruthPath(_ui->lineEdit_cameraImages_gt->text().toStdString(), _ui->comboBox_cameraImages_gtFormat->currentIndex(), this->getGroundTruthLocalTransform()); ((CameraStereoImages*)camera)->setMaxPoseTimeDiff(_ui->doubleSpinBox_maxPoseTimeDiff->value()); ((CameraStereoImages*)camera)->setScanPath( _ui->lineEdit_cameraImages_path_scans->text().isEmpty()?"":_ui->lineEdit_cameraImages_path_scans->text().append(QDir::separator()).toStdString(), @@ -7137,7 +7150,8 @@ Camera * PreferencesDialog::createCamera( _ui->comboBox_cameraImages_odomFormat->currentIndex()); ((CameraImages*)camera)->setGroundTruthPath( _ui->lineEdit_cameraImages_gt->text().toStdString(), - _ui->comboBox_cameraImages_gtFormat->currentIndex()); + _ui->comboBox_cameraImages_gtFormat->currentIndex(), + this->getGroundTruthLocalTransform()); ((CameraImages*)camera)->setMaxPoseTimeDiff(_ui->doubleSpinBox_maxPoseTimeDiff->value()); ((CameraImages*)camera)->setScanPath( _ui->lineEdit_cameraImages_path_scans->text().isEmpty()?"":_ui->lineEdit_cameraImages_path_scans->text().append(QDir::separator()).toStdString(), diff --git a/guilib/src/ui/preferencesDialog.ui b/guilib/src/ui/preferencesDialog.ui index d93985df..5d56b2b4 100644 --- a/guilib/src/ui/preferencesDialog.ui +++ b/guilib/src/ui/preferencesDialog.ui @@ -63,9 +63,9 @@ 0 - 0 + -2161 684 - 5110 + 5201 @@ -95,7 +95,7 @@ QFrame::Raised - 24 + 5 @@ -7536,129 +7536,50 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - + + + + + + + + + + + s + + + 4 + + + 9.990000000000000 + + + 0.010000000000000 + + + 0.020000000000000 + + + + + + + + + + + + ... - - - - Bayer mode. For convenience, if the images are bayered. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - IMU Rate. To synchronize capture rate with IMU timestamps, set to 0. This can be set a little over the actual IMU rate to keep up with camera capture rate if images are dropped by odometry. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - - - - - - - - <html><head/><body><p>Format (3 values): x y z<br/>Format (6 values): x y z roll pitch yaw<br/>Format (7 values): x y z qx qy qz qw<br/>Format (9 values, 3x3 rotation): r11 r12 r13 r21 r22 r23 r31 r32 r33<br/>Format (12 values, 3x4 transform): r11 r12 r13 tx r21 r22 r23 ty r31 r32 r33 tz</p><p>EuRoC: /base_link to /imu = 0 0 1 0 -1 0 1 0 0</p></body></html> - - - 0 0 0 0 0 0 - - - - - - - Ground truth file. Select the correct format below. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - <html><head/><body><p>Format (3 values): x y z<br/>Format (6 values): x y z roll pitch yaw<br/>Format (7 values): x y z qx qy qz qw<br/>Format (9 values, 3x3 rotation): r11 r12 r13 r21 r22 r23 r31 r32 r33<br/>Format (12 values, 3x4 transform): r11 r12 r13 tx r21 r22 r23 ty r31 r32 r33 tz</p><p>KITTI: /base_link to /scan = -0.27 0 0.08 0 0 0<br/>KITTI: /base_footprint to /scan = -0.27 0 1.75 0 0 0</p></body></html> - - - 0 0 0 0 0 0 - - - - - - - Maximum laser scan points. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - Odometry file. Select the correct format below. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - + - Local transform from /base_link to /imu_link. Mouse over the box to show formats. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - - - - - - - - Synchronize capture rate with timestamps. + Path to file containing optional IMU data (*.csv [EuRoC format]). Mouse over the box to show formats. true @@ -7743,10 +7664,10 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - + + - Timestamps file (*.txt). The file should contain one column. The number of rows should be the same than the number of images in the folder. Not used if "Use file names as timestamps" above is checked. + IMU Rate. To synchronize capture rate with IMU timestamps, set to 0. This can be set a little over the actual IMU rate to keep up with camera capture rate if images are dropped by odometry. true @@ -7756,10 +7677,10 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - + + - Odometry format. See tool tip for more details on formats. Note that formats without stamps should have the same number of values than the source images. + Synchronize capture rate with timestamps. true @@ -7769,51 +7690,7 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - - - Max time difference between data and corresponding pose for format with stamps. If delay is over this threshold, the pose won't be set on data loaded. This is used when odometry and/or ground truth files are set. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - - - - - - - - - - - - - - - ... - - - - - - - EuRoC: 200 Hz -> 250 Hz - - - 99999999 - - - - + Local transform from /base_link to /laser_link. Mouse over the box to show formats. @@ -7826,7 +7703,60 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - + + + + Odometry file. Select the correct format below. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + <html><head/><body><p>cvs format (comma split): &quot;stamp_sec,gyro_x,gyro_y,gyro_z,acc_x,acc_y,acc_z&quot;</p><p>EuRoC format: &quot;stamp_nanosec,gyro_x,gyro_y,gyro_z,acc_x,acc_y,acc_z&quot;</p></body></html> + + + + + + + + + + Local transform from /base_link to /imu_link. Mouse over the box to show formats. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + + + + + <html><head/><body><p>Format (3 values): x y z<br/>Format (6 values): x y z roll pitch yaw<br/>Format (7 values): x y z qx qy qz qw<br/>Format (9 values, 3x3 rotation): r11 r12 r13 r21 r22 r23 r31 r32 r33<br/>Format (12 values, 3x4 transform): r11 r12 r13 tx r21 r22 r23 ty r31 r32 r33 tz</p><p>KITTI: /base_link to /scan = -0.27 0 0.08 0 0 0<br/>KITTI: /base_footprint to /scan = -0.27 0 1.75 0 0 0</p></body></html> + + + 0 0 0 0 0 0 + + + + Path to directory containing optional laser scans (*.pcd, *.ply, *.bin [KITTI format]). The directory should have the same size has the images directory. @@ -7839,6 +7769,141 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki + + + + + + + + + + + Maximum laser scan points. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + <html><head/><body><p>KITTI: 130 000 points</p></body></html> + + + 99999999 + + + + + + + EuRoC: 200 Hz -> 250 Hz + + + 99999999 + + + + + + + QComboBox::AdjustToContents + + + + Disabled + + + + + BG + + + + + GB + + + + + RG + + + + + GR + + + + + + + + Max time difference between data and corresponding pose for format with stamps. If delay is over this threshold, the pose won't be set on data loaded. This is used when odometry and/or ground truth files are set. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + ... + + + + + + + + + + + + + + Use file names as timestamps. Format is epoch time. Example: "1305031102.175304.png". + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + <html><head/><body><p>Format (3 values): x y z<br/>Format (6 values): x y z roll pitch yaw<br/>Format (7 values): x y z qx qy qz qw<br/>Format (9 values, 3x3 rotation): r11 r12 r13 r21 r22 r23 r31 r32 r33<br/>Format (12 values, 3x4 transform): r11 r12 r13 tx r21 r22 r23 ty r31 r32 r33 tz</p><p>EuRoC: /base_link to /imu = 0 0 1 0 -1 0 1 0 0</p></body></html> + + + 0 0 0 0 0 0 + + + + + + + Ground truth file. Select the correct format below. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + @@ -7846,16 +7911,17 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - + + - Use file names as timestamps. Format is epoch time. Example: "1305031102.175304.png". + - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + ... @@ -7934,25 +8000,6 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - - - s - - - 4 - - - 9.990000000000000 - - - 0.010000000000000 - - - 0.020000000000000 - - - @@ -7966,59 +8013,17 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - - - <html><head/><body><p>KITTI: 130 000 points</p></body></html> - - - 99999999 - - - - - + + - + ... - - - - QComboBox::AdjustToContents - - - - Disabled - - - - - BG - - - - - GB - - - - - RG - - - - - GR - - - - - - + + - Path to file containing optional IMU data (*.csv [EuRoC format]). + Odometry format. See tool tip for more details on formats. Note that formats without stamps should have the same number of values than the source images. true @@ -8028,8 +8033,8 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - + + @@ -8048,31 +8053,52 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - + + - + Timestamps file (*.txt). The file should contain one column. The number of rows should be the same than the number of images in the folder. Not used if "Use file names as timestamps" above is checked. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - + + - ... + Bayer mode. For convenience, if the images are bayered. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - + + - + Local transform from /base_link to /gt_link. Mouse over the box to show formats. By default, we assume the ground truth matches the base frame, if the ground truth refers to another frame, set this to convert the poses in base frame. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - + + + + <html><head/><body><p>Format (3 values): x y z<br/>Format (6 values): x y z roll pitch yaw<br/>Format (7 values): x y z qx qy qz qw<br/>Format (9 values, 3x3 rotation): r11 r12 r13 r21 r22 r23 r31 r32 r33<br/>Format (12 values, 3x4 transform): r11 r12 r13 tx r21 r22 r23 ty r31 r32 r33 tz</p></body></html> + - ... + 0 0 0 0 0 0 diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index d50724b4..5d9ae525 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -7,6 +7,7 @@ ADD_SUBDIRECTORY( StereoEval ) ADD_SUBDIRECTORY( KittiDataset ) ADD_SUBDIRECTORY( RgbdDataset ) ADD_SUBDIRECTORY( EurocDataset ) +ADD_SUBDIRECTORY( CidSimsDataset ) ADD_SUBDIRECTORY( Recovery ) ADD_SUBDIRECTORY( Reprocess ) ADD_SUBDIRECTORY( DetectMoreLoopClosures ) diff --git a/tools/CidSimsDataset/CMakeLists.txt b/tools/CidSimsDataset/CMakeLists.txt new file mode 100644 index 00000000..0e540c2f --- /dev/null +++ b/tools/CidSimsDataset/CMakeLists.txt @@ -0,0 +1,11 @@ + +ADD_EXECUTABLE(cidsims_dataset main.cpp) + +TARGET_LINK_LIBRARIES(cidsims_dataset rtabmap_core) + +SET_TARGET_PROPERTIES( cidsims_dataset + PROPERTIES OUTPUT_NAME ${PROJECT_PREFIX}-cidsims_dataset) + +INSTALL(TARGETS cidsims_dataset + RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" COMPONENT runtime + BUNDLE DESTINATION "${CMAKE_BUNDLE_LOCATION}" COMPONENT runtime) diff --git a/tools/CidSimsDataset/main.cpp b/tools/CidSimsDataset/main.cpp new file mode 100644 index 00000000..62b244eb --- /dev/null +++ b/tools/CidSimsDataset/main.cpp @@ -0,0 +1,688 @@ +/* +Copyright (c) 2010-2016, Mathieu Labbe - IntRoLab - Universite de Sherbrooke +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Universite de Sherbrooke nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include +#include "rtabmap/core/Rtabmap.h" +#include "rtabmap/core/CameraRGBD.h" +#include "rtabmap/core/Graph.h" +#include "rtabmap/core/OdometryInfo.h" +#include "rtabmap/core/OdometryEvent.h" +#include "rtabmap/core/Memory.h" +#include "rtabmap/core/util3d_registration.h" +#include "rtabmap/utilite/UConversion.h" +#include "rtabmap/utilite/UDirectory.h" +#include "rtabmap/utilite/UFile.h" +#include "rtabmap/utilite/UMath.h" +#include "rtabmap/utilite/UStl.h" +#include "rtabmap/utilite/UProcessInfo.h" +#include +#include +#include +#include +#include + +using namespace rtabmap; + +void showUsage(const char * appName) +{ + printf("\nUsage:\n" + "%s [options] path\n" + " path Folder of the sequence (e.g., \"~/apartment1_1\")\n" + " containing color, depth, groundtruth.txt, imu.txt and odom.txt.\n" + " --output Output directory. By default, results are saved in \"path\".\n" + " --output_name Output database name (default \"rtabmap\").\n" + " --max_time_diff #.# Maximum time difference with frame to attribute a valid odometry and/or ground truth pose (default 0.1 s).\n" + " --quiet Don't show log messages and iteration updates.\n" + " --use_imu Use IMU.\n" + " --gt Record ground truth.\n" + " --odom Use wheel odometry as input guess to visual odometry.\n" + " --imu # Use IMU and set filter: 0=madgwick, 1=complementary.\n" + " --quiet Don't show log messages and iteration updates.\n" + "%s\n" + "Example:\n\n" + " $ %s \\\n" + " --Rtabmap/DetectionRate 2\\\n" + " --RGBD/OptimizeMaxError 5\\\n" + " --Mem/STMSize 30\\\n" + " --gt\\\n" + " --odom\\\n" + " --imu 1\\\n" + " ~/apartment1_1\n\n", appName, rtabmap::Parameters::showUsage(), appName); + exit(1); +} + +// catch ctrl-c +bool g_forever = true; +void sighandler(int sig) +{ + printf("\nSignal %d caught...\n", sig); + g_forever = false; +} + +int main(int argc, char * argv[]) +{ + signal(SIGABRT, &sighandler); + signal(SIGTERM, &sighandler); + signal(SIGINT, &sighandler); + + ULogger::setType(ULogger::kTypeConsole); + ULogger::setLevel(ULogger::kWarning); + + ParametersMap parameters; + std::string path; + std::string output; + std::string outputName = "rtabmap"; + int skipFrames = 0; + float maxTimeDiff = 0.1f; + bool quiet = false; + int imuFilter = 1; + bool useImu = false; + bool useOdom = false; + bool recordGt = false; + if(argc < 2) + { + showUsage(argv[0]); + } + else + { + for(int i=1; i 0.0f); + } + else if(std::strcmp(argv[i], "--odom") == 0) + { + useOdom = true; + } + else if(std::strcmp(argv[i], "--imu") == 0) + { + useImu = true; + imuFilter = atoi(argv[++i]); + } + else if(std::strcmp(argv[i], "--gt") == 0) + { + recordGt = true; + } + else if(std::strcmp(argv[i], "--quiet") == 0) + { + quiet = true; + } + } + parameters = Parameters::parseArguments(argc, argv); + path = argv[argc-1]; + path = uReplaceChar(path, '~', UDirectory::homeDir()); + path = uReplaceChar(path, '\\', '/'); + if(output.empty()) + { + output = path; + } + else + { + output = uReplaceChar(output, '~', UDirectory::homeDir()); + UDirectory::makeDir(output); + } + parameters.insert(ParametersPair(Parameters::kRtabmapWorkingDirectory(), output)); + parameters.insert(ParametersPair(Parameters::kRtabmapPublishRAMUsage(), "true")); + } + + std::string seq = uSplit(path, '/').back(); + std::string pathRgbImages = path+"/color"; + std::string pathDepthImages = path+"/depth"; + std::string pathGt = recordGt?path+"/groundtruth.txt":""; + if(recordGt && !UFile::exists(pathGt)) + { + UWARN("Ground truth file path doesn't exist: \"%s\", benchmark values won't be computed.", pathGt.c_str()); + pathGt.clear(); + } + std::string pathOdom = useOdom?path+"/rtabmap_odom.txt":""; + if(useOdom && !UFile::exists(pathOdom)) + { + std::string orgOdom = path+"/odom.txt"; + if(UFile::exists(orgOdom)) + { + printf("Converting odom.txt to rtabmap_odom.txt..."); + std::ifstream inputFile(orgOdom); + std::ofstream outputFile(pathOdom); + std::string line; + + if (!inputFile.is_open()) { + UERROR("Error: Could not open input file: %s", orgOdom.c_str()); + return 1; + } + + double previousStamp = 0.0; + float odomX = 0.0f; + float odomY = 0.0f; + float odomTheta = 0.0f; + while (std::getline(inputFile, line)) { + auto strList = uListToVector(uSplit(line, ' ')); + if(line.empty()) + { + break; + } + if(strList.size() != 14) + { + UERROR("Odometry shoud, have 14 entries per line, got %ld: \"%s\"", strList.size(), line.c_str()); + return 1; + } + // Recompute wheel odometry based on velocity + double stamp = uStr2Double(strList[0]); + if(previousStamp==0) + { + previousStamp = uStr2Double(strList[0]); + } + float dt = stamp - previousStamp; + float vx = uStr2Float(strList[8]); + float vtheta = uStr2Float(strList[13]); + + odomX += vx * cos(odomTheta) * dt; + odomY += vx * sin(odomTheta) * dt; + odomTheta = odomTheta + vtheta * dt; + + Transform t(odomX, odomY, odomTheta); + Eigen::Quaternionf q = t.getQuaternionf(); + outputFile << strList[0] << ' ' << t.x() << ' ' << t.y() << ' ' << t.z() << ' ' << q.x() << ' ' << q.y() << ' ' << q.z() << ' ' << q.w() << std::endl; + previousStamp = stamp; + } + + inputFile.close(); + outputFile.close(); + printf("Converting odom.txt to rtabmap_odom.txt...done!"); + } + else + { + pathOdom.clear(); + } + } + std::string pathImu = useImu?path+"/imu.txt":""; + if(useImu && !UFile::exists(pathImu)) + { + pathImu.clear(); + } + + if(quiet) + { + ULogger::setLevel(ULogger::kError); + } + + printf("Paths:\n" + " Dataset name: %s\n" + " Dataset path: %s\n" + " Color path: %s\n" + " Depth path: %s\n" + " Output: %s\n" + " Output name: %s\n" + " Max time diff: %f\n", + seq.c_str(), + path.c_str(), + pathRgbImages.c_str(), + pathDepthImages.c_str(), + output.c_str(), + outputName.c_str(), + maxTimeDiff); + printf(" Ground Truth: %s\n", !pathGt.empty()?pathGt.c_str():"Set --gt to record ground truth"); + printf(" Odometry: %s\n", !pathOdom.empty()?pathOdom.c_str():"Set --odom use wheel odometry"); + printf(" IMU: %s\n", !pathImu.empty()?pathImu.c_str():"Set --imu 1 to use IMU"); + printf(" IMU Filter: %d\n", imuFilter); + if(!parameters.empty()) + { + printf("Parameters:\n"); + for(ParametersMap::iterator iter=parameters.begin(); iter!=parameters.end(); ++iter) + { + printf(" %s=%s\n", iter->first.c_str(), iter->second.c_str()); + } + } + printf("RTAB-Map version: %s\n", RTABMAP_VERSION); + + // setup calibration file, based on https://cid-sims.github.io/calibration/calibration.yaml + CameraModel model(outputName+"_calib", + 386.52199190267083, 387.32300428823663, + 326.5103569741365, 237.40293732598795, + Transform::getIdentity(), 0, cv::Size(640,480)); + std::string sequenceName = UFile(path).getName(); + // The ground truth corresponds to camera frame, thus make the camera the base frame + Transform cameraHigh( + 0.013246, 0.0521412, 0.9982324, pathGt.empty()?0.28:0, + -0.9962321, 0.0610127, 0.0024175, 0, + -0.05647427, -0.9950674, 0.0591213, pathGt.empty()?0.20:0); + Transform cameraLow( + -0.00477153, 0.0888742, 0.995711, pathGt.empty()?0.369117:0, + -0.99571, 0.0665738, -0.018348, pathGt.empty()?0.0130432:0, + -0.0637357, -0.99188, 0.094639, pathGt.empty()?0.016175:0); + Transform camImu( + 0.9999691, 0.00720362, -0.00314765, -0.02707507, + -0.0071841, 0.99995517, 0.0061682, -0.004337, + 0.00319195, -0.0061454, 0.99997602, -0.01595186); + Transform imuHigh = cameraHigh * camImu; // base->IMU + Transform imuLow = cameraLow * camImu; // base->IMU + Transform baseToImu; + + // Based on https://cid-sims.github.io/overview/index.html + if( sequenceName.find("apartment1_1") != std::string::npos || + sequenceName.find("apartment2_1") != std::string::npos || + sequenceName.find("apartment2_3") != std::string::npos || + sequenceName.find("apartment3_1") != std::string::npos || + sequenceName.find("apartment3_1") != std::string::npos) + { + // using camera low + model.setLocalTransform(cameraLow); + baseToImu = imuLow; + } + else + { + // using camera high + model.setLocalTransform(cameraHigh); + baseToImu = imuHigh; + } + model.save(path); + + SensorCaptureThread cameraThread(new + CameraRGBDImages( + pathRgbImages, + pathDepthImages), parameters); + ((CameraRGBDImages*)cameraThread.camera())->setTimestamps(true, "", false); + if(!pathGt.empty()) + { + ((CameraRGBDImages*)cameraThread.camera())->setGroundTruthPath(pathGt, 1); + } + if(!pathOdom.empty()) + { + ((CameraRGBDImages*)cameraThread.camera())->setOdometryPath(pathOdom, 10); + } + ((CameraRGBDImages*)cameraThread.camera())->setMaxPoseTimeDiff(maxTimeDiff); + if(!pathImu.empty()) + { + cameraThread.enableIMUFiltering(imuFilter, parameters); + } + + bool intermediateNodes = Parameters::defaultRtabmapCreateIntermediateNodes(); + float detectionRate = Parameters::defaultRtabmapDetectionRate(); + int odomStrategy = Parameters::defaultOdomStrategy(); + Parameters::parse(parameters, Parameters::kRtabmapCreateIntermediateNodes(), intermediateNodes); + Parameters::parse(parameters, Parameters::kOdomStrategy(), odomStrategy); + Parameters::parse(parameters, Parameters::kRtabmapDetectionRate(), detectionRate); + std::string databasePath = output+"/"+outputName+".db"; + UFile::erase(databasePath); + if(cameraThread.camera()->init(path, outputName+"_calib")) + { + int totalImages = (int)((CameraRGBDImages*)cameraThread.camera())->filenames().size(); + + if(skipFrames>0) + { + totalImages /= skipFrames+1; + } + + printf("Processing %d images...\n", totalImages); + + ParametersMap odomParameters = parameters; + odomParameters.erase(Parameters::kRtabmapPublishRAMUsage()); // as odometry is in the same process than rtabmap, don't get RAM usage in odometry. + Odometry * odom = Odometry::create(odomParameters); + Rtabmap rtabmap; + rtabmap.init(parameters, databasePath); + + std::ifstream imu_file; + + if(!pathImu.empty()) + { + // open the IMU file + std::string line; + imu_file.open(pathImu.c_str()); + if (!imu_file.good()) { + UERROR("no imu file found at %s",pathImu.c_str()); + return -1; + } + int number_of_lines = 0; + while (std::getline(imu_file, line)) + ++number_of_lines; + printf("No. IMU measurements: %d\n", number_of_lines-1); + if (number_of_lines - 1 <= 0) { + UERROR("no imu messages present in %s", pathImu.c_str()); + return -1; + } + // set reading position to second line + imu_file.clear(); + imu_file.seekg(0, std::ios::beg); + std::getline(imu_file, line); + } + + UTimer totalTime; + UTimer timer; + SensorCaptureInfo cameraInfo; + SensorData data = cameraThread.camera()->takeData(&cameraInfo); + int iteration = 0; + double start = data.stamp(); + + ///////////////////////////// + // Processing dataset begin + ///////////////////////////// + int odomKeyFrames = 0; + double previousStamp = 0.0; + Transform previousOdomPose; + + while(data.isValid() && g_forever) + { + // get all IMU measurements till then + double t_imu = start; + do { + std::string line; + if (!std::getline(imu_file, line)) { + std::cout << std::endl << "Finished parsing IMU." << std::endl << std::flush; + break; + } + + std::stringstream stream(line); + std::string s; + std::getline(stream, s, ' '); + t_imu = uStr2Double(s); + + cv::Vec3d gyr; + for (int j = 0; j < 3; ++j) { + std::getline(stream, s, ' '); + gyr[j] = uStr2Double(s); + } + + cv::Vec3d acc; + for (int j = 0; j < 3; ++j) { + std::getline(stream, s, ' '); + acc[j] = uStr2Double(s); + } + + if (t_imu - start + 1 > 0) { + + SensorData dataImu(IMU(gyr, cv::Mat(3,3,CV_64FC1), acc, cv::Mat(3,3,CV_64FC1), baseToImu), 0, t_imu); + cameraThread.postUpdate(&dataImu); + odom->process(dataImu); + } + + } while (t_imu <= data.stamp()); + + cameraThread.postUpdate(&data, &cameraInfo); + cameraInfo.timeTotal = timer.ticks(); + + Transform guess = (!pathOdom.empty() && !cameraInfo.odomPose.isNull() && !previousOdomPose.isNull())?previousOdomPose.inverse() * cameraInfo.odomPose:Transform(); + + OdometryInfo odomInfo; + Transform previous = odom->getPose(); + Transform pose = odom->process(data, + guess, + &odomInfo); + + if(!pose.isNull() && odomInfo.reg.covariance.total() == 36) + { + previousOdomPose = cameraInfo.odomPose; + if(uIsFinite(odomInfo.reg.covariance.at(0,0)) && + odomInfo.reg.covariance.at(0,0)>0.0) + { + if( !pathOdom.empty() && + odomInfo.reg.covariance.at(0,0) >= 9999 && + !previousOdomPose.isNull() && + (pose.x() != 0.0f || pose.y() != 0.0f || pose.z() != 0.0f)) // not the first frame + { + // In case of external guess and auto reset, keep reporting lost till we + // process the second frame with valid covariance. This way it + // won't trigger a new map. + pose = Transform(); + } + } + } + + if(odomStrategy == 2) + { + //special case for FOVIS, set covariance 1 if 9999 is detected + if(!odomInfo.reg.covariance.empty() && odomInfo.reg.covariance.at(0,0) >= 9999) + { + odomInfo.reg.covariance = cv::Mat::eye(6,6,CV_64FC1); + } + } + + if(iteration!=0 && !pose.isNull() && !odomInfo.reg.covariance.empty() && odomInfo.reg.covariance.at(0,0)>=9999) + { + UWARN("Odometry is reset (high variance (%f >=9999 detected). Increment map id!", odomInfo.reg.covariance.at(0,0)); + rtabmap.triggerNewMap(); + } + + if(odomInfo.keyFrameAdded) + { + ++odomKeyFrames; + } + + bool processData = true; + if(detectionRate>0.0f && + previousStamp>0.0 && + data.stamp()>previousStamp && data.stamp() - previousStamp < 1.0/detectionRate) + { + processData = false; + } + + if(processData) + { + previousStamp = data.stamp(); + } + + if(!processData) + { + // set negative id so rtabmap will detect it as an intermediate node + data.setId(-1); + data.setFeatures(std::vector(), std::vector(), cv::Mat());// remove features + processData = intermediateNodes; + } + + timer.restart(); + if(processData) + { + std::map externalStats; + // save camera statistics to database + externalStats.insert(std::make_pair("Camera/BilateralFiltering/ms", cameraInfo.timeBilateralFiltering*1000.0f)); + externalStats.insert(std::make_pair("Camera/Capture/ms", cameraInfo.timeCapture*1000.0f)); + externalStats.insert(std::make_pair("Camera/Disparity/ms", cameraInfo.timeDisparity*1000.0f)); + externalStats.insert(std::make_pair("Camera/ImageDecimation/ms", cameraInfo.timeImageDecimation*1000.0f)); + externalStats.insert(std::make_pair("Camera/Mirroring/ms", cameraInfo.timeMirroring*1000.0f)); + externalStats.insert(std::make_pair("Camera/HistogramEqualization/ms", cameraInfo.timeHistogramEqualization*1000.0f)); + externalStats.insert(std::make_pair("Camera/ExposureCompensation/ms", cameraInfo.timeStereoExposureCompensation*1000.0f)); + externalStats.insert(std::make_pair("Camera/ScanFromDepth/ms", cameraInfo.timeScanFromDepth*1000.0f)); + externalStats.insert(std::make_pair("Camera/TotalTime/ms", cameraInfo.timeTotal*1000.0f)); + externalStats.insert(std::make_pair("Camera/UndistortDepth/ms", cameraInfo.timeUndistortDepth*1000.0f)); + // save odometry statistics to database + externalStats.insert(std::make_pair("Odometry/LocalBundle/ms", odomInfo.localBundleTime*1000.0f)); + externalStats.insert(std::make_pair("Odometry/LocalBundleConstraints/", odomInfo.localBundleConstraints)); + externalStats.insert(std::make_pair("Odometry/LocalBundleOutliers/", odomInfo.localBundleOutliers)); + externalStats.insert(std::make_pair("Odometry/TotalTime/ms", odomInfo.timeEstimation*1000.0f)); + externalStats.insert(std::make_pair("Odometry/Registration/ms", odomInfo.reg.totalTime*1000.0f)); + externalStats.insert(std::make_pair("Odometry/Inliers/", odomInfo.reg.inliers)); + externalStats.insert(std::make_pair("Odometry/Features/", odomInfo.features)); + externalStats.insert(std::make_pair("Odometry/DistanceTravelled/m", odomInfo.distanceTravelled)); + externalStats.insert(std::make_pair("Odometry/KeyFrameAdded/", odomInfo.keyFrameAdded)); + externalStats.insert(std::make_pair("Odometry/LocalKeyFrames/", odomInfo.localKeyFrames)); + externalStats.insert(std::make_pair("Odometry/LocalMapSize/", odomInfo.localMapSize)); + externalStats.insert(std::make_pair("Odometry/LocalScanMapSize/", odomInfo.localScanMapSize)); + + OdometryEvent e(SensorData(), Transform(), odomInfo); + rtabmap.process(data, pose, odomInfo.reg.covariance, e.velocity(), externalStats); + } + + ++iteration; + if(!quiet || iteration == totalImages) + { + double slamTime = timer.ticks(); + + float rmse = -1; + if(rtabmap.getStatistics().data().find(Statistics::kGtTranslational_rmse()) != rtabmap.getStatistics().data().end()) + { + rmse = rtabmap.getStatistics().data().at(Statistics::kGtTranslational_rmse()); + } + + if(rmse >= 0.0f) + { + printf("Iteration %d/%d: camera=%dms, odom(quality=%d/%d, kfs=%d)=%dms, slam=%dms, rmse=%fm", + iteration, totalImages, int(cameraInfo.timeTotal*1000.0f), odomInfo.reg.inliers, odomInfo.features, odomKeyFrames, int(odomInfo.timeEstimation*1000.0f), int(slamTime*1000.0f), rmse); + } + else + { + printf("Iteration %d/%d: camera=%dms, odom(quality=%d/%d, kfs=%d)=%dms, slam=%dms", + iteration, totalImages, int(cameraInfo.timeTotal*1000.0f), odomInfo.reg.inliers, odomInfo.features, odomKeyFrames, int(odomInfo.timeEstimation*1000.0f), int(slamTime*1000.0f)); + } + if(processData && rtabmap.getLoopClosureId()>0) + { + printf(" *"); + } + printf("\n"); + } + else if(iteration % (totalImages/10) == 0) + { + printf("."); + fflush(stdout); + } + + cameraInfo = SensorCaptureInfo(); + timer.restart(); + data = cameraThread.camera()->takeData(&cameraInfo); + } + delete odom; + printf("Total time=%fs\n", totalTime.ticks()); + ///////////////////////////// + // Processing dataset end + ///////////////////////////// + + // Save trajectory + printf("Saving trajectory...\n"); + std::map poses; + std::multimap links; + std::map signatures; + std::map stamps; + rtabmap.getGraph(poses, links, true, true, &signatures); + for(std::map::iterator iter=signatures.begin(); iter!=signatures.end(); ++iter) + { + stamps.insert(std::make_pair(iter->first, iter->second.getStamp())); + } + std::string pathTrajectory = output+"/"+outputName+"_poses.txt"; + if(poses.size() && graph::exportPoses(pathTrajectory, 1, poses, links, stamps)) + { + printf("Saving %s... done!\n", pathTrajectory.c_str()); + } + else + { + printf("Saving %s... failed!\n", pathTrajectory.c_str()); + } + + if(!pathGt.empty()) + { + // Log ground truth statistics + std::map groundTruth; + + for(std::map::const_iterator iter=poses.begin(); iter!=poses.end(); ++iter) + { + Transform o, gtPose; + int m,w; + std::string l; + double s; + std::vector v; + GPS gps; + EnvSensors sensors; + rtabmap.getMemory()->getNodeInfo(iter->first, o, m, w, l, s, gtPose, v, gps, sensors, true); + if(!gtPose.isNull()) + { + groundTruth.insert(std::make_pair(iter->first, gtPose)); + } + } + + + // compute RMSE statistics + float translational_rmse = 0.0f; + float translational_mean = 0.0f; + float translational_median = 0.0f; + float translational_std = 0.0f; + float translational_min = 0.0f; + float translational_max = 0.0f; + float rotational_rmse = 0.0f; + float rotational_mean = 0.0f; + float rotational_median = 0.0f; + float rotational_std = 0.0f; + float rotational_min = 0.0f; + float rotational_max = 0.0f; + graph::calcRMSE( + groundTruth, + poses, + translational_rmse, + translational_mean, + translational_median, + translational_std, + translational_min, + translational_max, + rotational_rmse, + rotational_mean, + rotational_median, + rotational_std, + rotational_min, + rotational_max); + + printf(" translational_rmse= %f m\n", translational_rmse); + printf(" rotational_rmse= %f deg\n", rotational_rmse); + + FILE * pFile = 0; + std::string pathErrors = output+"/"+outputName+"_rmse.txt"; + pFile = fopen(pathErrors.c_str(),"w"); + if(!pFile) + { + UERROR("could not save RMSE results to \"%s\"", pathErrors.c_str()); + } + fprintf(pFile, "Ground truth comparison:\n"); + fprintf(pFile, " translational_rmse= %f\n", translational_rmse); + fprintf(pFile, " translational_mean= %f\n", translational_mean); + fprintf(pFile, " translational_median= %f\n", translational_median); + fprintf(pFile, " translational_std= %f\n", translational_std); + fprintf(pFile, " translational_min= %f\n", translational_min); + fprintf(pFile, " translational_max= %f\n", translational_max); + fprintf(pFile, " rotational_rmse= %f\n", rotational_rmse); + fprintf(pFile, " rotational_mean= %f\n", rotational_mean); + fprintf(pFile, " rotational_median= %f\n", rotational_median); + fprintf(pFile, " rotational_std= %f\n", rotational_std); + fprintf(pFile, " rotational_min= %f\n", rotational_min); + fprintf(pFile, " rotational_max= %f\n", rotational_max); + fclose(pFile); + } + } + else + { + UERROR("Camera init failed!"); + } + + printf("Saving rtabmap database (with all statistics) to \"%s\"\n", (output+"/"+outputName+".db").c_str()); + printf("Do:\n" + " $ rtabmap-databaseViewer %s\n\n", (output+"/"+outputName+".db").c_str()); + + return 0; +} From 94bd3601fcbba413083524fcba97bf2d43a30165 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sat, 28 Mar 2026 18:47:18 -0700 Subject: [PATCH 090/126] rtabmap-matcher: Added option to recfify raw images for convenience (#1663) --- tools/Matcher/main.cpp | 61 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/tools/Matcher/main.cpp b/tools/Matcher/main.cpp index f99a3f06..04b69e33 100644 --- a/tools/Matcher/main.cpp +++ b/tools/Matcher/main.cpp @@ -87,6 +87,11 @@ void showUsage() " --to_depth \"to_depth.png\" Depth or right image file of the second image.\n" " For 3D->3D estimation, from_depth and to_depth\n" " should be both set.\n" + " --raw Provided images are raw and should be rectified.\n" + " Doesn't need to be explicitly set if calibration\n" + " is not provided. For RGB-D data, only the RGB image\n" + " is rectified, the depth is assumed already matching\n" + " the rectified one.\n" "\n\n" "%s\n", Parameters::showUsage()); @@ -107,6 +112,7 @@ int main(int argc, char * argv[]) std::string toDepthPath; std::string calibrationPath; std::string calibrationToPath; + bool imagesRectified = true; for(int i=1; i2D estimation for mono images parameters.insert(ParametersPair(Parameters::kVisEpipolarGeometryVar(), "1")); //Unknown scale - printf("Calibration not set, setting %s=1 and %s=2 by default (2D->2D estimation)\n", Parameters::kVisEpipolarGeometryVar().c_str(), Parameters::kVisEstimationType().c_str()); + printf("Depth/Stereo not set, setting %s=1 and %s=2 by default (2D->2D estimation)\n", Parameters::kVisEpipolarGeometryVar().c_str(), Parameters::kVisEstimationType().c_str()); } RegistrationVis reg(parameters); RegistrationInfo info; From 20409d2bf61db69c2e803c639f9f2a08598d69b2 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sat, 28 Mar 2026 18:47:46 -0700 Subject: [PATCH 091/126] Cudasift tuning and SSC supporting multicameras (#1677) * CudaSIFT: filter doubles * removed fixed threshold * SSC can be used with multicameras. Refactored CudaSIFT to support SSC. Add new parameter SIFT/MaxGaussianThreshold. DbViewer: show negative features with gray color (so that we can know which features are in the vocabulary) * Added SIFT/MaxGaussianThreshold parameter * Updated parameter description --- corelib/include/rtabmap/core/Features2d.h | 3 +- corelib/include/rtabmap/core/Parameters.h | 7 +- corelib/src/Features2d.cpp | 97 +++++----- corelib/src/Memory.cpp | 34 ++-- corelib/src/RegistrationVis.cpp | 22 +-- corelib/src/util2d.cpp | 1 + guilib/src/DatabaseViewer.cpp | 8 + guilib/src/ImageView.cpp | 4 +- guilib/src/PreferencesDialog.cpp | 3 + guilib/src/ui/preferencesDialog.ui | 214 +++++++++++++--------- tools/Reprocess/main.cpp | 19 +- 11 files changed, 232 insertions(+), 180 deletions(-) diff --git a/corelib/include/rtabmap/core/Features2d.h b/corelib/include/rtabmap/core/Features2d.h index e999a1fd..eec3f57b 100644 --- a/corelib/include/rtabmap/core/Features2d.h +++ b/corelib/include/rtabmap/core/Features2d.h @@ -309,7 +309,8 @@ private: bool preciseUpscale_; bool rootSIFT_; bool gpu_; - float guaussianThreshold_; + float gaussianThreshold_; + float maxGaussianThreshold_; bool upscale_; cv::Ptr sift_; diff --git a/corelib/include/rtabmap/core/Parameters.h b/corelib/include/rtabmap/core/Parameters.h index 4ee6f6f3..e0ae0e92 100644 --- a/corelib/include/rtabmap/core/Parameters.h +++ b/corelib/include/rtabmap/core/Parameters.h @@ -224,7 +224,7 @@ class RTABMAP_CORE_EXPORT Parameters RTABMAP_PARAM(Mem, BadSignaturesIgnored, bool, false, "Bad signatures are ignored."); RTABMAP_PARAM(Mem, InitWMWithAllNodes, bool, false, "Initialize the Working Memory with all nodes in Long-Term Memory. When false, it is initialized with nodes of the previous session."); RTABMAP_PARAM(Mem, DepthAsMask, bool, true, "Use depth image as mask when extracting features for vocabulary."); - RTABMAP_PARAM(Mem, DepthMaskFloorThr, float, 0.0, uFormat("Filter floor from depth mask below specified threshold (m) before extracting features. 0 means disabled, negative means remove all objects above the floor threshold instead. Ignored if %s is false.", kMemDepthAsMask().c_str())); + RTABMAP_PARAM(Mem, DepthMaskFloorThr, float, 0.0, uFormat("Filter floor from depth mask below specified threshold (m) before extracting features. 0 means disabled. Ignored if %s is false.", kMemDepthAsMask().c_str())); RTABMAP_PARAM(Mem, StereoFromMotion, bool, false, uFormat("Triangulate features without depth using stereo from motion (odometry). It would be ignored if %s is true and the feature detector used supports masking.", kMemDepthAsMask().c_str())); RTABMAP_PARAM(Mem, ImagePreDecimation, unsigned int, 1, uFormat("Decimation of the RGB image before visual feature detection. If depth size is larger than decimated RGB size, depth is decimated to be always at most equal to RGB size. If %s is true and if depth is smaller than decimated RGB, depth may be interpolated to match RGB size for feature detection.",kMemDepthAsMask().c_str())); RTABMAP_PARAM(Mem, ImagePostDecimation, unsigned int, 1, uFormat("Decimation of the RGB image before saving it to database. If depth size is larger than decimated RGB size, depth is decimated to be always at most equal to RGB size. Decimation is done from the original image. If set to same value than %s, data already decimated is saved (no need to re-decimate the image).", kMemImagePreDecimation().c_str())); @@ -294,7 +294,8 @@ class RTABMAP_CORE_EXPORT Parameters RTABMAP_PARAM(SIFT, PreciseUpscale, bool, false, "Whether to enable precise upscaling in the scale pyramid (OpenCV >= 4.8)."); RTABMAP_PARAM(SIFT, RootSIFT, bool, false, "Apply RootSIFT normalization of the descriptors."); RTABMAP_PARAM(SIFT, Gpu, bool, false, "CudaSift: Use GPU version of SIFT. This option is enabled only if RTAB-Map is built with CudaSift dependency and GPUs are detected."); - RTABMAP_PARAM(SIFT, GaussianThreshold, float, 2.0, "CudaSift: Threshold on difference of Gaussians for feature pruning. The higher the threshold, the less features are produced by the detector."); + RTABMAP_PARAM(SIFT, GaussianThreshold, float, 2.0, "CudaSift: Threshold on difference of Gaussians for feature pruning. The higher the threshold, the less features with low response/hessian are produced by the detector."); + RTABMAP_PARAM(SIFT, MaxGaussianThreshold, float, 0.0, uFormat("CudaSift: Maximum threshold on difference of Gaussians for feature pruning (ignored if smaller or equal than %s). The lower the threshold, the less features with high response/hessian are produced by the detector.", kSIFTGaussianThreshold().c_str())); RTABMAP_PARAM(SIFT, Upscale, bool, false, "CudaSift: Whether to enable upscaling."); RTABMAP_PARAM(BRIEF, Bytes, int, 32, "Bytes is a length of descriptor in bytes. It can be equal 16, 32 or 64 bytes."); @@ -724,7 +725,7 @@ class RTABMAP_CORE_EXPORT Parameters RTABMAP_PARAM(Vis, MaxDepth, float, 0, "Max depth of the features (0 means no limit)."); RTABMAP_PARAM(Vis, MinDepth, float, 0, "Min depth of the features (0 means no limit)."); RTABMAP_PARAM(Vis, DepthAsMask, bool, true, "Use depth image as mask when extracting features."); - RTABMAP_PARAM(Vis, DepthMaskFloorThr, float, 0.0, uFormat("Filter floor from depth mask below specified threshold (m) before extracting features. 0 means disabled, negative means remove all objects above the floor threshold instead. Ignored if %s is false.", kVisDepthAsMask().c_str())); + RTABMAP_PARAM(Vis, DepthMaskFloorThr, float, 0.0, uFormat("Filter floor from depth mask below specified threshold (m) before extracting features. 0 means disabled. Ignored if %s is false.", kVisDepthAsMask().c_str())); RTABMAP_PARAM_STR(Vis, RoiRatios, "0.0 0.0 0.0 0.0", "Region of interest ratios [left, right, top, bottom]."); RTABMAP_PARAM(Vis, SubPixWinSize, int, 3, "See cv::cornerSubPix()."); RTABMAP_PARAM(Vis, SubPixIterations, int, 0, "See cv::cornerSubPix(). 0 disables sub pixel refining."); diff --git a/corelib/src/Features2d.cpp b/corelib/src/Features2d.cpp index e3fc45fd..c3c0f93b 100644 --- a/corelib/src/Features2d.cpp +++ b/corelib/src/Features2d.cpp @@ -466,7 +466,7 @@ void Feature2D::limitKeypoints(const std::vector & keypoints, std: minimumHessian = iter->first; } } - ULOGGER_DEBUG("%d keypoints removed, (kept %d), minimum response=%f", removed, maxKeypoints, minimumHessian); + ULOGGER_DEBUG("%d keypoints removed, (kept %d), minimum response=%f", removed, keypoints.size()-removed, minimumHessian); ULOGGER_DEBUG("filter keypoints time = %f s", timer.ticks()); } else @@ -1251,7 +1251,8 @@ SIFT::SIFT(const ParametersMap & parameters) : preciseUpscale_(Parameters::defaultSIFTPreciseUpscale()), rootSIFT_(Parameters::defaultSIFTRootSIFT()), gpu_(Parameters::defaultSIFTGpu()), - guaussianThreshold_(Parameters::defaultSIFTGaussianThreshold()), + gaussianThreshold_(Parameters::defaultSIFTGaussianThreshold()), + maxGaussianThreshold_(Parameters::defaultSIFTMaxGaussianThreshold()), upscale_(Parameters::defaultSIFTUpscale()), cudaSiftData_(0), cudaSiftMemory_(0), @@ -1284,23 +1285,25 @@ void SIFT::parseParameters(const ParametersMap & parameters) Parameters::parse(parameters, Parameters::kSIFTPreciseUpscale(), preciseUpscale_); Parameters::parse(parameters, Parameters::kSIFTRootSIFT(), rootSIFT_); Parameters::parse(parameters, Parameters::kSIFTGpu(), gpu_); - Parameters::parse(parameters, Parameters::kSIFTGaussianThreshold(), guaussianThreshold_); + Parameters::parse(parameters, Parameters::kSIFTGaussianThreshold(), gaussianThreshold_); + Parameters::parse(parameters, Parameters::kSIFTMaxGaussianThreshold(), maxGaussianThreshold_); Parameters::parse(parameters, Parameters::kSIFTUpscale(), upscale_); if(gpu_) { #ifdef RTABMAP_CUDASIFT // Check if there is a cuda device - if(InitCuda(0, ULogger::level() == ULogger::kDebug)) { - UDEBUG("Init SiftData"); - if(cudaSiftData_ == 0) { + if(cudaSiftData_==0) + { + if(InitCuda(0, ULogger::level() == ULogger::kDebug)) { + UDEBUG("Init SiftData"); cudaSiftData_ = new SiftData(); InitSiftData(*cudaSiftData_, 8192, true, true); } - } - else{ - UWARN("No cuda device(s) detected, CudaSift is not available! Using SIFT CPU version instead."); - gpu_ = false; + else{ + UWARN("No cuda device(s) detected, CudaSift is not available! Using SIFT CPU version instead."); + gpu_ = false; + } } #else UWARN("RTAB-Map is not built with CudaSift so %s cannot be used!", Parameters::kSIFTGpu().c_str()); @@ -1363,7 +1366,7 @@ std::vector SIFT::generateKeypointsImpl(const cv::Mat & image, con numOctaves = 7; // hard-coded limit in CudaSift } float initBlur = sigma_; /* Amount of initial Gaussian blurring in standard deviations */ - float thresh = guaussianThreshold_; /* Threshold on difference of Gaussians for feature pruning */ + float thresh = gaussianThreshold_; /* Threshold on difference of Gaussians for feature pruning */ float edgeLimit = edgeThreshold_; float minScale = 0.0f; /* Minimum acceptable scale to remove fine-scale features */ UDEBUG("numOctaves=%d initBlur=%f thresh=%f edgeLimit=%f minScale=%f upScale=%s w=%d h=%d", numOctaves, initBlur, thresh, edgeLimit, minScale, upscale_?"true":"false", w, h); @@ -1388,15 +1391,9 @@ std::vector SIFT::generateKeypointsImpl(const cv::Mat & image, con cudaSiftDescriptors_ = cv::Mat(); if(cudaSiftData_->numPts) { - int maxKeypoints = this->getMaxFeatures(); - if(maxKeypoints == 0 || maxKeypoints > cudaSiftData_->numPts) - { - maxKeypoints = cudaSiftData_->numPts; - } - - // Re-using same implementation of limitKeypoints() directly here to avoid doubling memory copies - // Sort words by hessian - std::multimap hessianMap; // + keypoints.resize(cudaSiftData_->numPts); + cudaSiftDescriptors_ = cv::Mat(cudaSiftData_->numPts, 128, CV_32FC1); + size_t k=0; for(int i=0; inumPts; ++i) { // Ignore keypoints with invalid descriptors @@ -1413,29 +1410,40 @@ std::vector SIFT::generateKeypointsImpl(const cv::Mat & image, con continue; } - //Keep track of the data, to be easier to manage the data in the next step - hessianMap.insert(std::pair(abs(cudaSiftData_->h_data[i].sharpness), i)); - } + if(i>0 && + cudaSiftData_->h_data[i].subsampling == cudaSiftData_->h_data[i-1].subsampling && + fabs(cudaSiftData_->h_data[i].xpos-cudaSiftData_->h_data[i-1].xpos) + + fabs(cudaSiftData_->h_data[i].xpos-cudaSiftData_->h_data[i-1].ypos) < 0.1f) + { + // Same feature, skip doubles + continue; + } - if((int)hessianMap.size() < maxKeypoints) - { - maxKeypoints = hessianMap.size(); - } + float response = abs(cudaSiftData_->h_data[i].sharpness); + if(maxGaussianThreshold_>gaussianThreshold_ && response > maxGaussianThreshold_) + { + continue; + } - std::multimap::reverse_iterator iter = hessianMap.rbegin(); - keypoints.resize(maxKeypoints); - cudaSiftDescriptors_ = cv::Mat(maxKeypoints, 128, CV_32FC1); - for(unsigned int k=0; ksecond; - float *desc = cudaSiftData_->h_data[i].data; cv::Mat(1, 128, CV_32FC1, desc).copyTo(cudaSiftDescriptors_.row(k)); keypoints[k].pt.x = cudaSiftData_->h_data[i].xpos; keypoints[k].pt.y = cudaSiftData_->h_data[i].ypos; keypoints[k].size = 2.0f*cudaSiftData_->h_data[i].scale; // x2 because the scale is more like a radius than a diameter, see CudaSift's ExtractSiftDescriptors function to see how they convert scale to patch size keypoints[k].angle = cudaSiftData_->h_data[i].orientation; - keypoints[k].response = abs(cudaSiftData_->h_data[i].sharpness); + keypoints[k].response = response; keypoints[k].octave = log2(cudaSiftData_->h_data[i].subsampling)-(upscale_?1:0); + ++k; + } + if(k < keypoints.size()) + { + UDEBUG("keypoints extracted = %d, valid=%d", keypoints.size(), k); + keypoints.resize(k); + cudaSiftDescriptors_.resize(k); + } + if(this->getMaxFeatures() != 0 && this->getMaxFeatures() < (int)keypoints.size()) + { + // Call limitKeypoints() now to filter the descriptors. + this->limitKeypoints(keypoints, cudaSiftDescriptors_, this->getMaxFeatures(), cv::Size(w,h), this->getSSC()); } } } @@ -1457,12 +1465,13 @@ std::vector SIFT::generateKeypointsImpl(const cv::Mat & image, con cv::Mat SIFT::generateDescriptorsImpl(const cv::Mat & image, std::vector & keypoints) const { + cv::Mat descriptors; #ifdef RTABMAP_CUDASIFT if(gpu_) { if((int)keypoints.size() == cudaSiftDescriptors_.rows) { - return cudaSiftDescriptors_.clone(); + descriptors = cudaSiftDescriptors_.clone(); } else { @@ -1470,19 +1479,25 @@ cv::Mat SIFT::generateDescriptorsImpl(const cv::Mat & image, std::vectorcompute(image, keypoints, descriptors); + sift_->compute(image, keypoints, descriptors); #else - UWARN("RTAB-Map is not built with OpenCV nonfree module so SIFT cannot be used!"); + UWARN("RTAB-Map is not built with OpenCV nonfree module so SIFT cannot be used!"); #endif #else // >=4.4, >=3.4.11 - sift_->compute(image, keypoints, descriptors); + sift_->compute(image, keypoints, descriptors); #endif + +#ifdef RTABMAP_CUDASIFT + } +#endif + if( rootSIFT_ && !descriptors.empty()) { UDEBUG("Performing RootSIFT..."); diff --git a/corelib/src/Memory.cpp b/corelib/src/Memory.cpp index 3d046724..0bee2e91 100644 --- a/corelib/src/Memory.cpp +++ b/corelib/src/Memory.cpp @@ -5168,16 +5168,7 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor { UASSERT(!decimatedData.cameraModels().empty()); UDEBUG("Masking floor (threshold=%f)", _maskFloorThreshold); - if(_maskFloorThreshold<0.0f) - { - cv::Mat depthBelow; - util3d::filterFloor(depthMask, decimatedData.cameraModels(), _maskFloorThreshold*-1.0f, &depthBelow); - depthMask = depthBelow; - } - else - { - depthMask = util3d::filterFloor(depthMask, decimatedData.cameraModels(), _maskFloorThreshold); - } + depthMask = util3d::filterFloor(depthMask, decimatedData.cameraModels(), _maskFloorThreshold); UDEBUG("Masking floor done."); } @@ -5227,6 +5218,7 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor else { int oldMaxFeatures = _feature2D->getMaxFeatures(); + bool oldSSC = _feature2D->getSSC(); UDEBUG("rawDescriptorsKept=%d, pose=%d, maxFeatures=%d, visMaxFeatures=%d", _rawDescriptorsKept?1:0, pose.isNull()?0:1, _feature2D->getMaxFeatures(), _visMaxFeatures); ParametersMap tmpMaxFeatureParameter; if(_rawDescriptorsKept&&!pose.isNull()&&_feature2D->getMaxFeatures()>0&&_feature2D->getMaxFeatures()<_visMaxFeatures) @@ -5234,6 +5226,7 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor // The total extracted features should match the number of features used for transformation estimation UDEBUG("Changing temporary max features from %d to %d", _feature2D->getMaxFeatures(), _visMaxFeatures); tmpMaxFeatureParameter.insert(ParametersPair(Parameters::kKpMaxFeatures(), uNumber2Str(_visMaxFeatures))); + tmpMaxFeatureParameter.insert(ParametersPair(Parameters::kKpSSC(), uNumber2Str(_visSSC))); _feature2D->parseParameters(tmpMaxFeatureParameter); } @@ -5244,6 +5237,7 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor if(tmpMaxFeatureParameter.size()) { tmpMaxFeatureParameter.at(Parameters::kKpMaxFeatures()) = uNumber2Str(oldMaxFeatures); + tmpMaxFeatureParameter.at(Parameters::kKpSSC()) = uBool2Str(oldSSC); _feature2D->parseParameters(tmpMaxFeatureParameter); // reset back } t = timer.ticks(); @@ -5444,8 +5438,8 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor bool ssc = _rawDescriptorsKept&&!pose.isNull()&&_feature2D->getMaxFeatures()>0&&_feature2D->getMaxFeatures()<_visMaxFeatures?_visSSC:_feature2D->getSSC(); if((int)keypoints.size() > maxFeatures) { - if(data.cameraModels().size()==1 || data.stereoCameraModels().size()==1) - _feature2D->limitKeypoints(keypoints, keypoints3D, descriptors, maxFeatures, data.cameraModels().size()?data.cameraModels()[0].imageSize():data.stereoCameraModels()[0].left().imageSize(), ssc); + if(data.cameraModels().size()>=1 || data.stereoCameraModels().size()>=1) + _feature2D->limitKeypoints(keypoints, keypoints3D, descriptors, maxFeatures, data.cameraModels().size()?cv::Size(data.cameraModels()[0].imageWidth()*data.cameraModels().size(), data.cameraModels()[0].imageHeight()):cv::Size(data.stereoCameraModels()[0].left().imageWidth()*data.stereoCameraModels().size(), data.stereoCameraModels()[0].left().imageHeight()), ssc); else _feature2D->limitKeypoints(keypoints, keypoints3D, descriptors, maxFeatures); } @@ -5678,13 +5672,17 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor UWARN("Ignored %s and %s parameters as they cannot be used for multi-cameras setup or uncalibrated camera.", Parameters::kKpGridCols().c_str(), Parameters::kKpGridRows().c_str()); } - if(decimatedData.cameraModels().size()==1 || decimatedData.stereoCameraModels().size()==1 || - data.cameraModels().size()==1 || data.stereoCameraModels().size()==1) + if(decimatedData.cameraModels().size()>=1 || decimatedData.stereoCameraModels().size()>=1 || + data.cameraModels().size()>=1 || data.stereoCameraModels().size()>=1) { - Feature2D::limitKeypoints(keypoints, inliers, _feature2D->getMaxFeatures(), - decimatedData.cameraModels().size()?decimatedData.cameraModels()[0].imageSize(): - decimatedData.stereoCameraModels().size()?decimatedData.stereoCameraModels()[0].left().imageSize(): - data.cameraModels().size()?data.cameraModels()[0].imageSize():data.stereoCameraModels()[0].left().imageSize(), + Feature2D::limitKeypoints( + keypoints, + inliers, + _feature2D->getMaxFeatures(), + decimatedData.cameraModels().size()?cv::Size(decimatedData.cameraModels()[0].imageWidth()*decimatedData.cameraModels().size(), decimatedData.cameraModels()[0].imageHeight()): + decimatedData.stereoCameraModels().size()?cv::Size(decimatedData.stereoCameraModels()[0].left().imageWidth()*decimatedData.stereoCameraModels().size(), decimatedData.stereoCameraModels()[0].left().imageWidth()): + data.cameraModels().size()?cv::Size(data.cameraModels()[0].imageWidth()*data.cameraModels().size(), data.cameraModels()[0].imageHeight()): + cv::Size(data.stereoCameraModels()[0].left().imageWidth()*data.stereoCameraModels().size(), data.stereoCameraModels()[0].left().imageHeight()), _feature2D->getSSC()); } else diff --git a/corelib/src/RegistrationVis.cpp b/corelib/src/RegistrationVis.cpp index 5be29749..738e72b3 100644 --- a/corelib/src/RegistrationVis.cpp +++ b/corelib/src/RegistrationVis.cpp @@ -447,16 +447,7 @@ Transform RegistrationVis::computeTransformationImpl( { UASSERT(!fromSignature.sensorData().cameraModels().empty()); UDEBUG("Masking floor (threshold=%f)", _maskFloorThreshold); - if(_maskFloorThreshold<0.0f) - { - cv::Mat depthBelow; - util3d::filterFloor(depthMask, fromSignature.sensorData().cameraModels(), _maskFloorThreshold*-1.0f, &depthBelow); - depthMask = depthBelow; - } - else - { - depthMask = util3d::filterFloor(depthMask, fromSignature.sensorData().cameraModels(), _maskFloorThreshold); - } + depthMask = util3d::filterFloor(depthMask, fromSignature.sensorData().cameraModels(), _maskFloorThreshold); UDEBUG("Masking floor done."); } @@ -817,16 +808,7 @@ Transform RegistrationVis::computeTransformationImpl( { UASSERT(!toSignature.sensorData().cameraModels().empty()); UDEBUG("Masking floor (threshold=%f)", _maskFloorThreshold); - if(_maskFloorThreshold<0.0f) - { - cv::Mat depthBelow; - util3d::filterFloor(depthMask, toSignature.sensorData().cameraModels(), _maskFloorThreshold*-1.0f, &depthBelow); - depthMask = depthBelow; - } - else - { - depthMask = util3d::filterFloor(depthMask, toSignature.sensorData().cameraModels(), _maskFloorThreshold); - } + depthMask = util3d::filterFloor(depthMask, toSignature.sensorData().cameraModels(), _maskFloorThreshold); UDEBUG("Masking floor done."); } diff --git a/corelib/src/util2d.cpp b/corelib/src/util2d.cpp index 604eaa33..0251d0fe 100644 --- a/corelib/src/util2d.cpp +++ b/corelib/src/util2d.cpp @@ -2296,6 +2296,7 @@ std::vector SSC( const std::vector & keypoints, int maxKeypoints, float tolerance, int cols, int rows, const std::vector & indx) { bool useIndx = keypoints.size() == indx.size(); + maxKeypoints = maxKeypoints - round(maxKeypoints * tolerance); // Just the make sure the solution will always be <= input maxKeypoints // several temp expression variables to simplify solution equation int exp1 = rows + cols + 2*maxKeypoints; diff --git a/guilib/src/DatabaseViewer.cpp b/guilib/src/DatabaseViewer.cpp index f2bb5aab..ed7c4059 100644 --- a/guilib/src/DatabaseViewer.cpp +++ b/guilib/src/DatabaseViewer.cpp @@ -6163,6 +6163,14 @@ void DatabaseViewer::updateWordsMatching(const std::vector & inliers) kptB->keypoint().pt.y, cB); } + else if(ids[i]<0) + { + ui_->graphicsView_A->setFeatureColor(ids[i], Qt::gray); + } + } + for(auto iter = wordsB.begin(); iter.key()<0 && iter!=wordsB.end(); ++iter) + { + ui_->graphicsView_B->setFeatureColor(iter.key(), Qt::gray); } ui_->graphicsView_A->update(); ui_->graphicsView_B->update(); diff --git a/guilib/src/ImageView.cpp b/guilib/src/ImageView.cpp index cdcb28bb..84f03687 100644 --- a/guilib/src/ImageView.cpp +++ b/guilib/src/ImageView.cpp @@ -1255,11 +1255,11 @@ void ImageView::setFeatures(const std::multimap & refWords, c { if (xRatio > 0 && yRatio > 0) { - addFeature(iter->first, iter->second, util2d::getDepth(depth, iter->second.pt.x*xRatio, iter->second.pt.y*yRatio, false), color); + addFeature(iter->first, iter->second, util2d::getDepth(depth, iter->second.pt.x*xRatio, iter->second.pt.y*yRatio, false), iter->first<0?Qt::gray:color); } else { - addFeature(iter->first, iter->second, 0, color); + addFeature(iter->first, iter->second, 0, iter->first<0?Qt::gray:color); } } diff --git a/guilib/src/PreferencesDialog.cpp b/guilib/src/PreferencesDialog.cpp index 0822cd76..00bb6e5b 100644 --- a/guilib/src/PreferencesDialog.cpp +++ b/guilib/src/PreferencesDialog.cpp @@ -171,6 +171,8 @@ PreferencesDialog::PreferencesDialog(QWidget * parent) : _ui->sift_label_gpu->setEnabled(false); _ui->sift_doubleSpinBox_gaussianDiffThreshold->setEnabled(false); _ui->sift_label_gaussianThreshold->setEnabled(false); + _ui->sift_doubleSpinBox_maxGaussianDiffThreshold->setEnabled(false); + _ui->sift_label_maxGaussianThreshold->setEnabled(false); _ui->sift_checkBox_upscale->setEnabled(false); _ui->sift_label_upscale->setEnabled(false); #endif @@ -1145,6 +1147,7 @@ PreferencesDialog::PreferencesDialog(QWidget * parent) : _ui->sift_checkBox_rootsift->setObjectName(Parameters::kSIFTRootSIFT().c_str()); _ui->sift_checkBox_gpu->setObjectName(Parameters::kSIFTGpu().c_str()); _ui->sift_doubleSpinBox_gaussianDiffThreshold->setObjectName(Parameters::kSIFTGaussianThreshold().c_str()); + _ui->sift_doubleSpinBox_maxGaussianDiffThreshold->setObjectName(Parameters::kSIFTMaxGaussianThreshold().c_str()); _ui->sift_checkBox_upscale->setObjectName(Parameters::kSIFTUpscale().c_str()); //BRIEF descriptor diff --git a/guilib/src/ui/preferencesDialog.ui b/guilib/src/ui/preferencesDialog.ui index 5d56b2b4..5787cae7 100644 --- a/guilib/src/ui/preferencesDialog.ui +++ b/guilib/src/ui/preferencesDialog.ui @@ -63,9 +63,9 @@ 0 - -2161 + -96 684 - 5201 + 5218 @@ -95,7 +95,7 @@ QFrame::Raised - 5 + 9 @@ -11659,7 +11659,7 @@ generate the number of words requested. - Filter floor from depth mask. 0 means disabled, negative means keeping pixels below the floor theshold instead. + Filter floor from depth mask. 0 means disabled. true @@ -23860,7 +23860,7 @@ Lower the ratio -> higher the precision. - Filter floor from depth mask. 0 means disabled, negative means keeping pixels below the floor theshold instead. + Filter floor from depth mask. 0 means disabled. true @@ -26733,39 +26733,6 @@ Lower the ratio -> higher the precision. SIFT - - - - - - - - - - - Edge threshold. The threshold used to filter out edge-like features. Note that the its meaning is different from the contrastThreshold, i.e. the larger the edgeThreshold, the less features are filtered out (more features are retained). - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - - - - Whether to enable precise upscaling in the scale pyramid. - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - @@ -26779,13 +26746,53 @@ Lower the ratio -> higher the precision. - - - - 0.100000000000000 + + + + - - 10.000000000000000 + + + + + + Whether to enable precise upscaling in the scale pyramid. + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + CudaSift: Threshold on difference of Gaussians for feature pruning. The higher the threshold, the less features with low response/hessian are produced by the detector. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + + + + + Contrast threshold. The contrast threshold used to filter out weak features in semi-uniform (low-contrast) regions. The larger the threshold, the less features are produced by the detector. Not used by CudaSift. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse @@ -26796,6 +26803,42 @@ Lower the ratio -> higher the precision. + + + + + + + + + + + CudaSift: Use GPU version of SIFT. This option is enabled only RTAB-Map is built with CudaSift dependency and GPUs are detected. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + Sigma. The sigma of the Gaussian applied to the input image at the octave #0. If your image is captured with a weak camera with soft lenses, you might want to reduce the number. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + @@ -26825,23 +26868,20 @@ Lower the ratio -> higher the precision. - - + + - CudaSift: Use GPU version of SIFT. This option is enabled only RTAB-Map is built with CudaSift dependency and GPUs are detected. - - - true + CudaSift: Whether to enable upscaling. Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - + + - Contrast threshold. The contrast threshold used to filter out weak features in semi-uniform (low-contrast) regions. The larger the threshold, the less features are produced by the detector. Not used by CudaSift. + Edge threshold. The threshold used to filter out edge-like features. Note that the its meaning is different from the contrastThreshold, i.e. the larger the edgeThreshold, the less features are filtered out (more features are retained). true @@ -26870,6 +26910,16 @@ Lower the ratio -> higher the precision. + + + + 0.100000000000000 + + + 10.000000000000000 + + + @@ -26880,43 +26930,13 @@ Lower the ratio -> higher the precision. - - - - CudaSift: Threshold on difference of Gaussians for feature pruning. The higher the threshold, the less features are produced by the detector. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - - - - - - - - Sigma. The sigma of the Gaussian applied to the input image at the octave #0. If your image is captured with a weak camera with soft lenses, you might want to reduce the number. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - + - CudaSift: Whether to enable upscaling. + CudaSift: Maximum threshold on difference of Gaussians for feature pruning (ignored if smaller or equal than gaussian threshold above). The lower the threshold, the less features with high response/hessian are produced by the detector. + + + true Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse @@ -26924,9 +26944,21 @@ Lower the ratio -> higher the precision. - - - + + + 2 + + + 0.010000000000000 + + + 100.000000000000000 + + + 0.100000000000000 + + + 4.500000000000000 diff --git a/tools/Reprocess/main.cpp b/tools/Reprocess/main.cpp index 4006a036..30c27402 100644 --- a/tools/Reprocess/main.cpp +++ b/tools/Reprocess/main.cpp @@ -128,6 +128,8 @@ void sighandler(int sig) int loopCount = 0; int proxCount = 0; int loopCountMotion = 0; +int loopInter = 0; +int loopIntra = 0; int totalFrames = 0; int totalFramesMotion = 0; std::vector previousLocalizationDistances; @@ -1239,15 +1241,23 @@ int main(int argc, char * argv[]) ++loopCountMotion; } int loopMapId = stats.loopClosureId() > 0? stats.loopClosureMapId(): stats.proximityDetectionMapId(); - printf("Processed %d/%d nodes [id=%d map=%d opt_graph=%d]... %dms %s on %d [%d]\n", ++processed, totalIds, refId, refMapId, int(stats.poses().size()), int(iterationTime.ticks() * 1000), stats.loopClosureId() > 0?"Loop":"Prox", loopId, loopMapId); + if(loopMapId != stats.refImageMapId()) + { + ++loopInter; + } + else + { + ++loopIntra; + } + printf("Processed %d/%d nodes [id=%d map=%d graph=%d hyp=%d]... %dms %s on %d [%d]\n", ++processed, totalIds, refId, refMapId, int(stats.poses().size()), int(uValue(stats.data(), Statistics::kLoopHighest_hypothesis_value())*100.0f), int(iterationTime.ticks() * 1000), stats.loopClosureId() > 0?"Loop":"Prox", loopId, loopMapId); } else if(landmarkId != 0) { - printf("Processed %d/%d nodes [id=%d map=%d opt_graph=%d]... %dms Loop on landmark %d\n", ++processed, totalIds, refId, refMapId, int(stats.poses().size()), int(iterationTime.ticks() * 1000), landmarkId); + printf("Processed %d/%d nodes [id=%d map=%d graph=%d hyp=%d]... %dms Loop on landmark %d\n", ++processed, totalIds, refId, refMapId, int(stats.poses().size()), int(uValue(stats.data(), Statistics::kLoopHighest_hypothesis_value())*100.0f), int(iterationTime.ticks() * 1000), landmarkId); } else { - printf("Processed %d/%d nodes [id=%d map=%d opt_graph=%d]... %dms\n", ++processed, totalIds, refId, refMapId, int(stats.poses().size()), int(iterationTime.ticks() * 1000)); + printf("Processed %d/%d nodes [id=%d map=%d graph=%d hyp=%d]... %dms\n", ++processed, totalIds, refId, refMapId, int(stats.poses().size()), int(uValue(stats.data(), Statistics::kLoopHighest_hypothesis_value())*100.0f), int(iterationTime.ticks() * 1000)); } // Here we accumulate statistics about distance from last localization @@ -1340,7 +1350,8 @@ int main(int argc, char * argv[]) } else { - printf("Total loop closures = %d (Loop=%d, Prox=%d, In Motion=%d/%d)\n", loopCount+proxCount, loopCount, proxCount, loopCountMotion, totalFramesMotion); + printf("Total loop closures = %d (Loop=%d, Prox=%d, In Motion=%d/%d, Intra=%d, Inter=%d)\n", + loopCount+proxCount, loopCount, proxCount, loopCountMotion, totalFramesMotion, loopIntra, loopInter); if(databases.size()>1) { From 26bab2ba15201a7b03fb4dedad66ac5baf8bfdc6 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 29 Mar 2026 11:30:33 -0700 Subject: [PATCH 092/126] OptimizerG2O: adding support for priors in SBA (#1670) * OptimizerG2O: adding support for priors in SBA * Fixed computeError * don't fix root id roll/pitch if gravity constraints are fed (SBA) * changed number_t to double * forward compatibility * cleanup * log --- corelib/src/optimizer/OptimizerG2O.cpp | 104 +++++++++++++-- .../src/optimizer/g2o/edge_sbacam_gravity.h | 1 + corelib/src/optimizer/g2o/edge_sbacam_prior.h | 121 ++++++++++++++++++ 3 files changed, 218 insertions(+), 8 deletions(-) create mode 100644 corelib/src/optimizer/g2o/edge_sbacam_prior.h diff --git a/corelib/src/optimizer/OptimizerG2O.cpp b/corelib/src/optimizer/OptimizerG2O.cpp index fa410232..a214f2b0 100644 --- a/corelib/src/optimizer/OptimizerG2O.cpp +++ b/corelib/src/optimizer/OptimizerG2O.cpp @@ -62,6 +62,7 @@ typedef Eigen::Matrix Matr #include "g2o/edge_se3_xyzprior.h" // Include after types_slam3d.h to be ignored on newest g2o versions #include "g2o/edge_se3_gravity.h" #include "g2o/edge_sbacam_gravity.h" +#include "g2o/edge_sbacam_prior.h" #include "g2o/edge_xy_prior.h" // Include after types_slam2d.h to be ignored on newest g2o versions #include "g2o/edge_xyz_prior.h" // Include after types_slam3d.h to be ignored on newest g2o versions #ifdef G2O_HAVE_CSPARSE @@ -1579,8 +1580,25 @@ std::map OptimizerG2O::optimizeBA( #endif } + // detect if there are gravity constraints + bool hasGravityConstraints = false; +#ifndef RTABMAP_ORB_SLAM + if(!isSlam2d() && gravitySigma() > 0) + { + for(std::multimap::const_iterator iter=links.begin(); iter!=links.end(); ++iter) + { + if( iter->second.from() == iter->second.to() && + iter->second.type() == Link::kGravity) + { + hasGravityConstraints = true; + break; + } + } + } +#endif - UDEBUG("fill poses to g2o..."); + + UDEBUG("fill %ld poses to g2o... (rootId=%d hasGravityConstraints=%d isSlam2d=%d)", poses.size(), rootId, hasGravityConstraints?1:0, isSlam2d()?1:0); for(std::map::const_iterator iter=poses.begin(); iter!=poses.end(); ++iter) { if(iter->first > 0) @@ -1619,7 +1637,65 @@ std::map OptimizerG2O::optimizeBA( vCam->setId(iter->first*MULTICAM_OFFSET + i); // negative root means that all other poses should be fixed instead of the root - vCam->setFixed((rootId >= 0 && iter->first == rootId) || (rootId < 0 && iter->first != -rootId)); + bool fixNode = (rootId >= 0 && iter->first == rootId) || (rootId < 0 && iter->first != -rootId); + + UASSERT_MSG(optimizer.addVertex(vCam), uFormat("cannot insert cam vertex %d (pose=%d)!?", vCam->id(), iter->first).c_str()); + + if(this->isSlam2d()) + { + if(fixNode) + { + UDEBUG("Set node %d fixed", iter->first); + vCam->setFixed(true); + } + else if(i==0) // Only set prior on the first camera + { + // add a singleton constraint that locks the position of the robot on the plane + EdgeSBACamPrior* planeConstraint = new EdgeSBACamPrior(); + Eigen::Matrix pinfo = Eigen::Matrix::Zero(); + pinfo(2, 2) = 1e9; + planeConstraint->setInformation(pinfo); + g2o::SE3Quat fixedZ = g2o::SE3Quat(); + fixedZ.setTranslation(g2o::Vector3(0,0,iter->second.z())); + planeConstraint->setMeasurement(fixedZ); + Eigen::Affine3d a = iterModel->second[i].localTransform().inverse().toEigen3d(); + planeConstraint->setCameraInvLocalTransform(g2o::SE3Quat(a.linear(), a.translation())); + planeConstraint->vertices()[0] = vCam; + optimizer.addEdge(planeConstraint); + } + } + else if(fixNode) + { + if(rootId < 0 || !hasGravityConstraints) + { + UDEBUG("Set node %d fixed", iter->first); + vCam->setFixed(true); + } + else if(hasGravityConstraints && i==0) // Only set prior on the first camera in case of multi-cam + { + // Setup root prior (fixed x,y,z,yaw) + EdgeSBACamPrior * e = new EdgeSBACamPrior(); + e->vertices()[0] = vCam; + Eigen::Affine3d a = iter->second.toEigen3d(); + e->setMeasurement(g2o::SE3Quat(a.linear(), a.translation())); + a = iterModel->second[i].localTransform().inverse().toEigen3d(); + e->setCameraInvLocalTransform(g2o::SE3Quat(a.linear(), a.translation())); + Eigen::Matrix information = Eigen::Matrix::Identity()*10e6; + // pitch and roll not fixed + information(3,3) = information(4,4) = 1; + e->setInformation(information); + if (!optimizer.addEdge(e)) + { + delete e; + UERROR("Map: Failed adding fixed constraint of node %d, set as fixed instead", iter->first); + vCam->setFixed(true); + } + else + { + UDEBUG("Set node %d fixed with prior (have gravity constraints)", iter->first); + } + } + } /*UDEBUG("camPose %d (camid=%d) (fixed=%d) fx=%f fy=%f cx=%f cy=%f Tx=%f baseline=%f t=%s", iter->first, @@ -1632,8 +1708,6 @@ std::map OptimizerG2O::optimizeBA( iterModel->second[i].Tx(), iterModel->second[i].Tx()<0.0?-iterModel->second[i].Tx()/iterModel->second[i].fx():baseline_, camPose.prettyPrint().c_str());*/ - - UASSERT_MSG(optimizer.addVertex(vCam), uFormat("cannot insert cam vertex %d (pose=%d)!?", vCam->id(), iter->first).c_str()); } } } @@ -2043,12 +2117,26 @@ std::map OptimizerG2O::optimizeBA( return optimizedPoses; } - // FIXME: is there a way that we can add the 2D constraint directly in SBA? if(this->isSlam2d()) { - // get transform between old and new pose - t = iter->second.inverse() * t; - optimizedPoses.insert(std::pair(iter->first, iter->second * t.to3DoF())); + // The optimized poses should be already fixed to original height, + // but it may have varied a little (not exaclty the same number). + // Here we just put back the original z value. + if(fabs(t.z() - iter->second.z()) < 0.001) + { + t.z() = iter->second.z(); + optimizedPoses.insert(std::pair(iter->first, t)); + } + else + { + UWARN("Planar constraints didn't work!? original pose (%d), pose %s -> %s. Falling back to old approach.", + iter->first, + iter->second.prettyPrint().c_str(), + t.prettyPrint().c_str()); + // get transform between old and new pose + t = iter->second.inverse() * t; + optimizedPoses.insert(std::pair(iter->first, iter->second * t.to3DoF())); + } } else { diff --git a/corelib/src/optimizer/g2o/edge_sbacam_gravity.h b/corelib/src/optimizer/g2o/edge_sbacam_gravity.h index d2fe24cd..2559df2b 100644 --- a/corelib/src/optimizer/g2o/edge_sbacam_gravity.h +++ b/corelib/src/optimizer/g2o/edge_sbacam_gravity.h @@ -44,6 +44,7 @@ class EdgeSBACamGravity : public g2o::BaseUnaryEdge<3, Eigen::Matrix { + public: + EIGEN_MAKE_ALIGNED_OPERATOR_NEW + EdgeSBACamPrior() { + setMeasurement(g2o::SE3Quat()); + information().setIdentity(); + } + + void setCameraInvLocalTransform(const g2o::SE3Quat & t) + { + _cameraInvLocalTransform = t; + } + + // return the error estimate as a 3-vector + void computeError() { + const g2o::VertexCam* v = static_cast(_vertices[0]); + g2o::SE3Quat delta = _inverseMeasurement * v->estimate() * _cameraInvLocalTransform; + _error[0]=delta.translation().x(); + _error[1]=delta.translation().y(); + _error[2]=delta.translation().z(); + _error[3]=delta.rotation().x(); + _error[4]=delta.rotation().y(); + _error[5]=delta.rotation().z(); + } + + // jacobian + virtual void linearizeOplus() { + _jacobianOplusXi = Eigen::Matrix::Identity(); + } + + virtual void setMeasurement(const g2o::SE3Quat& m){ + _measurement = m; + _inverseMeasurement = m.inverse(); + } + + virtual bool setMeasurementData(const double* d) override { + Eigen::Map > v(d); + // SE3Quat expects [x, y, z, qx, qy, qz, qw] + _measurement.fromVector(v); + _inverseMeasurement = _measurement.inverse(); + return true; + } + + virtual bool getMeasurementData(double* d) const override { + Eigen::Map > v(d); + // Returns [x, y, z, qx, qy, qz, qw] + v = _measurement.toVector(); + return true; + } + + virtual int measurementDimension() const {return 7;} + + virtual double initialEstimatePossible(const g2o::OptimizableGraph::VertexSet& /*from*/, + g2o::OptimizableGraph::Vertex* /*to*/) { + return 1.; + } + + virtual void initialEstimate(const g2o::OptimizableGraph::VertexSet& from, g2o::OptimizableGraph::Vertex* to) { + g2o::VertexCam *v = static_cast(_vertices[0]); + assert(v && "Vertex for the Prior edge is not set"); + + g2o::SE3Quat newEstimate = measurement()*_cameraInvLocalTransform.inverse(); + if (_information.block<3,3>(0,0).array().abs().sum() == 0){ // do not set translation, as that part of the information is all zero + newEstimate.setTranslation(v->estimate().translation()); + } + if (_information.block<3,3>(3,3).array().abs().sum() == 0){ // do not set rotation, as that part of the information is all zero + newEstimate.setRotation(v->estimate().rotation()); + } + v->setEstimate(newEstimate); + } + + virtual bool read(std::istream& is) override { return true; } + virtual bool write(std::ostream& os) const override { return true; } + protected: + g2o::SE3Quat _inverseMeasurement; + g2o::SE3Quat _cameraInvLocalTransform; +}; + +} +#endif From 51cfc379231180b66ba1a7f459bde9c7ebea1cc9 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 29 Mar 2026 12:34:09 -0700 Subject: [PATCH 093/126] Update CI badges (#1681) * OptimizerG2O: adding support for priors in SBA * Fixed computeError * don't fix root id roll/pitch if gravity constraints are fed (SBA) * changed number_t to double * forward compatibility * cleanup * log * Updated CI status * split cmake workflow in linux and windows * bump release badge version * uniformize name * fixing android build --- .github/workflows/cmake-linux.yml | 66 +++++++++++++++++++ .../{cmake.yml => cmake-windows.yml} | 33 +--------- README.md | 16 ++--- corelib/src/optimizer/OptimizerG2O.cpp | 2 +- 4 files changed, 77 insertions(+), 40 deletions(-) create mode 100644 .github/workflows/cmake-linux.yml rename .github/workflows/{cmake.yml => cmake-windows.yml} (72%) diff --git a/.github/workflows/cmake-linux.yml b/.github/workflows/cmake-linux.yml new file mode 100644 index 00000000..b26409af --- /dev/null +++ b/.github/workflows/cmake-linux.yml @@ -0,0 +1,66 @@ +name: CMake-Linux + +on: + push: + branches: + - master + pull_request: + branches: + - '**' + +env: + BUILD_TYPE: Release + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + build: + name: ${{ matrix.build_name }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + build_name: [ubuntu-22.04, ubuntu-24.04, ubuntu-24.04-with-opengv] + include: + - build_name: ubuntu-22.04 + os: ubuntu-22.04 + extra_deps: "libunwind-dev libceres-dev" + extra_cmake_def: "-DWITH_CERES=ON" + - build_name: ubuntu-24.04 + os: ubuntu-24.04 + extra_deps: "libg2o-dev libceres-dev" + extra_cmake_def: "-DWITH_CERES=ON" + - build_name: ubuntu-24.04-with-opengv + os: ubuntu-24.04 + extra_deps: "libg2o-dev libceres-dev" + extra_cmake_def: "-DWITH_CERES=ON -DBUILD_OPENGV=ON" + + steps: + - uses: actions/checkout@v4 + + - name: Install Linux Dependencies + run: | + DEBIAN_FRONTEND=noninteractive + sudo apt-get update + sudo apt-get -y install libopencv-dev libpcl-dev git cmake software-properties-common libyaml-cpp-dev ${{ matrix.extra_deps }} + + - name: Configure CMake + run: | + cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} ${{ matrix.extra_cmake_def }} + + - name: Build + run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} + + - name: Info + working-directory: ${{github.workspace}}/build/bin + run: | + ./rtabmap-console --version + +# - name: Test +# working-directory: ${{github.workspace}}/build +# # Execute tests defined by the CMake configuration. +# # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail +# run: ctest -C ${{env.BUILD_TYPE}} + diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake-windows.yml similarity index 72% rename from .github/workflows/cmake.yml rename to .github/workflows/cmake-windows.yml index a61d5215..9af50aa3 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake-windows.yml @@ -1,4 +1,4 @@ -name: CMake +name: CMake-Windows on: push: @@ -22,20 +22,8 @@ jobs: strategy: fail-fast: true matrix: - build_name: [ubuntu-22.04, ubuntu-24.04, ubuntu-24.04-with-opengv, windows-2022, windows-2022-cuda] + build_name: [windows-2022, windows-2022-cuda] include: - - build_name: ubuntu-22.04 - os: ubuntu-22.04 - extra_deps: "libunwind-dev libceres-dev" - extra_cmake_def: "-DWITH_CERES=ON" - - build_name: ubuntu-24.04 - os: ubuntu-24.04 - extra_deps: "libg2o-dev libceres-dev" - extra_cmake_def: "-DWITH_CERES=ON" - - build_name: ubuntu-24.04-with-opengv - os: ubuntu-24.04 - extra_deps: "libg2o-dev libceres-dev" - extra_cmake_def: "-DWITH_CERES=ON -DBUILD_OPENGV=ON" - build_name: windows-2022 os: windows-2022 extra_deps: "" @@ -56,15 +44,7 @@ jobs: if: matrix.build_name == 'windows-2022-cuda' uses: ./.github/actions/install-windows-cuda-deps - - name: Install Linux Dependencies - if: matrix.os != 'windows-2022' - run: | - DEBIAN_FRONTEND=noninteractive - sudo apt-get update - sudo apt-get -y install libopencv-dev libpcl-dev git cmake software-properties-common libyaml-cpp-dev ${{ matrix.extra_deps }} - - name: Configure CMake - if: matrix.os == 'windows-2022' run: | cmake ` -B ${{github.workspace}}/build ` @@ -76,16 +56,10 @@ jobs: -DCMAKE_TOOLCHAIN_FILE=${{env.VCPKG_EXPORT_PATH}}/scripts/buildsystems/vcpkg.cmake ` -DTorch_DIR=${{env.VCPKG_EXPORT_PATH}}/installed/x64-windows-release/tools/python3/Lib/site-packages/torch/share/cmake/Torch - - name: Configure CMake - if: matrix.os != 'windows-2022' - run: | - cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} ${{ matrix.extra_cmake_def }} - - name: Build run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} - name: Build Windows Package - if: matrix.os == 'windows-2022' shell: pwsh run: | if ("${{ github.event_name }}" -eq "pull_request") { @@ -105,7 +79,6 @@ jobs: ./rtabmap-console --version - name: Upload RTABMap Artifacts (ZIP) - if: matrix.os == 'windows-2022' uses: actions/upload-artifact@v4 with: name: RTABMap-Binaries-${{ matrix.build_name }}-zip @@ -116,7 +89,7 @@ jobs: retention-days: ${{ github.event_name == 'pull_request' && 1 || 90 }} - name: Upload RTABMap Artifacts (Installer) - if: matrix.os == 'windows-2022' && github.event_name != 'pull_request' + if: github.event_name != 'pull_request' uses: actions/upload-artifact@v4 with: name: RTABMap-Binaries-${{ matrix.build_name }}-exe diff --git a/README.md b/README.md index a0978e79..b13adcf7 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ rtabmap [![Downloads][downloads-image]][downloads] [![License][license-image]][license] -[release-image]: https://img.shields.io/badge/release-0.21.4-green.svg?style=flat +[release-image]: https://img.shields.io/badge/release-0.23.1-green.svg?style=flat [releases]: https://github.com/introlab/rtabmap/releases [downloads-image]: https://img.shields.io/github/downloads/introlab/rtabmap/total?label=downloads @@ -35,13 +35,7 @@ This project is supported by [IntRoLab - Intelligent / Interactive / Integrated - - - - - - @@ -59,7 +53,7 @@ This project is supported by [IntRoLab - Intelligent / Interactive / Integrated - + @@ -67,6 +61,10 @@ This project is supported by [IntRoLab - Intelligent / Interactive / Integrated + + + + diff --git a/corelib/src/optimizer/OptimizerG2O.cpp b/corelib/src/optimizer/OptimizerG2O.cpp index a214f2b0..a97dac9b 100644 --- a/corelib/src/optimizer/OptimizerG2O.cpp +++ b/corelib/src/optimizer/OptimizerG2O.cpp @@ -1656,7 +1656,7 @@ std::map OptimizerG2O::optimizeBA( pinfo(2, 2) = 1e9; planeConstraint->setInformation(pinfo); g2o::SE3Quat fixedZ = g2o::SE3Quat(); - fixedZ.setTranslation(g2o::Vector3(0,0,iter->second.z())); + fixedZ.setTranslation(Eigen::Vector3d(0,0,iter->second.z())); planeConstraint->setMeasurement(fixedZ); Eigen::Affine3d a = iterModel->second[i].localTransform().inverse().toEigen3d(); planeConstraint->setCameraInvLocalTransform(g2o::SE3Quat(a.linear(), a.translation())); From 1ea8fa2e06e52bdd6e3638ad2c4bda0801a7b36d Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sat, 4 Apr 2026 19:48:03 -0700 Subject: [PATCH 094/126] New rtabmap-reduceGraph CLI tool (#1655) * New rtabmap-reduceGraph CLI tool * fixed some edge cases * Regenerating optimized map if there was one before reducing the graph * addMoreLoopClosures: refactored how ctrl-c is handled to stop faster when no loop closures are added * Added kilted status * Make offline tool always propagate neighbor merged links * removed a parameter * fixed disconnected graph * fixed --help * Added error log on Kp/NNStrategy not compatible with huge vocabulary. ReduceGraph/DetectMoreLoopClosures: Make sure original parameters are saved back on closing. g2o: fixing optimizer to Levenberg for SBA to avoid [SetJac] infinite jac fatal error. * exposing neighbor merged ratio parameter to the tool * show param in log * refactored detectMoreLoopClosures to ignore too close nodes in terms of neighbor links based on Mem/STMSize parameter. Reduce graph: added direction parameter. * Simplified: removed ratio parameter, removed recursive reduction. Just don't reduce if a NM link is longer than maxDistance. * Removed NNStrategy override, as it was still done on closing when we changed back to original params * DBViewer: show missing links when showing OptimizedPoses in GraphView, fixed clicking on landmark links * DetectMoreLoopClosures: Added support for min graph distance option in MainWindow and DbViewer * slight renaming of ROS jobs * reprocess: added option --params_last --- .github/workflows/cmake-ros.yml | 2 +- corelib/include/rtabmap/core/Graph.h | 7 +- corelib/include/rtabmap/core/Memory.h | 2 + corelib/src/DBDriverSqlite3.cpp | 15 +- corelib/src/FlannIndex.cpp | 2 +- corelib/src/Graph.cpp | 35 +-- corelib/src/Memory.cpp | 233 +++++++++------ corelib/src/Optimizer.cpp | 6 +- corelib/src/Rtabmap.cpp | 30 +- corelib/src/VWDictionary.cpp | 11 +- corelib/src/optimizer/OptimizerG2O.cpp | 20 +- guilib/include/rtabmap/gui/MainWindow.h | 1 + .../rtabmap/gui/PostProcessingDialog.h | 2 + guilib/src/DatabaseViewer.cpp | 68 ++++- guilib/src/ExportCloudsDialog.cpp | 4 +- guilib/src/MainWindow.cpp | 31 ++ guilib/src/PostProcessingDialog.cpp | 13 + guilib/src/ui/DatabaseViewer.ui | 248 +++++++++------- guilib/src/ui/postProcessingDialog.ui | 62 ++-- tools/CMakeLists.txt | 1 + tools/DetectMoreLoopClosures/main.cpp | 25 +- tools/ReduceGraph/CMakeLists.txt | 13 + tools/ReduceGraph/main.cpp | 281 ++++++++++++++++++ tools/Reprocess/main.cpp | 47 ++- 24 files changed, 865 insertions(+), 294 deletions(-) create mode 100644 tools/ReduceGraph/CMakeLists.txt create mode 100644 tools/ReduceGraph/main.cpp diff --git a/.github/workflows/cmake-ros.yml b/.github/workflows/cmake-ros.yml index 064c5339..9274cc06 100644 --- a/.github/workflows/cmake-ros.yml +++ b/.github/workflows/cmake-ros.yml @@ -22,7 +22,7 @@ jobs: # well on Windows or Mac. You can convert this to a matrix build if you need # cross-platform coverage. # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix - name: Build on ros ${{ matrix.ros_distribution }} and ${{ matrix.os }} + name: ${{ matrix.ros_distribution }}-${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false diff --git a/corelib/include/rtabmap/core/Graph.h b/corelib/include/rtabmap/core/Graph.h index 010b74b8..ffd828ee 100644 --- a/corelib/include/rtabmap/core/Graph.h +++ b/corelib/include/rtabmap/core/Graph.h @@ -277,7 +277,8 @@ std::list > RTABMAP_CORE_EXPORT computePath( bool lookInDatabase = true, bool updateNewCosts = false, float linearVelocity = 0.0f, // m/sec - float angularVelocity = 0.0f); // rad/sec + float angularVelocity = 0.0f, // rad/sec + bool ignoreDirectLinks = false); /** * Find the nearest node of the target pose @@ -336,9 +337,7 @@ RTABMAP_DEPRECATED std::map RTABMAP_CORE_EXPORT getPosesInRadius RTABMAP_DEPRECATED std::map RTABMAP_CORE_EXPORT getPosesInRadius(const Transform & targetPose, const std::map & nodes, float radius, float angle = 0.0f); float RTABMAP_CORE_EXPORT computePathLength( - const std::vector > & path, - unsigned int fromIndex = 0, - unsigned int toIndex = 0); + const std::vector > & path); // assuming they are all linked in map order float RTABMAP_CORE_EXPORT computePathLength( diff --git a/corelib/include/rtabmap/core/Memory.h b/corelib/include/rtabmap/core/Memory.h index 5de7c741..40b30ad4 100644 --- a/corelib/include/rtabmap/core/Memory.h +++ b/corelib/include/rtabmap/core/Memory.h @@ -144,6 +144,7 @@ public: void saveLocationData(int locationId); void removeLink(int idA, int idB); void removeRawData(int id, bool image = true, bool scan = true, bool userData = true); + int reduceNode(int id, float maxDistance = 0.0f, bool keepLinkedInDb = false, int direction = 0); //getters const std::map & getWorkingMem() const {return _workingMem;} @@ -277,6 +278,7 @@ private: void initCountId(); void rehearsal(Signature * signature, Statistics * stats = 0); bool rehearsalMerge(int oldId, int newId); + bool canBeReduced(const Link & link, float maxDistance, int direction); const std::map & getSignatures() const {return _signatures;} diff --git a/corelib/src/DBDriverSqlite3.cpp b/corelib/src/DBDriverSqlite3.cpp index d2118f9b..66986051 100644 --- a/corelib/src/DBDriverSqlite3.cpp +++ b/corelib/src/DBDriverSqlite3.cpp @@ -4419,14 +4419,7 @@ void DBDriverSqlite3::updateQuery(const std::list & nodes, bool upd ULOGGER_DEBUG("Update Node table, Time=%fs", timer.ticks()); // Update links part1 - if(uStrNumCmp(_version, "0.18.3") >= 0) - { - query = uFormat("DELETE FROM Link WHERE from_id=? and type!=%d;", (int)Link::kLandmark); - } - else - { - query = uFormat("DELETE FROM Link WHERE from_id=?;"); - } + query = uFormat("DELETE FROM Link WHERE from_id=?;"); rc = sqlite3_prepare_v2(_ppDb, query.c_str(), -1, &ppStmt, 0); UASSERT_MSG(rc == SQLITE_OK, uFormat("DB error (%s): %s", _version.c_str(), sqlite3_errmsg(_ppDb)).c_str()); for(std::list::const_iterator j=nodes.begin(); j!=nodes.end(); ++j) @@ -4461,6 +4454,12 @@ void DBDriverSqlite3::updateQuery(const std::list & nodes, bool upd { stepLink(ppStmt, i->second); } + // Save landmarks + const std::map & landmarks = (*j)->getLandmarks(); + for(std::map::const_iterator i=landmarks.begin(); i!=landmarks.end(); ++i) + { + stepLink(ppStmt, i->second); + } } } // Finalize (delete) the statement diff --git a/corelib/src/FlannIndex.cpp b/corelib/src/FlannIndex.cpp index cf2cf406..b6b09a4c 100644 --- a/corelib/src/FlannIndex.cpp +++ b/corelib/src/FlannIndex.cpp @@ -96,7 +96,7 @@ std::vector FlannIndex::serializeIndex(bool computeChecksum) cons #else UTimer timer; const int headerSizeBytes = sizeof(int)*FLANN_INDEX_HEADER_SIZE; - std::vector indexData(1024*1024*100 + headerSizeBytes); // Max 100 MB + std::vector indexData(1024*1024*1024 + headerSizeBytes); // Max 1 GB FILE* indexDataPtr = fmemopen(indexData.data()+headerSizeBytes, indexData.size() - headerSizeBytes, "wb"); long bytes_written = 0; if (indexDataPtr) { diff --git a/corelib/src/Graph.cpp b/corelib/src/Graph.cpp index 5c2b5c10..a887fddb 100644 --- a/corelib/src/Graph.cpp +++ b/corelib/src/Graph.cpp @@ -2020,19 +2020,21 @@ std::list > computePath( bool lookInDatabase, bool updateNewCosts, float linearVelocity, // m/sec - float angularVelocity) // rad/sec + float angularVelocity, // rad/sec + bool ignoreDirectLinks) { UASSERT(memory!=0); UASSERT(fromId>=0); UASSERT(toId!=0); std::list > path; - UDEBUG("fromId=%d, toId=%d, lookInDatabase=%d, updateNewCosts=%d, linearVelocity=%f, angularVelocity=%f", + UDEBUG("fromId=%d, toId=%d, lookInDatabase=%d, updateNewCosts=%d, linearVelocity=%f, angularVelocity=%f ignoreDirectLinks=%d", fromId, toId, lookInDatabase?1:0, updateNewCosts?1:0, linearVelocity, - angularVelocity); + angularVelocity, + ignoreDirectLinks?1:0); std::multimap allLinks; if(lookInDatabase) @@ -2110,7 +2112,9 @@ std::list > computePath( } for(std::multimap::const_iterator iter = links.begin(); iter!=links.end(); ++iter) { - if(iter->second.from() != iter->second.to()) + if(iter->second.from() != iter->second.to() && + (!ignoreDirectLinks || + (!(iter->second.from()==fromId && iter->second.to()==toId) && !(iter->second.to()==fromId && iter->second.from()==toId)))) { Transform nextPose = currentNode->pose()*iter->second.transform(); float cost = 0.0f; @@ -2396,26 +2400,15 @@ std::map getPosesInRadius(const Transform & targetPose, const st float computePathLength( - const std::vector > & path, - unsigned int fromIndex, - unsigned int toIndex) + const std::vector > & path) { float length = 0.0f; if(path.size() > 1) { - UASSERT(fromIndex < path.size() && toIndex < path.size() && fromIndex <= toIndex); - if(fromIndex >= toIndex) + for(unsigned int i=0; i 1) { - float x=0, y=0, z=0; std::map::const_iterator iter=path.begin(); Transform previousPose = iter->second; ++iter; for(; iter!=path.end(); ++iter) { const Transform & currentPose = iter->second; - x += fabs(previousPose.x() - currentPose.x()); - y += fabs(previousPose.y() - currentPose.y()); - z += fabs(previousPose.z() - currentPose.z()); + length+=previousPose.getDistance(currentPose); previousPose = currentPose; } - length = sqrt(x*x + y*y + z*z); } return length; } diff --git a/corelib/src/Memory.cpp b/corelib/src/Memory.cpp index 0bee2e91..eebdcc6a 100644 --- a/corelib/src/Memory.cpp +++ b/corelib/src/Memory.cpp @@ -1288,114 +1288,180 @@ void Memory::addSignatureToWmFromLTM(Signature * signature) } } -void Memory::moveSignatureToWMFromSTM(int id, int * reducedTo) +bool Memory::canBeReduced(const Link & link, float maxDistance, int direction) { - UDEBUG("Inserting node %d from STM in WM...", id); - UASSERT(_stMem.find(id) != _stMem.end()); + return link.to() != link.from() && + link.type() != Link::kNeighbor && + link.type() != Link::kNeighborMerged && + link.userDataCompressed().empty() && + link.type() != Link::kUndef && + link.type() != Link::kVirtualClosure && + (maxDistance == 0.0f || link.transform().getNorm() < maxDistance) && + (direction == 0 || (direction==-1 && link.to() < link.from()) || (direction==1 && link.to() > link.from())); +} + +int Memory::reduceNode(int id, float maxDistance, bool keepLinkedInDb, int direction) +{ + UDEBUG("Reducing %d (max distance=%f, keep linked in db=%s, direction=%d)", + id, maxDistance, keepLinkedInDb?"true":"false", direction); Signature * s = this->_getSignature(id); - UASSERT(s!=0); - - if(_reduceGraph) + if(s==0) { - bool merge = false; - const std::multimap & links = s->getLinks(); - std::map neighbors; - for(std::multimap::const_iterator iter=links.begin(); iter!=links.end(); ++iter) - { - if(!merge) - { - merge = iter->second.to() < s->id() && // should be a parent->child link - iter->second.to() != iter->second.from() && - iter->second.type() != Link::kNeighbor && - iter->second.type() != Link::kNeighborMerged && - iter->second.userDataCompressed().empty() && - iter->second.type() != Link::kUndef && - iter->second.type() != Link::kVirtualClosure; - if(merge) - { - UDEBUG("Reduce %d to %d", s->id(), iter->second.to()); - if(reducedTo) - { - *reducedTo = iter->second.to(); - } - } + UWARN("Node %d is not in WM/STM, cannot reduce it.", id); + return 0; + } - } - if(iter->second.type() == Link::kNeighbor) + if(!s->getLabel().empty()) + { + // We currently not remove nodes with labels + return 0; + } + + std::multimap links = s->getLinks(); + std::map neighbors; + int reducedTo = 0; + for(std::multimap::const_iterator iter=links.begin(); iter!=links.end(); ++iter) + { + if(canBeReduced(iter->second, maxDistance, direction)) + { + float distance = iter->second.transform().getNorm(); + reducedTo = iter->second.to(); + UDEBUG("Reduce %d to %d (distance=%f)", + s->id(), iter->second.to(), distance); + } + + if(iter->second.type() == Link::kNeighbor) + { + neighbors.insert(*iter); + } + } + if(reducedTo>0) + { + if(maxDistance > 0.0f) + { + // Only reduce if all neighbor merged links are also below maxDistance + for(std::multimap::const_iterator iter=links.begin(); iter!=links.end(); ++iter) { - neighbors.insert(*iter); + if( iter->second.type() == Link::kNeighborMerged && + iter->second.transform().getNorm() > maxDistance) + { + return 0; + } } } - if(merge) + + for(std::multimap::const_iterator iter=links.begin(); iter!=links.end(); ++iter) { - if(s->getLabel().empty()) + Signature * sTo = this->_getSignature(iter->first); + if(sTo->id()!=s->id()) // Not Prior/Gravity links... { - for(std::multimap::const_iterator iter=links.begin(); iter!=links.end(); ++iter) + UASSERT_MSG(sTo!=0, uFormat("id=%d", iter->first).c_str()); + sTo->removeLink(s->id()); + if(iter->second.type() != Link::kNeighbor && + iter->second.type() != Link::kUndef) { - Signature * sTo = this->_getSignature(iter->first); - if(sTo->id()!=s->id()) // Not Prior/Gravity links... + if(iter->second.type() == Link::kNeighborMerged) { - UASSERT_MSG(sTo!=0, uFormat("id=%d", iter->first).c_str()); - sTo->removeLink(s->id()); - if(iter->second.type() != Link::kNeighbor && - iter->second.type() != Link::kNeighborMerged && - iter->second.type() != Link::kUndef) + s->removeLink(sTo->id()); + if(maxDistance == 0.0f) { - // link to all neighbors - for(std::map::iterator jter=neighbors.begin(); jter!=neighbors.end(); ++jter) + // online graph reduction, always skip these links + continue; + } + } + // link to all neighbors + for(std::map::iterator jter=neighbors.begin(); jter!=neighbors.end(); ++jter) + { + if(!sTo->hasLink(jter->second.to())) + { + Link l = iter->second.inverse().merge( + jter->second, + iter->second.userDataCompressed().empty() && iter->second.type() != Link::kVirtualClosure?Link::kNeighborMerged:iter->second.type()); + UDEBUG("Merging link %d->%d (type=%d) to with %d->%d (type %d). Adding %d->%d (type %d) to %d and %d", + iter->second.to(), iter->second.from(), iter->second.type(), + jter->second.from(), jter->second.to(), jter->second.type(), + l.from(), l.to(), l.type(), sTo->id(), l.to()); + sTo->addLink(l); + Signature * sB = this->_getSignature(l.to()); + UASSERT(sB!=0); + UASSERT_MSG(!sB->hasLink(l.from()), uFormat("%d->%d type=%d", sB->id(), l.to(), l.type()).c_str()); + sB->addLink(l.inverse()); + } + } + // link to all landmarks + for(std::map::const_iterator jter=s->getLandmarks().begin(); jter!=s->getLandmarks().end(); ++jter) + { + if(!uContains(sTo->getLandmarks(), jter->first)) + { + UDEBUG("Move landmark observation %d from %d to %d", + jter->first, s->id(), sTo->id()); + Link l = iter->second.inverse().merge( + jter->second, + jter->second.type()); + sTo->addLandmark(l); + // Update landmark index + std::map >::iterator nter = _landmarksIndex.find(jter->first); + if(nter!=_landmarksIndex.end()) { - if(!sTo->hasLink(jter->second.to())) - { - UDEBUG("Merging link %d->%d (type=%d) to link %d->%d (type %d)", - iter->second.from(), iter->second.to(), iter->second.type(), - jter->second.from(), jter->second.to(), jter->second.type()); - Link l = iter->second.inverse().merge( - jter->second, - iter->second.userDataCompressed().empty() && iter->second.type() != Link::kVirtualClosure?Link::kNeighborMerged:iter->second.type()); - sTo->addLink(l); - Signature * sB = this->_getSignature(l.to()); - UASSERT(sB!=0); - UASSERT_MSG(!sB->hasLink(l.from()), uFormat("%d->%d", sB->id(), l.to()).c_str()); - sB->addLink(l.inverse()); - } + nter->second.insert(sTo->id()); + } + else + { + std::set tmp; + tmp.insert(sTo->id()); + _landmarksIndex.insert(std::make_pair(jter->first, tmp)); } } } } + } + } - //remove neighbor links - std::multimap linksCopy = links; - for(std::multimap::iterator iter=linksCopy.begin(); iter!=linksCopy.end(); ++iter) + this->moveToTrash(s, keepLinkedInDb); + s = 0; + _linksChanged = true; + _memoryChanged = true; + } + return reducedTo; +} + +void Memory::moveSignatureToWMFromSTM(int id, int * reducedToOut) +{ + UDEBUG("Inserting node %d from STM in WM...", id); + UASSERT(_stMem.find(id) != _stMem.end()); + int reducedId = 0; + if(_reduceGraph) + { + Signature * s = this->_getSignature(id); + UASSERT(s!=0); + std::multimap links = s->getLinks(); + // Setting true to make sure we save all visual + // words that could be referenced in a previously + // transferred node in LTM (#979) + reducedId = reduceNode(s->id(), 0, true); + if(reducedToOut) { + *reducedToOut = reducedId; + } + if(reducedId>0) + { + for(std::multimap::iterator iter=links.begin(); iter!=links.end(); ++iter) + { + if(iter->second.type() == Link::kNeighbor) { - if(iter->second.type() == Link::kNeighborMerged) + if(_lastGlobalLoopClosureId == s->id()) { - // Removing only merged neighbor links, we keep original neighbor - // links to be able to reprocess databases with correct odometry covariance. - s->removeLink(iter->first); - } - if(iter->second.type() == Link::kNeighbor) - { - if(_lastGlobalLoopClosureId == s->id()) - { - _lastGlobalLoopClosureId = iter->first; - } + _lastGlobalLoopClosureId = iter->first; } } - - // Setting true to make sure we save all visual - // words that could be referenced in a previously - // transferred node in LTM (#979) - this->moveToTrash(s, true); - s = 0; } } } - if(s != 0) + if(reducedId == 0) { _workingMem.insert(_workingMem.end(), std::make_pair(*_stMem.begin(), UTimer::now())); _stMem.erase(*_stMem.begin()); } - // else already removed from STM/WM in moveToTrash() + // else already removed from STM/WM in reduceNode() } const Signature * Memory::getSignature(int id) const @@ -2610,9 +2676,10 @@ void Memory::moveToTrash(Signature * s, bool keepLinkedToGraph, std::list * // If not saved to database if(!keepLinkedToGraph) { - UASSERT_MSG(this->isInSTM(s->id()), + UASSERT_MSG(this->isInSTM(s->id()) || this->isInWM(s->id()), uFormat("Deleting location (%d) outside the " - "STM is not implemented!", s->id()).c_str()); + "WM/STM is not implemented! STM size=%ld WM size=%ld", + s->id(), this->getStMem().size(), this->getWorkingMem().size()).c_str()); const std::multimap & links = s->getLinks(); for(std::multimap::const_iterator iter=links.begin(); iter!=links.end(); ++iter) { @@ -2623,7 +2690,7 @@ void Memory::moveToTrash(Signature * s, bool keepLinkedToGraph, std::list * UASSERT_MSG(sTo!=0, uFormat("A neighbor (%d) of the deleted location %d is " "not found in WM/STM! Are you deleting a location " - "outside the STM?", iter->first, s->id()).c_str()); + "outside the WM/STM?", iter->first, s->id()).c_str()); if(iter->first > s->id() && links.size()>1 && sTo->hasLink(s->id())) { @@ -2633,7 +2700,7 @@ void Memory::moveToTrash(Signature * s, bool keepLinkedToGraph, std::list * } // child - if(iter->second.type() == Link::kGlobalClosure && s->id() > sTo->id() && s->getWeight()>0) + if(iter->second.type() == Link::kGlobalClosure && s->getWeight()>0) { sTo->setWeight(sTo->getWeight() + s->getWeight()); // copy weight } diff --git a/corelib/src/Optimizer.cpp b/corelib/src/Optimizer.cpp index 4f2197b2..86be75dc 100644 --- a/corelib/src/Optimizer.cpp +++ b/corelib/src/Optimizer.cpp @@ -217,6 +217,10 @@ LinkIdKey(int id, Link::Type type) : { return true; } + else if(k.type_ == Link::kNeighborMerged && type_ != Link::kNeighbor && type_ != Link::kNeighborMerged) + { + return false; + } else { // normal link, sort by smallest to largest id @@ -256,7 +260,7 @@ void Optimizer::getConnectedGraph( } } - while(nextPoses.size()) + while(!nextPoses.empty()) { // Fill up all nodes before landmarks // For nodes, fill up all neightbor nodes before loop closure ones diff --git a/corelib/src/Rtabmap.cpp b/corelib/src/Rtabmap.cpp index dd99a2c9..16055cf7 100644 --- a/corelib/src/Rtabmap.cpp +++ b/corelib/src/Rtabmap.cpp @@ -5731,6 +5731,7 @@ int Rtabmap::detectMoreLoopClosures( if(toFromMapId >=0) { + size_t clustersBefore = clusters.size(); for(std::multimap::iterator iter=clusters.begin(); iter!=clusters.end();) { int mapId = uValue(mapIds, iter->first, 0); @@ -5742,7 +5743,7 @@ int Rtabmap::detectMoreLoopClosures( ++iter; } } - UINFO("Looking for more loop closures: filtered %ld clusters for map session %d.", clusters.size(), toFromMapId); + UINFO("Looking for more loop closures: filtered %ld/%ld clusters for map session %d.", clustersBefore-clusters.size(), clustersBefore, toFromMapId); if(clusters.empty()) { UERROR("No clusters belong to mapId %d, aborting.", toFromMapId); @@ -5750,6 +5751,33 @@ int Rtabmap::detectMoreLoopClosures( } } + if(_memory->getMaxStMemSize() > 1) + { + size_t clustersBefore = clusters.size(); + for(std::multimap::iterator iter=clusters.begin(); iter!=clusters.end();) + { + if(abs(iter->first - iter->second) < _memory->getMaxStMemSize()) + { + iter = clusters.erase(iter); + } + else + { + // compute path to know how far we are in terms of graph length + std::map ids = _memory->getNeighborsId(iter->first, _memory->getMaxStMemSize(), -1, true, true, true); + if(ids.find(iter->second) != ids.end()) + { + iter = clusters.erase(iter); + } + else + { + ++iter; + } + } + } + UINFO("Looking for more loop closures: filtered %ld/%ld clusters for too close nodes (below %s=%d).", + clustersBefore-clusters.size(), clustersBefore, Parameters::kMemSTMSize().c_str(), _memory->getMaxStMemSize()); + } + int i=0; std::set addedLinks; for(std::multimap::iterator iter=clusters.begin(); iter!= clusters.end(); ++iter, ++i) diff --git a/corelib/src/VWDictionary.cpp b/corelib/src/VWDictionary.cpp index 2829b301..952e012c 100644 --- a/corelib/src/VWDictionary.cpp +++ b/corelib/src/VWDictionary.cpp @@ -573,6 +573,15 @@ void VWDictionary::update() _removedIndexedWords.size() == 0 && _visualWords.size()) { + const int IMGIDX_SHIFT = 18; + const int IMGIDX_ONE = (1 << IMGIDX_SHIFT); // a limit defined in https://github.com/opencv/opencv/blob/4.x/modules/features2d/src/matchers.cpp + if(_dataTree.rows >= IMGIDX_ONE) + { + UWARN("%s=%d is not a FLANN strategy and the number of words in the vocabulary (%d) is over %d (IMGIDX_ONE), so opencv may " + "assert on an IMGIDX_ONE check when adding new words. Use a FLANN strategy instead (%s<%d).", + Parameters::kKpNNStrategy().c_str(), _strategy, _dataTree.rows, IMGIDX_ONE, Parameters::kKpNNStrategy().c_str(), kNNBruteForce); + } + //just add not indexed words int i = _dataTree.rows; if(!_dataTree.empty()) { @@ -1006,7 +1015,7 @@ std::list VWDictionary::addNewWords( if(_flannIndex->isBuilt() || (!_dataTree.empty() && _dataTree.rows >= (int)k)) { //Find nearest neighbors - UDEBUG("newPts.total()=%d ", descriptors.rows); + UDEBUG("newPts.total()=%d _strategy=%d", descriptors.rows, _strategy); if(_strategy == kNNFlannNaive || _strategy == kNNFlannKdTree || _strategy == kNNFlannLSH) { diff --git a/corelib/src/optimizer/OptimizerG2O.cpp b/corelib/src/optimizer/OptimizerG2O.cpp index a97dac9b..0a869c10 100644 --- a/corelib/src/optimizer/OptimizerG2O.cpp +++ b/corelib/src/optimizer/OptimizerG2O.cpp @@ -1560,7 +1560,13 @@ std::map OptimizerG2O::optimizeBA( #endif // RTABMAP_ORB_SLAM #ifndef RTABMAP_ORB_SLAM - if(optimizer_ == 1) + // ISSUE: It seems the fatal error + // "[SetJac] infinite jac" happens relatively + // easily with GaussNewton on SBA problem, + // ignore optimizer_ and always use Levenberg for SBA. + // TODO: Note that g2o/RobustKernelDelta parameter could be + // potentially tuned to avoid that error with GaussNewton. + if(0)//optimizer_ == 1) { #ifdef RTABMAP_G2O_CPP11 optimizer.setAlgorithm(new g2o::OptimizationAlgorithmGaussNewton( @@ -2018,7 +2024,8 @@ std::map OptimizerG2O::optimizeBA( if(uIsNan(chi2)) { - UERROR("Optimization generated NANs, aborting optimization! Try another g2o's optimizer (current=%d).", optimizer_); + UERROR("Optimization generated NANs, aborting optimization! Try another g2o's optimizer (current %s=%d) or solver (current %s=%d).", + Parameters::kg2oOptimizer().c_str(), optimizer_, Parameters::kg2oSolver().c_str(), solver_); return optimizedPoses; } UDEBUG("iteration %d: %d nodes, %d edges, chi2: %f", i, (int)optimizer.vertices().size(), (int)optimizer.edges().size(), chi2); @@ -2052,15 +2059,18 @@ std::map OptimizerG2O::optimizeBA( //UDEBUG("Ignoring edge (%d<->%d) d=%f var=%f kernel=%f chi2=%f", (*iter)->vertex(0)->id()-stepVertexId, (*iter)->vertex(1)->id(), d, 1.0/((g2o::EdgeProjectP2SC*)(*iter))->information()(0,0), (*iter)->robustKernel()->delta(), (*iter)->chi2()); #endif - cv::Point3f pt3d; + int id=-1; if((*iter)->vertex(0)->id() > negVertexOffset) { - pt3d = points3DMap.at(negVertexOffset - (*iter)->vertex(0)->id()); + id = negVertexOffset - (*iter)->vertex(0)->id(); } else { - pt3d = points3DMap.at((*iter)->vertex(0)->id()-stepVertexId); + id = (*iter)->vertex(0)->id() - stepVertexId; } + UASSERT_MSG(points3DMap.find(id) != points3DMap.end(), uFormat("word id=%d points3DMap=%ld vertex id=%d (negVertexOffset=%d stepVertexId=%d)", + id, points3DMap.size(), (*iter)->vertex(0)->id(), negVertexOffset, stepVertexId).c_str()); + cv::Point3f pt3d = points3DMap.at(id); ((g2o::VertexSBAPointXYZ*)(*iter)->vertex(0))->setEstimate(Eigen::Vector3d(pt3d.x, pt3d.y, pt3d.z)); if(outliers) diff --git a/guilib/include/rtabmap/gui/MainWindow.h b/guilib/include/rtabmap/gui/MainWindow.h index 2f8b14b5..24db8941 100644 --- a/guilib/include/rtabmap/gui/MainWindow.h +++ b/guilib/include/rtabmap/gui/MainWindow.h @@ -330,6 +330,7 @@ protected: int iterations, bool interSession, bool intraSession, + int minGraphDistance, // SBA params: bool sba, int sbaIterations, diff --git a/guilib/include/rtabmap/gui/PostProcessingDialog.h b/guilib/include/rtabmap/gui/PostProcessingDialog.h index b0b2bfe8..c9310a82 100644 --- a/guilib/include/rtabmap/gui/PostProcessingDialog.h +++ b/guilib/include/rtabmap/gui/PostProcessingDialog.h @@ -59,6 +59,7 @@ public: int iterations() const; bool intraSession() const; bool interSession() const; + int minGraphDistance() const; bool isRefineNeighborLinks() const; bool isRefineLoopClosureLinks() const; bool isSBA() const; @@ -74,6 +75,7 @@ public: void setIterations(int iterations); void setIntraSession(bool enabled); void setInterSession(bool enabled); + void setMinGraphDistance(int value); void setRefineNeighborLinks(bool on); void setRefineLoopClosureLinks(bool on); void setSBA(bool on); diff --git a/guilib/src/DatabaseViewer.cpp b/guilib/src/DatabaseViewer.cpp index ed7c4059..303b9baa 100644 --- a/guilib/src/DatabaseViewer.cpp +++ b/guilib/src/DatabaseViewer.cpp @@ -239,6 +239,7 @@ DatabaseViewer::DatabaseViewer(const QString & ini, QWidget * parent) : parameters.insert(*Parameters::getDefaultParameters().find(Parameters::kRGBDLoopClosureReextractFeatures())); parameters.insert(*Parameters::getDefaultParameters().find(Parameters::kRGBDLoopCovLimited())); parameters.insert(*Parameters::getDefaultParameters().find(Parameters::kRGBDProximityPathFilteringRadius())); + parameters.insert(*Parameters::getDefaultParameters().find(Parameters::kMemSTMSize())); ui_->parameters_toolbox->setupUi(parameters); exportDialog_->setObjectName("ExportCloudsDialog"); restoreDefaultSettings(); @@ -481,6 +482,7 @@ DatabaseViewer::DatabaseViewer(const QString & ini, QWidget * parent) : connect(ui_->spinBox_detectMore_iterations, SIGNAL(valueChanged(int)), this, SLOT(configModified())); connect(ui_->checkBox_detectMore_intraSession, SIGNAL(stateChanged(int)), this, SLOT(configModified())); connect(ui_->checkBox_detectMore_interSession, SIGNAL(stateChanged(int)), this, SLOT(configModified())); + connect(ui_->spinBox_minGraphDistance, SIGNAL(valueChanged(int)), this, SLOT(configModified())); connect(ui_->checkBox_opt_graph_as_guess, SIGNAL(stateChanged(int)), this, SLOT(configModified())); connect(ui_->lineEdit_obstacleColor, SIGNAL(textChanged(const QString &)), this, SLOT(configModified())); @@ -678,6 +680,7 @@ void DatabaseViewer::readSettings() ui_->checkBox_detectMore_intraSession->setChecked(settings.value("intra_session", ui_->checkBox_detectMore_intraSession->isChecked()).toBool()); ui_->checkBox_detectMore_interSession->setChecked(settings.value("inter_session", ui_->checkBox_detectMore_interSession->isChecked()).toBool()); ui_->checkBox_opt_graph_as_guess->setChecked(settings.value("opt_graph_as_guess", ui_->checkBox_opt_graph_as_guess->isChecked()).toBool()); + ui_->spinBox_minGraphDistance->setValue(settings.value("min_graph_distance", ui_->spinBox_minGraphDistance->value()).toInt()); settings.endGroup(); settings.endGroup(); @@ -775,6 +778,7 @@ void DatabaseViewer::writeSettings() settings.setValue("intra_session", ui_->checkBox_detectMore_intraSession->isChecked()); settings.setValue("inter_session", ui_->checkBox_detectMore_interSession->isChecked()); settings.setValue("opt_graph_as_guess", ui_->checkBox_opt_graph_as_guess->isChecked()); + settings.setValue("min_graph_distance", ui_->spinBox_minGraphDistance->value()); settings.endGroup(); settings.endGroup(); @@ -856,6 +860,7 @@ void DatabaseViewer::restoreDefaultSettings() ui_->checkBox_detectMore_interSession->setChecked(true); ui_->checkBox_opt_graph_as_guess->setChecked(true); ui_->spinBox_fromToMapId->setValue(-1); + ui_->spinBox_minGraphDistance->setValue(10); } void DatabaseViewer::openDatabase() @@ -4285,10 +4290,12 @@ void DatabaseViewer::detectMoreLoopClosures() const ParametersMap & parameters = ui_->parameters_toolbox->getParameters(); bool loopCovLimited = Parameters::defaultRGBDLoopCovLimited(); Parameters::parse(parameters, Parameters::kRGBDLoopCovLimited(), loopCovLimited); + std::multimap links = updateLinksWithModifications(links_); if(loopCovLimited) { - odomMaxInf_ = graph::getMaxOdomInf(updateLinksWithModifications(links_)); + odomMaxInf_ = graph::getMaxOdomInf(links); } + links = graph::filterLinks(links, Link::kNeighbor, true); // keep only neighbor links int iterations = ui_->spinBox_detectMore_iterations->value(); UASSERT(iterations > 0); @@ -4299,6 +4306,7 @@ void DatabaseViewer::detectMoreLoopClosures() bool interSession = ui_->checkBox_detectMore_interSession->isChecked(); bool useOptimizedGraphAsGuess = ui_->checkBox_opt_graph_as_guess->isChecked(); int fromToMapId = ui_->spinBox_fromToMapId->value(); + int minimumGraphDistance = ui_->spinBox_minGraphDistance->value(); if(!interSession && !intraSession) { QMessageBox::warning(this, tr("Cannot detect more loop closures"), tr("Intra and inter session parameters are disabled! Enable one or both.")); @@ -4324,6 +4332,7 @@ void DatabaseViewer::detectMoreLoopClosures() progressDialog->appendText(tr("Looking for more loop closures: %1 clusters found.").arg(clusters.size())); if(fromToMapId >=0) { + int clusterBefore = clusters.size(); for(std::multimap::iterator iter=clusters.begin(); iter!=clusters.end();) { int mapId = uValue(mapIds_, iter->first, 0); @@ -4335,7 +4344,8 @@ void DatabaseViewer::detectMoreLoopClosures() ++iter; } } - progressDialog->appendText(tr("Looking for more loop closures: filtered %1 clusters for map session %2.").arg(clusters.size()).arg(fromToMapId)); + progressDialog->appendText(tr("Looking for more loop closures: filtered %1/%2 clusters for map session %3.") + .arg(clusterBefore-clusters.size()).arg(clusterBefore).arg(fromToMapId)); if(clusters.empty()) { progressDialog->appendText(tr("No clusters belong to mapId %1, aborting!").arg(fromToMapId)); @@ -4344,6 +4354,34 @@ void DatabaseViewer::detectMoreLoopClosures() } } + if(minimumGraphDistance > 1) + { + int clusterBefore = clusters.size(); + for(std::multimap::iterator iter=clusters.begin(); iter!=clusters.end();) + { + if(abs(iter->first - iter->second) < minimumGraphDistance) + { + iter = clusters.erase(iter); + } + else + { + // compute path to know how far we are in terms of graph length + std::list path = graph::computePath(links, iter->first, iter->second); + if(!path.empty() && (int)path.size() <= minimumGraphDistance) + { + iter = clusters.erase(iter); + } + else + { + ++iter; + } + } + } + progressDialog->appendText(tr("Filtered %1/%2 clusters for too close nodes (below minimum graph distance=%3).") + .arg(clusterBefore-clusters.size()).arg(clusterBefore).arg(minimumGraphDistance)); + QApplication::processEvents(); + } + progressDialog->setMaximumSteps(progressDialog->maximumSteps()+(int)clusters.size()); QApplication::processEvents(); @@ -4700,10 +4738,17 @@ void DatabaseViewer::graphNodeSelected(int id) void DatabaseViewer::graphLinkSelected(int from, int to) { - if(from>0 && idToIndex_.contains(from)) - ui_->horizontalSlider_A->setValue(idToIndex_.value(from)); - if(to>0 && idToIndex_.contains(to)) - ui_->horizontalSlider_B->setValue(idToIndex_.value(to)); + if(from < 0 || to < 0) + { + updateLoopClosuresSlider(from, to); + } + else + { + if(idToIndex_.contains(from)) + ui_->horizontalSlider_A->setValue(idToIndex_.value(from)); + if(idToIndex_.contains(to)) + ui_->horizontalSlider_B->setValue(idToIndex_.value(to)); + } } void DatabaseViewer::sliderAValueChanged(int value) @@ -8051,6 +8096,17 @@ void DatabaseViewer::updateGraphView() ui_->label_timeOptimization->setNum(0); ui_->label_poses->setNum((int)optPoses.size()); graphes_.push_back(optPoses); + // Just get the links: + std::map posesOut; + UINFO("Get connected graph from %d (%d poses, %d links)", fromId, (int)poses.size(), (int)links.size()); + std::shared_ptr optimizer(Optimizer::create(parameters)); + optimizer->getConnectedGraph( + fromId, + optPoses, + links, + posesOut, + graphLinks_); + UINFO("Connected graph of %d poses and %d links", (int)posesOut.size(), (int)graphLinks_.size()); } ui_->horizontalSlider_rotation->setEnabled(false); ui_->pushButton_applyRotation->setEnabled(false); diff --git a/guilib/src/ExportCloudsDialog.cpp b/guilib/src/ExportCloudsDialog.cpp index 2c5ef930..a85c9e53 100644 --- a/guilib/src/ExportCloudsDialog.cpp +++ b/guilib/src/ExportCloudsDialog.cpp @@ -3703,7 +3703,7 @@ std::map::Ptr, pcl::Indic 0, 0, 0, - _ui->checkBox_fromDepth->isChecked()?&confidence:0); + _ui->checkBox_fromDepth->isChecked()&&_ui->spinBox_depthConfidence->value()>0?&confidence:0); } else if(_dbDriver) { @@ -3717,7 +3717,7 @@ std::map::Ptr, pcl::Indic 0, 0, 0, - _ui->checkBox_fromDepth->isChecked()?&confidence:0); + _ui->checkBox_fromDepth->isChecked()&&_ui->spinBox_depthConfidence->value()>0?&confidence:0); } if(_ui->checkBox_fromDepth->isChecked() && !data.imageRaw().empty() && !data.depthOrRightRaw().empty()) diff --git a/guilib/src/MainWindow.cpp b/guilib/src/MainWindow.cpp index 66108bec..97f5ff5c 100644 --- a/guilib/src/MainWindow.cpp +++ b/guilib/src/MainWindow.cpp @@ -6563,6 +6563,7 @@ void MainWindow::showPostProcessingDialog() _postProcessingDialog->iterations(), _postProcessingDialog->interSession(), _postProcessingDialog->intraSession(), + _postProcessingDialog->minGraphDistance(), _postProcessingDialog->isSBA(), _postProcessingDialog->sbaIterations(), _postProcessingDialog->sbaVariance(), @@ -6579,6 +6580,7 @@ void MainWindow::postProcessing( int iterations, bool interSession, bool intraSession, + int minGraphDistance, bool sba, int sbaIterations, double sbaVariance, @@ -6687,6 +6689,7 @@ void MainWindow::postProcessing( { odomMaxInf = graph::getMaxOdomInf(_currentLinksMap); } + std::multimap neigborLinks = graph::filterLinks(_currentLinksMap, Link::kNeighbor, true); std::shared_ptr registration(Registration::create(parameters)); @@ -6705,6 +6708,34 @@ void MainWindow::postProcessing( _progressDialog->appendText(tr("Looking for more loop closures, clustering poses... found %1 clusters.").arg(clusters.size())); QApplication::processEvents(); + if(minGraphDistance > 1) + { + int clustersBefore = clusters.size(); + for(std::multimap::iterator iter=clusters.begin(); iter!=clusters.end();) + { + if(abs(iter->first - iter->second) < minGraphDistance) + { + iter = clusters.erase(iter); + } + else + { + // compute path to know how far we are in terms of graph length + std::list path = graph::computePath(neigborLinks, iter->first, iter->second); + if(!path.empty() && (int)path.size() <= minGraphDistance) + { + iter = clusters.erase(iter); + } + else + { + ++iter; + } + } + } + _progressDialog->appendText(tr("Filtered %1/%2 clusters for too close nodes (below minimum graph distance=%3).") + .arg(clustersBefore-clusters.size()).arg(clustersBefore).arg(minGraphDistance)); + QApplication::processEvents(); + } + int i=0; std::set addedLinks; for(std::multimap::iterator iter=clusters.begin(); iter!= clusters.end() && !_progressCanceled; ++iter, ++i) diff --git a/guilib/src/PostProcessingDialog.cpp b/guilib/src/PostProcessingDialog.cpp index 54c498bf..e4bf4409 100644 --- a/guilib/src/PostProcessingDialog.cpp +++ b/guilib/src/PostProcessingDialog.cpp @@ -82,6 +82,7 @@ PostProcessingDialog::PostProcessingDialog(QWidget * parent) : connect(_ui->iterations, SIGNAL(valueChanged(int)), this, SIGNAL(configChanged())); connect(_ui->intraSession, SIGNAL(stateChanged(int)), this, SIGNAL(configChanged())); connect(_ui->interSession, SIGNAL(stateChanged(int)), this, SIGNAL(configChanged())); + connect(_ui->minGraphDistance, SIGNAL(valueChanged(int)), this, SIGNAL(configChanged())); connect(_ui->refineNeighborLinks, SIGNAL(stateChanged(int)), this, SIGNAL(configChanged())); connect(_ui->refineLoopClosureLinks, SIGNAL(stateChanged(int)), this, SIGNAL(configChanged())); @@ -150,6 +151,7 @@ void PostProcessingDialog::saveSettings(QSettings & settings, const QString & gr settings.setValue("iterations", this->iterations()); settings.setValue("intra_session", this->intraSession()); settings.setValue("inter_session", this->interSession()); + settings.setValue("min_graph_distance", this->minGraphDistance()); settings.setValue("refine_neigbors", this->isRefineNeighborLinks()); settings.setValue("refine_lc", this->isRefineLoopClosureLinks()); settings.setValue("sba", this->isSBA()); @@ -175,6 +177,7 @@ void PostProcessingDialog::loadSettings(QSettings & settings, const QString & gr this->setIterations(settings.value("iterations", this->iterations()).toInt()); this->setIntraSession(settings.value("intra_session", this->intraSession()).toBool()); this->setInterSession(settings.value("inter_session", this->interSession()).toBool()); + this->setMinGraphDistance(settings.value("min_graph_distance", this->minGraphDistance()).toInt()); this->setRefineNeighborLinks(settings.value("refine_neigbors", this->isRefineNeighborLinks()).toBool()); this->setRefineLoopClosureLinks(settings.value("refine_lc", this->isRefineLoopClosureLinks()).toBool()); this->setSBA(settings.value("sba", this->isSBA()).toBool()); @@ -197,6 +200,7 @@ void PostProcessingDialog::restoreDefaults() setIterations(5); setIntraSession(true); setInterSession(true); + setMinGraphDistance(10); setRefineNeighborLinks(false); setRefineLoopClosureLinks(false); setSBA(false); @@ -254,6 +258,11 @@ bool PostProcessingDialog::interSession() const return _ui->interSession->isChecked(); } +int PostProcessingDialog::minGraphDistance() const +{ + return _ui->minGraphDistance->value(); +} + bool PostProcessingDialog::isRefineNeighborLinks() const { return _ui->refineNeighborLinks->isChecked(); @@ -311,6 +320,10 @@ void PostProcessingDialog::setInterSession(bool enabled) { _ui->interSession->setChecked(enabled); } +void PostProcessingDialog::setMinGraphDistance(int value) +{ + _ui->minGraphDistance->setValue(value); +} void PostProcessingDialog::setRefineNeighborLinks(bool on) { _ui->refineNeighborLinks->setChecked(on); diff --git a/guilib/src/ui/DatabaseViewer.ui b/guilib/src/ui/DatabaseViewer.ui index 54995ee9..cc7918fb 100644 --- a/guilib/src/ui/DatabaseViewer.ui +++ b/guilib/src/ui/DatabaseViewer.ui @@ -1729,7 +1729,7 @@ 0 0 - 314 + 403 188 @@ -1885,8 +1885,8 @@ 0 0 - 276 - 1598 + 518 + 1071 @@ -2604,15 +2604,29 @@ 0 - -152 + -175 403 - 287 + 319 Detect more loop closures + + + + Angle + + + + + + + Inter-session + + + @@ -2620,6 +2634,103 @@ + + + + Radius Max + + + + + + + Intra-session + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + degrees + + + 0 + + + 180.000000000000000 + + + 1.000000000000000 + + + 30.000000000000000 + + + + + + + Radius Min + + + + + + + 1 + + + 100 + + + 5 + + + + + + + From/to map ID only (-1 is all) + + + + + + + Iterations + + + + + + + + + + true + + + + + + + + + + @@ -2639,43 +2750,6 @@ - - - - Intra-session - - - - - - - Iterations - - - - - - - 1 - - - 100 - - - 5 - - - - - - - - - - true - - - @@ -2686,13 +2760,6 @@ - - - - Angle - - - @@ -2712,66 +2779,6 @@ - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - - - - Radius Max - - - - - - - Radius Min - - - - - - - Inter-session - - - - - - - degrees - - - 0 - - - 180.000000000000000 - - - 1.000000000000000 - - - 30.000000000000000 - - - @@ -2785,10 +2792,23 @@ - - + + - From/to map ID only (-1 is all) + Minimum graph distance + + + + + + + 1 + + + 9999 + + + 10 @@ -2799,8 +2819,8 @@ 0 0 - 181 - 485 + 428 + 196 diff --git a/guilib/src/ui/postProcessingDialog.ui b/guilib/src/ui/postProcessingDialog.ui index f6fbe23e..de0292fb 100644 --- a/guilib/src/ui/postProcessingDialog.ui +++ b/guilib/src/ui/postProcessingDialog.ui @@ -6,8 +6,8 @@ 0 0 - 552 - 633 + 553 + 662 @@ -67,10 +67,10 @@ - - + + - Cluster radius + Inter-session true @@ -87,10 +87,10 @@ - - + + - Iterations + Cluster radius true @@ -120,16 +120,6 @@ - - - - Inter-session - - - true - - - @@ -137,6 +127,16 @@ + + + + Iterations + + + true + + + @@ -144,6 +144,32 @@ + + + + Minimum graph distance + + + true + + + + + + + nodes + + + 1 + + + 9999 + + + 10 + + + diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 5d9ae525..e3f4c258 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -16,6 +16,7 @@ ADD_SUBDIRECTORY( Report ) ADD_SUBDIRECTORY( Info ) ADD_SUBDIRECTORY( CleanupLocalGrids ) ADD_SUBDIRECTORY( GlobalBundleAdjustment ) +ADD_SUBDIRECTORY( ReduceGraph ) IF(OPENCV_NONFREE_FOUND) ADD_SUBDIRECTORY( VocabularyComparison ) diff --git a/tools/DetectMoreLoopClosures/main.cpp b/tools/DetectMoreLoopClosures/main.cpp index 3f4f7219..2f4fb31f 100644 --- a/tools/DetectMoreLoopClosures/main.cpp +++ b/tools/DetectMoreLoopClosures/main.cpp @@ -63,14 +63,7 @@ void showUsage() exit(1); } -// catch ctrl-c bool g_loopForever = true; -void sighandler(int sig) -{ - printf("\nSignal %d caught...\n", sig); - g_loopForever = false; -} - class PrintProgressState : public ProgressState { public: @@ -86,6 +79,15 @@ public: private: double stamp_; }; +PrintProgressState progress; + +// catch ctrl-c +void sighandler(int sig) +{ + printf("\nSignal %d caught...\n", sig); + g_loopForever = false; + progress.setCanceled(true); +} int main(int argc, char * argv[]) { @@ -94,7 +96,7 @@ int main(int argc, char * argv[]) signal(SIGINT, &sighandler); ULogger::setType(ULogger::kTypeConsole); - ULogger::setLevel(ULogger::kError); + ULogger::setLevel(ULogger::kWarning); if(argc < 2) { @@ -195,7 +197,7 @@ int main(int argc, char * argv[]) // Add some optimizations (soft set, can be overriden by arguments) inputParams.insert(ParametersPair(Parameters::kMemLoadVisualLocalFeaturesOnInit(), "false")); // don't need features already loaded in RAM - inputParams.insert(ParametersPair(Parameters::kKpNNStrategy(), "3")); // don't need flann index + inputParams.insert(ParametersPair(Parameters::kMemIncrementalMemory(), "true")); // should be incremental to update links std::string dbPath = argv[argc-1]; if(!UFile::exists(dbPath)) @@ -245,6 +247,7 @@ int main(int argc, char * argv[]) Rtabmap rtabmap; printf("Initialization...\n"); UTimer timer; + ParametersMap originalParameters = parameters; uInsert(parameters, inputParams); rtabmap.init(parameters, dbPath); printf("Initialization... done! (%f sec)\n", timer.ticks()); @@ -267,7 +270,6 @@ int main(int argc, char * argv[]) printf("From/To Session ID = %d%s\n", fromToMapId, last?" (last session)":""); } - PrintProgressState progress; printf("Detecting...\n"); int detected = rtabmap.detectMoreLoopClosures(clusterRadiusMax, clusterAngle, iterations, intraSession, interSession, &progress, clusterRadiusMin, fromToMapId); if(detected < 0) @@ -306,6 +308,9 @@ int main(int argc, char * argv[]) } } + // Restore original parameters before saving back the database + rtabmap.parseParameters(originalParameters); + rtabmap.close(); return 0; diff --git a/tools/ReduceGraph/CMakeLists.txt b/tools/ReduceGraph/CMakeLists.txt new file mode 100644 index 00000000..dfa61deb --- /dev/null +++ b/tools/ReduceGraph/CMakeLists.txt @@ -0,0 +1,13 @@ + +ADD_EXECUTABLE(reduceGraph main.cpp) + +TARGET_LINK_LIBRARIES(reduceGraph rtabmap_core) + +SET_TARGET_PROPERTIES( reduceGraph + PROPERTIES OUTPUT_NAME ${PROJECT_PREFIX}-reduceGraph) + +INSTALL(TARGETS reduceGraph + RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" COMPONENT runtime + BUNDLE DESTINATION "${CMAKE_BUNDLE_LOCATION}" COMPONENT runtime) + + diff --git a/tools/ReduceGraph/main.cpp b/tools/ReduceGraph/main.cpp new file mode 100644 index 00000000..eaabf7a2 --- /dev/null +++ b/tools/ReduceGraph/main.cpp @@ -0,0 +1,281 @@ +/* +Copyright (c) 2010-2026, Mathieu Labbe - IntRoLab - Universite de Sherbrooke +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Universite de Sherbrooke nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace rtabmap; + +void showUsage(const char * exec) +{ + printf("\nUsage:\n" + "%s [Options] database.db\n" + "Options:\n" + " --keep_latest Merge old nodes to newer nodes, thus keeping only latest nodes.\n" + " --keep_linked Keep reduced nodes linked to graph.\n" + " --pre_cleanup Remove all user loop closures linking nodes closer than %s in the graph before reducing the graph.\n" + " --radius #.# Maximum loop closure distance that can be merged. Default is 1 m. Should be > 0.\n" + " --udebug/--uinfo/--warn can also be used to change verbosity.\n" + "\n", exec, Parameters::kMemSTMSize().c_str()); + exit(1); +} + +int main(int argc, char * argv[]) +{ + ULogger::setType(ULogger::kTypeConsole); + ULogger::setLevel(ULogger::kWarning); + + if(argc < 2) + { + showUsage(argv[0]); + } + + bool keepLatest = false; + bool keepLinked = false; + float radius = 1.0f; + bool preCleanup = false; + for(int i=1; i 0, parsed %f\n", radius); + showUsage(argv[0]); + } + } + else { + showUsage(argv[0]); + } + } + } + printf("Parameters:\n"); + printf(" radius = %f m\n", radius); + printf(" keep_latest = %s\n", keepLatest?"true":"false"); + printf(" keep_linked = %s\n", keepLinked?"true":"false"); + printf(" pre_cleanup = %s\n", preCleanup?"true":"false"); + + // Just parse logging options + Parameters::parseArguments(argc, argv); + + // Add some optimizations + ParametersMap inputParams; + inputParams.insert(ParametersPair(Parameters::kMemInitWMWithAllNodes(), "true")); // load the whole map in RAM + inputParams.insert(ParametersPair(Parameters::kMemLoadVisualLocalFeaturesOnInit(), "false")); // don't need features already loaded in RAM + + std::string dbPath = argv[argc-1]; + if(!UFile::exists(dbPath)) + { + printf("Database %s doesn't exist!\n", dbPath.c_str()); + return 1; + } + + printf("Database: %s\n", dbPath.c_str()); + + // Get parameters + ParametersMap parameters; + DBDriver * driver = DBDriver::create(); + if(driver->openConnection(dbPath)) + { + parameters = driver->getLastParameters(); + driver->closeConnection(false); + } + else + { + UERROR("Cannot open database %s!", dbPath.c_str()); + } + delete driver; + + Memory memory; + printf("Initialization...\n"); + UTimer timer; + ParametersMap originalParameters = parameters; + uInsert(parameters, inputParams); + if(!memory.init(dbPath, false, parameters)) + { + printf("Initialization... failed! Aborting!\n"); + return 1; + } + std::set ids = memory.getAllSignatureIds(); + printf("Initialization... done! %ld nodes loaded. (%f sec)\n", ids.size(), timer.ticks()); + + if(ids.empty()) + { + printf("IDs are empty?! Aborting.\n"); + return 1; + } + + Transform lastLocalizationPose; + std::map optimizedPoses = memory.loadOptimizedPoses(&lastLocalizationPose); + float xMin, yMin, cellSize; + bool hasOptimizedMap = !memory.load2DMap(xMin, yMin, cellSize).empty(); + + int totalNodesReduced = 0; + std::vector vids; + vids.reserve(ids.size()); + if(keepLatest) + { + // we process older to newer nodes, merging to new nodes + vids.insert(vids.end(), ids.begin(), ids.end()); + } + else + { + // we process newer to older nodes, merging to old nodes + vids.insert(vids.end(), ids.rbegin(), ids.rend()); + } + + if(preCleanup) + { + if(memory.getMaxStMemSize() <= 1) + { + printf("--pre_cleanup is used but %s <= 1, skipping pre cleanup...\n", Parameters::kMemSTMSize().c_str()); + } + else + { + int totalRemoved = 0; + for(auto id: vids) + { + auto nids = memory.getNeighborsId(id, memory.getMaxStMemSize(), -1, true, true, true); + auto links = memory.getLinks(id, true, false); + for(auto link:links) + { + if( link.second.type() == Link::kUserClosure && + nids.find(link.first)!=nids.end()) + { + memory.removeLink(id, link.first); + ++totalRemoved; + } + } + } + printf("Removed %d user links that were linking nodes that were close in the graph (below %s=%d)\n", + totalRemoved, Parameters::kMemSTMSize().c_str(), memory.getMaxStMemSize()); + } + } + + for(auto id: vids) + { + // Nodes can be already reduced by other nodes, check if they are still there + if(memory.getSignature(id) != 0) + { + int reducedId = memory.reduceNode(id, radius, keepLinked, keepLatest?1:-1); + if(reducedId > 0) + { + printf("Reduced node %d to node %d!\n", id, reducedId); + ++totalNodesReduced; + } + } + } + printf("Reduced a total of %d nodes out of %ld nodes\n", totalNodesReduced, ids.size()); + + if(!optimizedPoses.empty()) + { + size_t removed = 0; + // cleanup reduced nodes from the optimized poses + for(std::map::iterator iter=optimizedPoses.lower_bound(0); iter!=optimizedPoses.end();) + { + if(memory.getSignature(iter->first) == 0) + { + iter = optimizedPoses.erase(iter); + ++removed; + } + else + { + ++iter; + } + } + printf("Updated optimized graph from %ld poses to %ld poses\n", optimizedPoses.size()+removed, optimizedPoses.size()); + memory.saveOptimizedPoses(optimizedPoses, lastLocalizationPose); + } + if(hasOptimizedMap) + { + printf("The database has a global occupancy grid, regenerating one with the remaining nodes of the optimized graph!\n"); + LocalGridCache cache; + OccupancyGrid grid(&cache, parameters); + for(std::map::iterator iter=optimizedPoses.lower_bound(0); iter!=optimizedPoses.end(); ++iter) + { + SensorData data = memory.getNodeData(iter->first, false, false, false, true); + data.uncompressData(); + cache.add(iter->first, data.gridGroundCellsRaw(), data.gridObstacleCellsRaw(), data.gridEmptyCellsRaw(), data.gridCellSize(), data.gridViewPoint()); + } + grid.update(optimizedPoses); + cv::Mat map = grid.getMap(xMin, yMin); + if(map.empty()) + { + printf("Could not regenerate the global occupancy grid! The grid is not updated.\n"); + } + else + { + memory.save2DMap(map, xMin, yMin, grid.getCellSize()); + printf("Saved the new global occupancy grid!\n"); + } + } + + // Restore original parameters before saving back the database + memory.parseParameters(originalParameters); + + printf("Saving all changes to database...\n"); + memory.close(true); + + return 0; +} diff --git a/tools/Reprocess/main.cpp b/tools/Reprocess/main.cpp index 30c27402..0449079c 100644 --- a/tools/Reprocess/main.cpp +++ b/tools/Reprocess/main.cpp @@ -57,7 +57,7 @@ void showUsage() " rtabmap-reprocess [options] \"input.db\" \"output.db\"\n" " rtabmap-reprocess [options] \"input1.db;input2.db;input3.db\" \"output.db\"\n" "\n" - " For the second example, only parameters from the first database are used.\n" + " For the second example, only parameters from the first database are used (unless -params_last or -default are used).\n" " If Mem/IncrementalMemory is false, RTAB-Map is initialized with the first input database,\n" " then localization-only is done with next databases against the first one.\n" " To see warnings when loop closures are rejected, add \"--uwarn\" argument.\n" @@ -71,6 +71,7 @@ void showUsage() " from the database. If custom parameters are also set as \n" " arguments, they overwrite those in config file and the database.\n" " -default Input database's parameters are ignored, using default ones instead.\n" + " -params_last Parameters of the last database is used instead of the first one (ignored if -default is also used).\n" " -odom Recompute odometry. See \"Odom/\" parameters with --params. If -skip option\n" " is used, it will be applied to odometry frames, not rtabmap frames. Multi-session\n" " may not be detected correctly if the input covariance between sessions doesn't have 9999.\n" @@ -260,6 +261,7 @@ int main(int argc, char * argv[]) bool assemble3dOctoMap = false; bool useDatabaseRate = false; bool useDefaultParameters = false; + bool useLastDatabaseParameters = false; bool recomputeOdometry = false; bool useInputOdometryAsGuess = false; double odomLinVarOverride = 0.0; @@ -318,6 +320,10 @@ int main(int argc, char * argv[]) useDefaultParameters = true; printf("Using default parameters.\n"); } + else if(strcmp(argv[i], "-params_last") == 0 || strcmp(argv[i], "--params_last") == 0) + { + useLastDatabaseParameters = true; + } else if(strcmp(argv[i], "-odom") == 0 || strcmp(argv[i], "--odom") == 0) { recomputeOdometry = true; @@ -683,11 +689,10 @@ int main(int argc, char * argv[]) } // Get parameters of the first database - DBDriver * dbDriver = DBDriver::create(); + std::shared_ptr dbDriver(DBDriver::create()); if(!dbDriver->openConnection(databases.front(), false)) { - printf("Failed opening input database!\n"); - delete dbDriver; + printf("Failed opening the input database!\n"); return 1; } @@ -695,13 +700,28 @@ int main(int argc, char * argv[]) std::string targetVersion; if(!useDefaultParameters) { - parameters = dbDriver->getLastParameters(); - targetVersion = dbDriver->getDatabaseVersion(); - parameters.insert(ParametersPair(Parameters::kDbTargetVersion(), targetVersion)); + if(databases.size() > 1 && useLastDatabaseParameters) + { + printf("Using last database's parameters.\n"); + std::shared_ptr lastDbDriver(DBDriver::create()); + if(!lastDbDriver->openConnection(databases.back(), true)) + { + printf("Failed opening the last input database!\n"); + return 1; + } + parameters = lastDbDriver->getLastParameters(); + targetVersion = lastDbDriver->getDatabaseVersion(); + } + else + { + parameters = dbDriver->getLastParameters(); + targetVersion = dbDriver->getDatabaseVersion(); + } if(parameters.empty()) { - printf("WARNING: Failed getting parameters from database, reprocessing will be done with default parameters! Database version may be too old (%s).\n", dbDriver->getDatabaseVersion().c_str()); + printf("WARNING: Failed getting parameters from database, reprocessing will be done with default parameters! Database version may be too old (%s).\n", targetVersion.c_str()); } + parameters.insert(ParametersPair(Parameters::kDbTargetVersion(), targetVersion)); } if(customParameters.size()) @@ -800,7 +820,6 @@ int main(int argc, char * argv[]) { printf("Input database doesn't have any nodes saved in it.\n"); dbDriver->closeConnection(false); - delete dbDriver; return 1; } if(!((!incrementalMemory || appendMode) && databases.size() > 1)) @@ -822,7 +841,6 @@ int main(int argc, char * argv[]) if (!dbDriver->openConnection(*iter, false)) { printf("Failed opening input database!\n"); - delete dbDriver; return 1; } ids.clear(); @@ -830,8 +848,7 @@ int main(int argc, char * argv[]) totalIds += ids.size(); dbDriver->closeConnection(false); } - delete dbDriver; - dbDriver = 0; + dbDriver.reset(); std::string workingDirectory = UDirectory::getDir(outputDatabasePath); printf("Set working directory to \"%s\".\n", workingDirectory.c_str()); @@ -1386,13 +1403,12 @@ int main(int argc, char * argv[]) { if(save2DMap) { - DBDriver * driver = DBDriver::create(); + std::shared_ptr driver(DBDriver::create()); if(driver->openConnection(outputDatabasePath)) { driver->save2DMap(map, xMin, yMin, grid.getCellSize()); printf("Saving occupancy grid to database... done!\n"); } - delete driver; } else { @@ -1481,13 +1497,12 @@ int main(int argc, char * argv[]) { if(save2DMap) { - DBDriver * driver = DBDriver::create(); + std::shared_ptr driver(DBDriver::create()); if(driver->openConnection(outputDatabasePath)) { driver->save2DMap(map, xMin, yMin, cellSize); printf("Saving occupancy grid to database... done!\n"); } - delete driver; } else { From 8fd701aabe3e792c526184904047c45079a32e87 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 5 Apr 2026 12:36:54 -0700 Subject: [PATCH 095/126] =?UTF-8?q?Fixing=20prior=20and=20gravity=20constr?= =?UTF-8?q?aints=20support=20for=20SBA=20with=20orbslam=20dep=E2=80=A6=20(?= =?UTF-8?q?#1683)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixing prior and gravity constraints support for SBA with orbslam dependency * Updated an error log msg --- corelib/src/odometry/OdometryORBSLAM3.cpp | 9 +- corelib/src/optimizer/OptimizerG2O.cpp | 109 +++--------------- .../src/optimizer/g2o/edge_sbacam_gravity.h | 48 +++++--- corelib/src/optimizer/g2o/edge_sbacam_prior.h | 22 +++- corelib/src/optimizer/g2o/edge_se3_expmap.h | 74 ++++++++++++ guilib/include/rtabmap/gui/StatsToolBox.h | 5 + guilib/src/StatsToolBox.cpp | 45 ++++++-- 7 files changed, 188 insertions(+), 124 deletions(-) create mode 100644 corelib/src/optimizer/g2o/edge_se3_expmap.h diff --git a/corelib/src/odometry/OdometryORBSLAM3.cpp b/corelib/src/odometry/OdometryORBSLAM3.cpp index 73d47e43..f0e5d644 100644 --- a/corelib/src/odometry/OdometryORBSLAM3.cpp +++ b/corelib/src/odometry/OdometryORBSLAM3.cpp @@ -430,7 +430,14 @@ Transform OdometryORBSLAM3::computeTransform( (data.stereoCameraModels().size() == 1 && data.stereoCameraModels()[0].isValidForProjection()))) { - UERROR("Invalid camera model!"); + if(data.cameraModels().size() > 1 || data.stereoCameraModels().size() > 1) + { + UERROR("Multi-camera not supported with ORB_SLAM integration!"); + } + else + { + UERROR("Invalid camera model!"); + } return t; } diff --git a/corelib/src/optimizer/OptimizerG2O.cpp b/corelib/src/optimizer/OptimizerG2O.cpp index 0a869c10..4534e346 100644 --- a/corelib/src/optimizer/OptimizerG2O.cpp +++ b/corelib/src/optimizer/OptimizerG2O.cpp @@ -61,8 +61,6 @@ typedef Eigen::Matrix Matr #include "g2o/types/slam3d/types_slam3d.h" #include "g2o/edge_se3_xyzprior.h" // Include after types_slam3d.h to be ignored on newest g2o versions #include "g2o/edge_se3_gravity.h" -#include "g2o/edge_sbacam_gravity.h" -#include "g2o/edge_sbacam_prior.h" #include "g2o/edge_xy_prior.h" // Include after types_slam2d.h to be ignored on newest g2o versions #include "g2o/edge_xyz_prior.h" // Include after types_slam3d.h to be ignored on newest g2o versions #ifdef G2O_HAVE_CSPARSE @@ -78,6 +76,19 @@ typedef Eigen::Matrix Matr #include "g2o/types/types_sba.h" #include "g2o/types/types_six_dof_expmap.h" #include "g2o/solvers/linear_solver_eigen.h" +#include "g2o/edge_se3_expmap.h" +#endif + +#if defined(RTABMAP_G2O) || defined(RTABMAP_ORB_SLAM) +namespace rtabmap { +#ifdef RTABMAP_ORB_SLAM +typedef g2o::VertexSE3Expmap VertexCam; +#else +typedef g2o::VertexCam VertexCam; +#endif +} +#include "g2o/edge_sbacam_gravity.h" +#include "g2o/edge_sbacam_prior.h" #endif typedef g2o::BlockSolver< g2o::BlockSolverTraits<-1, -1> > SlamBlockSolver; @@ -1415,81 +1426,6 @@ std::map OptimizerG2O::optimize( return optimizedPoses; } -#ifdef RTABMAP_ORB_SLAM -/** - * \brief 3D edge between two SBAcam - */ - class EdgeSE3Expmap : public g2o::BaseBinaryEdge<6, g2o::SE3Quat, g2o::VertexSE3Expmap, g2o::VertexSE3Expmap> -{ - public: - EIGEN_MAKE_ALIGNED_OPERATOR_NEW; - EdgeSE3Expmap(): BaseBinaryEdge<6, g2o::SE3Quat, g2o::VertexSE3Expmap, g2o::VertexSE3Expmap>(){} - bool read(std::istream& is) - { - return false; - } - - bool write(std::ostream& os) const - { - return false; - } - - void computeError() - { - const g2o::VertexSE3Expmap* v1 = dynamic_cast(_vertices[0]); - const g2o::VertexSE3Expmap* v2 = dynamic_cast(_vertices[1]); - g2o::SE3Quat delta = _inverseMeasurement * (v1->estimate().inverse()*v2->estimate()); - _error[0]=delta.translation().x(); - _error[1]=delta.translation().y(); - _error[2]=delta.translation().z(); - _error[3]=delta.rotation().x(); - _error[4]=delta.rotation().y(); - _error[5]=delta.rotation().z(); - } - - virtual void setMeasurement(const g2o::SE3Quat& meas){ - _measurement=meas; - _inverseMeasurement=meas.inverse(); - } - - virtual double initialEstimatePossible(const g2o::OptimizableGraph::VertexSet& , g2o::OptimizableGraph::Vertex* ) { return 1.;} - virtual void initialEstimate(const g2o::OptimizableGraph::VertexSet& from_, g2o::OptimizableGraph::Vertex* ){ - g2o::VertexSE3Expmap* from = static_cast(_vertices[0]); - g2o::VertexSE3Expmap* to = static_cast(_vertices[1]); - if (from_.count(from) > 0) - to->setEstimate((g2o::SE3Quat) from->estimate() * _measurement); - else - from->setEstimate((g2o::SE3Quat) to->estimate() * _inverseMeasurement); - } - - virtual bool setMeasurementData(const double* d){ - Eigen::Map v(d); - _measurement.fromVector(v); - _inverseMeasurement = _measurement.inverse(); - return true; - } - - virtual bool getMeasurementData(double* d) const{ - Eigen::Map v(d); - v = _measurement.toVector(); - return true; - } - - virtual int measurementDimension() const {return 7;} - - virtual bool setMeasurementFromState() { - const g2o::VertexSE3Expmap* v1 = dynamic_cast(_vertices[0]); - const g2o::VertexSE3Expmap* v2 = dynamic_cast(_vertices[1]); - _measurement = (v1->estimate().inverse()*v2->estimate()); - _inverseMeasurement = _measurement.inverse(); - return true; - } - - protected: - g2o::SE3Quat _inverseMeasurement; -}; -#endif - std::map OptimizerG2O::optimizeBA( int rootId, const std::map & poses, @@ -1588,7 +1524,6 @@ std::map OptimizerG2O::optimizeBA( // detect if there are gravity constraints bool hasGravityConstraints = false; -#ifndef RTABMAP_ORB_SLAM if(!isSlam2d() && gravitySigma() > 0) { for(std::multimap::const_iterator iter=links.begin(); iter!=links.end(); ++iter) @@ -1601,7 +1536,6 @@ std::map OptimizerG2O::optimizeBA( } } } -#endif UDEBUG("fill %ld poses to g2o... (rootId=%d hasGravityConstraints=%d isSlam2d=%d)", poses.size(), rootId, hasGravityConstraints?1:0, isSlam2d()?1:0); @@ -1620,11 +1554,8 @@ std::map OptimizerG2O::optimizeBA( // Add node's pose UASSERT(!camPose.isNull()); -#ifdef RTABMAP_ORB_SLAM - g2o::VertexSE3Expmap * vCam = new g2o::VertexSE3Expmap(); -#else - g2o::VertexCam * vCam = new g2o::VertexCam(); -#endif + + rtabmap::VertexCam * vCam = new rtabmap::VertexCam(); Eigen::Affine3d a = camPose.toEigen3d(); #ifdef RTABMAP_ORB_SLAM @@ -1732,7 +1663,6 @@ std::map OptimizerG2O::optimizeBA( if(id1 == id2) { -#ifndef RTABMAP_ORB_SLAM g2o::HyperGraph::Edge * edge = 0; if(gravitySigma() > 0 && iter->second.type() == Link::kGravity && poses.find(iter->first) != poses.end()) { @@ -1746,7 +1676,7 @@ std::map OptimizerG2O::optimizeBA( Eigen::MatrixXd information = Eigen::MatrixXd::Identity(3, 3) * 1.0/(gravitySigma()*gravitySigma()); - g2o::VertexCam* v1 = (g2o::VertexCam*)optimizer.vertex(id1*MULTICAM_OFFSET); + rtabmap::VertexCam* v1 = (rtabmap::VertexCam*)optimizer.vertex(id1*MULTICAM_OFFSET); EdgeSBACamGravity* priorEdge(new EdgeSBACamGravity()); std::map >::const_iterator iterModel = models.find(iter->first); // Gravity constraint added only to first camera of a pose @@ -1763,7 +1693,6 @@ std::map OptimizerG2O::optimizeBA( UERROR("Map: Failed adding constraint between %d and %d, skipping", id1, id2); return optimizedPoses; } -#endif } else if(id1>0 && id2>0) // not supporting landmarks { @@ -1919,14 +1848,14 @@ std::map OptimizerG2O::optimizeBA( g2o::OptimizableGraph::Edge * e; double baseline = 0.0; + rtabmap::VertexCam* vcam = dynamic_cast(optimizer.vertex(camId)); #ifdef RTABMAP_ORB_SLAM - g2o::VertexSE3Expmap* vcam = dynamic_cast(optimizer.vertex(camId)); + std::map >::const_iterator iterModel = models.find(poseId); - UASSERT(iterModel != models.end() && camIndexsecond.size() && iterModel->second[camIndex].isValidForProjection()); + UASSERT(iterModel != models.end() && camIndex<(int)iterModel->second.size() && iterModel->second[camIndex].isValidForProjection()); baseline = iterModel->second[camIndex].Tx()<0.0?-iterModel->second[camIndex].Tx()/iterModel->second[camIndex].fx():baseline_; #else - g2o::VertexCam* vcam = dynamic_cast(optimizer.vertex(camId)); baseline = vcam->estimate().baseline; #endif double variance = pixelVariance_; diff --git a/corelib/src/optimizer/g2o/edge_sbacam_gravity.h b/corelib/src/optimizer/g2o/edge_sbacam_gravity.h index 2559df2b..9f146344 100644 --- a/corelib/src/optimizer/g2o/edge_sbacam_gravity.h +++ b/corelib/src/optimizer/g2o/edge_sbacam_gravity.h @@ -32,14 +32,18 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #ifndef RTAB_G2O_EDGE_SBACAM_GRAVITY_H_ #define RTAB_G2O_EDGE_SBACAM_GRAVITY_H_ +#ifdef RTABMAP_ORB_SLAM +#include "g2o/types/types_six_dof_expmap.h" +#else #include "g2o/types/sba/types_sba.h" +#endif #include "g2o/core/base_unary_edge.h" namespace rtabmap { /** * \brief EdgeSBACamGravity * \brief g2o edge with gravity constraint */ -class EdgeSBACamGravity : public g2o::BaseUnaryEdge<3, Eigen::Matrix, g2o::VertexCam> { +class EdgeSBACamGravity : public g2o::BaseUnaryEdge<3, Eigen::Matrix, VertexCam> { public: EIGEN_MAKE_ALIGNED_OPERATOR_NEW EdgeSBACamGravity(){ @@ -56,30 +60,36 @@ class EdgeSBACamGravity : public g2o::BaseUnaryEdge<3, Eigen::Matrix(_vertices[0]); + const VertexCam* v = static_cast(_vertices[0]); - Eigen::Vector3d direction = _measurement.head<3>(); - Eigen::Vector3d measurement = _measurement.tail<3>(); + Eigen::Vector3d direction = _measurement.head<3>(); + Eigen::Vector3d measurement = _measurement.tail<3>(); - Eigen::Vector3d ea; + g2o::SE3Quat estimate; +#ifdef RTABMAP_ORB_SLAM + estimate = v->estimate().inverse(); +#else + estimate = v->estimate(); +#endif - // Transform pose from camera frame to world frame - Eigen::Matrix3d t = v1->estimate().rotation().toRotationMatrix() * cameraInvLocalTransform_; - ea[0] = atan2(t (2, 1), t (2, 2)); - ea[1] = asin(-t (2, 0)); - ea[2] = atan2(t (1, 0), t (0, 0)); + // Transform pose from camera frame to world frame + Eigen::Matrix3d t = estimate.rotation().toRotationMatrix() * cameraInvLocalTransform_; + Eigen::Vector3d ea; + ea[0] = atan2(t (2, 1), t (2, 2)); + ea[1] = asin(-t (2, 0)); + ea[2] = atan2(t (1, 0), t (0, 0)); - Eigen::Matrix3d rot = - (Eigen::AngleAxisd(ea[1], Eigen::Vector3d::UnitY()) * - Eigen::AngleAxisd(ea[0], Eigen::Vector3d::UnitX())).toRotationMatrix(); + Eigen::Matrix3d rot = + (Eigen::AngleAxisd(ea[1], Eigen::Vector3d::UnitY()) * + Eigen::AngleAxisd(ea[0], Eigen::Vector3d::UnitX())).toRotationMatrix(); - Eigen::Vector3d estimate = rot * -direction; - _error = estimate - measurement; + Eigen::Vector3d newEstimate = rot * -direction; + _error = newEstimate - measurement; - /*printf("%d : measured=%f %f %f est=%f %f %f error=%f %f %f\n", v1->id(), - measurement[0], measurement[1], measurement[2], - estimate[0], estimate[1], estimate[2], - _error[0], _error[1], _error[2]);*/ + /*printf("%d : measured=%f %f %f est=%f %f %f error=%f %f %f\n", v1->id(), + measurement[0], measurement[1], measurement[2], + estimate[0], estimate[1], estimate[2], + _error[0], _error[1], _error[2]);*/ } // 6 values: diff --git a/corelib/src/optimizer/g2o/edge_sbacam_prior.h b/corelib/src/optimizer/g2o/edge_sbacam_prior.h index 7fadb971..3976e2e8 100644 --- a/corelib/src/optimizer/g2o/edge_sbacam_prior.h +++ b/corelib/src/optimizer/g2o/edge_sbacam_prior.h @@ -32,14 +32,18 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #ifndef RTAB_G2O_EDGE_SBACAM_PRIOR_H_ #define RTAB_G2O_EDGE_SBACAM_PRIOR_H_ +#ifdef RTABMAP_ORB_SLAM +#include "g2o/types/types_six_dof_expmap.h" +#else #include "g2o/types/sba/types_sba.h" +#endif #include "g2o/core/base_unary_edge.h" namespace rtabmap { /** * \brief EdgeSBACamPrior * \brief g2o edge with gravity constraint */ -class EdgeSBACamPrior : public g2o::BaseUnaryEdge<6, g2o::SE3Quat, g2o::VertexCam> { +class EdgeSBACamPrior : public g2o::BaseUnaryEdge<6, g2o::SE3Quat, VertexCam> { public: EIGEN_MAKE_ALIGNED_OPERATOR_NEW EdgeSBACamPrior() { @@ -54,8 +58,14 @@ class EdgeSBACamPrior : public g2o::BaseUnaryEdge<6, g2o::SE3Quat, g2o::VertexCa // return the error estimate as a 3-vector void computeError() { - const g2o::VertexCam* v = static_cast(_vertices[0]); - g2o::SE3Quat delta = _inverseMeasurement * v->estimate() * _cameraInvLocalTransform; + const VertexCam* v = static_cast(_vertices[0]); + g2o::SE3Quat estimate; +#ifdef RTABMAP_ORB_SLAM + estimate = v->estimate().inverse(); +#else + estimate = v->estimate(); +#endif + g2o::SE3Quat delta = _inverseMeasurement * estimate * _cameraInvLocalTransform; _error[0]=delta.translation().x(); _error[1]=delta.translation().y(); _error[2]=delta.translation().z(); @@ -97,10 +107,14 @@ class EdgeSBACamPrior : public g2o::BaseUnaryEdge<6, g2o::SE3Quat, g2o::VertexCa } virtual void initialEstimate(const g2o::OptimizableGraph::VertexSet& from, g2o::OptimizableGraph::Vertex* to) { - g2o::VertexCam *v = static_cast(_vertices[0]); + VertexCam *v = static_cast(_vertices[0]); assert(v && "Vertex for the Prior edge is not set"); +#ifdef RTABMAP_ORB_SLAM + g2o::SE3Quat newEstimate = _cameraInvLocalTransform * _inverseMeasurement; +#else g2o::SE3Quat newEstimate = measurement()*_cameraInvLocalTransform.inverse(); +#endif if (_information.block<3,3>(0,0).array().abs().sum() == 0){ // do not set translation, as that part of the information is all zero newEstimate.setTranslation(v->estimate().translation()); } diff --git a/corelib/src/optimizer/g2o/edge_se3_expmap.h b/corelib/src/optimizer/g2o/edge_se3_expmap.h new file mode 100644 index 00000000..5bad8278 --- /dev/null +++ b/corelib/src/optimizer/g2o/edge_se3_expmap.h @@ -0,0 +1,74 @@ +#include "g2o/types/types_six_dof_expmap.h" + +/** + * \brief 3D edge between two SBAcam + */ + class EdgeSE3Expmap : public g2o::BaseBinaryEdge<6, g2o::SE3Quat, g2o::VertexSE3Expmap, g2o::VertexSE3Expmap> +{ + public: + EIGEN_MAKE_ALIGNED_OPERATOR_NEW; + EdgeSE3Expmap(): BaseBinaryEdge<6, g2o::SE3Quat, g2o::VertexSE3Expmap, g2o::VertexSE3Expmap>(){} + bool read(std::istream& is) + { + return false; + } + + bool write(std::ostream& os) const + { + return false; + } + + void computeError() + { + const g2o::VertexSE3Expmap* v1 = dynamic_cast(_vertices[0]); + const g2o::VertexSE3Expmap* v2 = dynamic_cast(_vertices[1]); + g2o::SE3Quat delta = _inverseMeasurement * (v1->estimate().inverse()*v2->estimate()); + _error[0]=delta.translation().x(); + _error[1]=delta.translation().y(); + _error[2]=delta.translation().z(); + _error[3]=delta.rotation().x(); + _error[4]=delta.rotation().y(); + _error[5]=delta.rotation().z(); + } + + virtual void setMeasurement(const g2o::SE3Quat& meas){ + _measurement=meas; + _inverseMeasurement=meas.inverse(); + } + + virtual double initialEstimatePossible(const g2o::OptimizableGraph::VertexSet& , g2o::OptimizableGraph::Vertex* ) { return 1.;} + virtual void initialEstimate(const g2o::OptimizableGraph::VertexSet& from_, g2o::OptimizableGraph::Vertex* ){ + g2o::VertexSE3Expmap* from = static_cast(_vertices[0]); + g2o::VertexSE3Expmap* to = static_cast(_vertices[1]); + if (from_.count(from) > 0) + to->setEstimate((g2o::SE3Quat) from->estimate() * _measurement); + else + from->setEstimate((g2o::SE3Quat) to->estimate() * _inverseMeasurement); + } + + virtual bool setMeasurementData(const double* d){ + Eigen::Map v(d); + _measurement.fromVector(v); + _inverseMeasurement = _measurement.inverse(); + return true; + } + + virtual bool getMeasurementData(double* d) const{ + Eigen::Map v(d); + v = _measurement.toVector(); + return true; + } + + virtual int measurementDimension() const {return 7;} + + virtual bool setMeasurementFromState() { + const g2o::VertexSE3Expmap* v1 = dynamic_cast(_vertices[0]); + const g2o::VertexSE3Expmap* v2 = dynamic_cast(_vertices[1]); + _measurement = (v1->estimate().inverse()*v2->estimate()); + _inverseMeasurement = _measurement.inverse(); + return true; + } + + protected: + g2o::SE3Quat _inverseMeasurement; +}; \ No newline at end of file diff --git a/guilib/include/rtabmap/gui/StatsToolBox.h b/guilib/include/rtabmap/gui/StatsToolBox.h index b08b7cdf..c230d273 100644 --- a/guilib/include/rtabmap/gui/StatsToolBox.h +++ b/guilib/include/rtabmap/gui/StatsToolBox.h @@ -32,6 +32,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include +#include class QToolButton; class QLabel; @@ -59,6 +60,7 @@ public: public Q_SLOTS: void updateMenu(const QMenu * menu); + void updateLabel(); Q_SIGNALS: void valueAdded(qreal); @@ -117,6 +119,8 @@ Q_SIGNALS: private Q_SLOTS: void plot(const StatItem * stat, const QString & plotName = QString()); void figureDeleted(QObject * obj); + void requestLabelsUpdate(); + void updateLabels(); protected: virtual void contextMenuEvent(QContextMenuEvent * event); @@ -127,6 +131,7 @@ private: QString _workingDirectory; int _newFigureMaxItems; QMap _figures; + QTimer _updateLabelsTimer; }; } diff --git a/guilib/src/StatsToolBox.cpp b/guilib/src/StatsToolBox.cpp index 54933e94..cf6d0fbe 100644 --- a/guilib/src/StatsToolBox.cpp +++ b/guilib/src/StatsToolBox.cpp @@ -69,6 +69,7 @@ StatItem::StatItem(const QString & name, bool cacheOn, const std::vector _y = y; } _unit->setText(unit); + _value->setTextFormat(Qt::PlainText); this->updateMenu(menu); } @@ -90,7 +91,6 @@ void StatItem::addValue(qreal y) { _y.push_back(y); } - _value->setText(QString::number(y, 'g', 3)); Q_EMIT valueAdded(y); } @@ -107,7 +107,6 @@ void StatItem::addValue(qreal x, qreal y) _x.push_back(x); } - _value->setText(QString::number(y, 'g', 3)); Q_EMIT valueAdded(x,y); } @@ -117,18 +116,23 @@ void StatItem::setValues(const std::vector & x, const std::vector { _x = x; _y = y; - if(y.size()) - { - _value->setNum(y[y.size()-1]); - } - } - else - { - _value->setText("*"); } Q_EMIT valuesChanged(x,y); } +void StatItem::updateLabel() +{ + QString newText; + if(_y.size()) + { + newText = QString::number(_y.back(), 'g', 3); + } + if(newText != _value->text()) + { + _value->setText(newText); + } +} + QString StatItem::value() const { return _value->text(); @@ -227,6 +231,8 @@ StatsToolBox::StatsToolBox(QWidget * parent) : _plotMenu->addAction(tr("")); _workingDirectory = QDir::homePath(); _newFigureMaxItems = 0; + _updateLabelsTimer.setSingleShot(true); + connect(&_updateLabelsTimer, &QTimer::timeout, this, &StatsToolBox::updateLabels); } StatsToolBox::~StatsToolBox() @@ -263,6 +269,7 @@ void StatsToolBox::updateStat(const QString & statFullName, qreal y, bool cacheO std::vector vx,vy(1); vy[0] = y; updateStat(statFullName, vx, vy, cacheOn); + requestLabelsUpdate(); } void StatsToolBox::updateStat(const QString & statFullName, qreal x, qreal y, bool cacheOn) @@ -271,6 +278,7 @@ void StatsToolBox::updateStat(const QString & statFullName, qreal x, qreal y, bo vx[0] = x; vy[0] = y; updateStat(statFullName, vx, vy, cacheOn); + requestLabelsUpdate(); } void StatsToolBox::updateStat(const QString & statFullName, const std::vector & x, const std::vector & y, bool cacheOn) @@ -295,6 +303,7 @@ void StatsToolBox::updateStat(const QString & statFullName, const std::vectorsetValues(x, y); } + requestLabelsUpdate(); } else { @@ -378,6 +387,22 @@ void StatsToolBox::updateStat(const QString & statFullName, const std::vector items = _statBox->findChildren(); + for(int i=0; iupdateLabel(); + } +} + void StatsToolBox::plot(const StatItem * stat, const QString & plotName) { QWidget * fig = _figures.value(plotName, (QWidget*)0); From a9f63bd5fdd6ca469f6323b664c8ee0b8279256f Mon Sep 17 00:00:00 2001 From: Abhijith <57417062+abhi2198@users.noreply.github.com> Date: Sun, 12 Apr 2026 20:06:44 -0500 Subject: [PATCH 096/126] Add LIO-SAM as an odometry strategy (#1684) * Added liosam odometry integration * Add kXYZIRT scan format with per-point ring channel for LIO-SAM integration Introduce PointXYZIRT point type and kXYZIRT LaserScan format (x,y,z, intensity,ring,time) so that ring indices survive the scan pipeline. Update OdometryLIOSAM to require kXYZIRT and properly split ring/time into the parallel buffers LIO-SAM expects. Extend deskewing to preserve ring data and disable base-class deskew in OdometryLIOSAM since LIO-SAM handles it internally. * Address PR review: config file, deferred init, and GUI panel for LIO-SAM - Add OdomLIOSAM/ConfigPath parameter to load LIO-SAM settings from a YAML file. When set, individual params are ignored. Extrinsics from sensor local transforms always override config file values. - Defer LioSamCore initialization until both IMU and lidar local transforms are available, computing T_lidar_imu from sensor data. IMU samples are buffered and replayed after init. - Fix deferred init for scan-only messages that arrive after IMU local transform is already cached. - Add LIO-SAM entry to odometry strategy combo box (index 14) with full PreferencesDialog panel including config path browse button and all parameter widgets. * OdometryLIOSAM: propagate deskewed scan to SensorData Capture the deskewed cloud produced by LIO-SAM's image projection stage and replace the raw scan on SensorData with it, so loop closure registration and other downstream stages operate on the motion- compensated points instead of the raw pre-deskew input. * Minor updates for rtabmap_ros --------- Co-authored-by: matlabbe --- CMakeLists.txt | 17 + Version.h.in | 1 + corelib/include/rtabmap/core/LaserScan.h | 8 +- corelib/include/rtabmap/core/Odometry.h | 3 +- corelib/include/rtabmap/core/Parameters.h | 17 +- corelib/include/rtabmap/core/impl/util3d.hpp | 99 +++- .../rtabmap/core/odometry/OdometryLIOSAM.h | 82 +++ corelib/include/rtabmap/core/util3d.h | 26 + corelib/src/CMakeLists.txt | 13 + corelib/src/LaserScan.cpp | 14 +- corelib/src/Odometry.cpp | 4 + corelib/src/Parameters.cpp | 6 + corelib/src/odometry/OdometryLIOSAM.cpp | 478 ++++++++++++++++++ corelib/src/util3d.cpp | 82 ++- .../include/rtabmap/gui/PreferencesDialog.h | 1 + guilib/src/PreferencesDialog.cpp | 37 ++ guilib/src/ui/preferencesDialog.ui | 457 +++++++++++++++++ 17 files changed, 1316 insertions(+), 29 deletions(-) create mode 100644 corelib/include/rtabmap/core/odometry/OdometryLIOSAM.h create mode 100644 corelib/src/odometry/OdometryLIOSAM.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ab2e396a..b2f3f51b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -192,6 +192,7 @@ option(WITH_CCCORELIB "Include CCCoreLib support" OFF) option(WITH_OPEN3D "Include Open3D support" OFF) option(WITH_LOAM "Include LOAM support" OFF) option(WITH_FLOAM "Include FLOAM support" OFF) +option(WITH_LIOSAM "Include LIO-SAM support" OFF) option(WITH_FLYCAPTURE2 "Include FlyCapture2/Triclops support" ON) option(WITH_ZED "Include ZED sdk support" ON) option(WITH_ZEDOC "Include ZED Open Capture support" ON) @@ -630,6 +631,12 @@ IF(WITH_FLOAM) FIND_PACKAGE(Ceres REQUIRED) ENDIF(floam_FOUND) ENDIF(WITH_FLOAM) +IF(WITH_LIOSAM) + find_package(lio_sam QUIET) + IF(lio_sam_FOUND) + MESSAGE(STATUS "Found lio_sam: ${lio_sam_INCLUDE_DIRS}") + ENDIF(lio_sam_FOUND) +ENDIF(WITH_LIOSAM) SET(ZED_FOUND FALSE) IF(WITH_ZED) @@ -1069,6 +1076,9 @@ ENDIF(NOT loam_velodyne_FOUND) IF(NOT floam_FOUND) SET(FLOAM "//") ENDIF(NOT floam_FOUND) +IF(NOT lio_sam_FOUND) + SET(LIOSAM "//") +ENDIF(NOT lio_sam_FOUND) IF(NOT Freenect_FOUND) SET(FREENECT "//") ENDIF() @@ -1873,6 +1883,13 @@ MESSAGE(STATUS " With floam = NO (WITH_FLOAM=OFF)") ELSE() MESSAGE(STATUS " With floam = NO (floam not found)") ENDIF() +IF(lio_sam_FOUND) +MESSAGE(STATUS " With lio_sam = YES (License: BSD)") +ELSEIF(NOT WITH_LIOSAM) +MESSAGE(STATUS " With lio_sam = NO (WITH_LIOSAM=OFF)") +ELSE() +MESSAGE(STATUS " With lio_sam = NO (lio_sam not found)") +ENDIF() IF(libfovis_FOUND) MESSAGE(STATUS " With libfovis = YES (License: GPLv2)") diff --git a/Version.h.in b/Version.h.in index faebf7c2..77b5e6f7 100644 --- a/Version.h.in +++ b/Version.h.in @@ -63,6 +63,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. @CUDASIFT@#define RTABMAP_CUDASIFT @LOAM@#define RTABMAP_LOAM @FLOAM@#define RTABMAP_FLOAM +@LIOSAM@#define RTABMAP_LIOSAM @DC1394@#define RTABMAP_DC1394 @FLYCAPTURE2@#define RTABMAP_FLYCAPTURE2 @ZED@#define RTABMAP_ZED diff --git a/corelib/include/rtabmap/core/LaserScan.h b/corelib/include/rtabmap/core/LaserScan.h index 0e76fdcb..8e2e4f02 100644 --- a/corelib/include/rtabmap/core/LaserScan.h +++ b/corelib/include/rtabmap/core/LaserScan.h @@ -48,7 +48,8 @@ public: kXYZNormal=8, kXYZINormal=9, kXYZRGBNormal=10, - kXYZIT=11}; + kXYZIT=11, + kXYZIRT=12}; static std::string formatName(const Format & format); static int channels(const Format & format); @@ -57,6 +58,7 @@ public: static bool isScanHasRGB(const Format & format); static bool isScanHasIntensity(const Format & format); static bool isScanHasTime(const Format & format); + static bool isScanHasRing(const Format & format); static LaserScan backwardCompatibility( const cv::Mat & oldScanFormat, int maxPoints = 0, @@ -135,6 +137,7 @@ public: bool hasRGB() const {return isScanHasRGB(format_);} bool hasIntensity() const {return isScanHasIntensity(format_);} bool hasTime() const {return isScanHasTime(format_);} + bool hasRing() const {return isScanHasRing(format_);} bool isCompressed() const {return !data_.empty() && data_.type()==CV_8UC1;} bool isOrganized() const {return data_.rows > 1;} LaserScan clone() const; @@ -143,7 +146,8 @@ public: int getIntensityOffset() const {return hasIntensity()?(is2d()?2:3):-1;} int getRGBOffset() const {return hasRGB()?(is2d()?2:3):-1;} int getNormalsOffset() const {return hasNormals()?(2 + (is2d()?0:1) + ((hasRGB() || hasIntensity())?1:0)):-1;} - int getTimeOffset() const {return hasTime()?4:-1;} + int getRingOffset() const {return format_==kXYZIRT?4:-1;} + int getTimeOffset() const {return format_==kXYZIT?4:(format_==kXYZIRT?5:-1);} float & field(unsigned int pointIndex, unsigned int channelOffset); diff --git a/corelib/include/rtabmap/core/Odometry.h b/corelib/include/rtabmap/core/Odometry.h index f682afca..463f56b0 100644 --- a/corelib/include/rtabmap/core/Odometry.h +++ b/corelib/include/rtabmap/core/Odometry.h @@ -57,7 +57,8 @@ public: kTypeOpenVINS = 10, kTypeFLOAM = 11, kTypeOpen3D = 12, - kTypeCuVSLAM = 13 + kTypeCuVSLAM = 13, + kTypeLIOSAM = 14 }; public: diff --git a/corelib/include/rtabmap/core/Parameters.h b/corelib/include/rtabmap/core/Parameters.h index e0ae0e92..d51c8043 100644 --- a/corelib/include/rtabmap/core/Parameters.h +++ b/corelib/include/rtabmap/core/Parameters.h @@ -465,7 +465,7 @@ class RTABMAP_CORE_EXPORT Parameters RTABMAP_PARAM(GTSAM, IncRelinearizeSkip, int, 1, "Only relinearize any variables every X calls to ISAM2::update(). See GTSAM::ISAM2 doc for more info."); // Odometry - RTABMAP_PARAM(Odom, Strategy, int, 0, "0=Frame-to-Map (F2M) 1=Frame-to-Frame (F2F) 2=Fovis 3=viso2 4=DVO-SLAM 5=ORB_SLAM 6=OKVIS 7=LOAM 8=MSCKF_VIO 9=VINS-Fusion 10=OpenVINS 11=FLOAM 12=Open3D 13=cuVSLAM"); + RTABMAP_PARAM(Odom, Strategy, int, 0, "0=Frame-to-Map (F2M) 1=Frame-to-Frame (F2F) 2=Fovis 3=viso2 4=DVO-SLAM 5=ORB_SLAM 6=OKVIS 7=LOAM 8=MSCKF_VIO 9=VINS-Fusion 10=OpenVINS 11=FLOAM 12=Open3D 13=cuVSLAM 14=LIO-SAM"); RTABMAP_PARAM(Odom, ResetCountdown, int, 0, "Automatically reset odometry after X consecutive images where odometry cannot be computed (a value of 0 disables auto-reset). When a reset occurs, odometry resumes from the last successfully computed pose with large covariance to trigger a new map. If external odometry is used, it will also be reset based on the motion estimated relative to the last computed pose but no large covariance will be received, so that a new map won't be triggered."); RTABMAP_PARAM(Odom, Holonomic, bool, true, "If the robot is holonomic (strafing commands can be issued). If not, y value will be estimated from x and yaw values (y=x*tan(yaw))."); RTABMAP_PARAM(Odom, FillInfoData, bool, true, "Fill info with data (inliers/outliers features)."); @@ -687,6 +687,21 @@ class RTABMAP_CORE_EXPORT Parameters // Odometry cuVSLAM RTABMAP_PARAM(OdomCuVSLAM, MulticamMode, int, 0, "cuVSLAM multicam_mode setting: 0=moderate, 1=performance, 2=precision."); + // Odometry LIO-SAM + RTABMAP_PARAM_STR(OdomLIOSAM, ConfigPath, "", "Path to LIO-SAM params.yaml config file. When set, sensor/IMU/feature parameters are loaded from the file and the individual parameters below are ignored."); + RTABMAP_PARAM(OdomLIOSAM, Sensor, int, 0, "LiDAR sensor: 0=Velodyne, 1=Ouster, 2=Livox"); + RTABMAP_PARAM(OdomLIOSAM, NScan, int, 16, "Number of LiDAR channels (16, 32, 64, 128)."); + RTABMAP_PARAM(OdomLIOSAM, HorizonScan, int, 1800, "Horizontal resolution (Velodyne:1800, Ouster:512/1024/2048)."); + RTABMAP_PARAM(OdomLIOSAM, ImuAccNoise, float, 0.01, "IMU accelerometer white noise."); + RTABMAP_PARAM(OdomLIOSAM, ImuGyrNoise, float, 0.001, "IMU gyroscope white noise."); + RTABMAP_PARAM(OdomLIOSAM, ImuAccBiasN, float, 0.0002,"IMU accelerometer bias noise."); + RTABMAP_PARAM(OdomLIOSAM, ImuGyrBiasN, float, 0.00003,"IMU gyroscope bias noise."); + RTABMAP_PARAM(OdomLIOSAM, ImuGravity, float, 9.80511,"Gravity magnitude."); + RTABMAP_PARAM(OdomLIOSAM, EdgeThreshold,float, 1.0, "Edge feature curvature threshold."); + RTABMAP_PARAM(OdomLIOSAM, SurfThreshold,float, 0.1, "Surface feature curvature threshold."); + RTABMAP_PARAM(OdomLIOSAM, LinVar, float, 0.01, "Linear output variance."); + RTABMAP_PARAM(OdomLIOSAM, AngVar, float, 0.01, "Angular output variance."); + // Common registration parameters RTABMAP_PARAM(Reg, RepeatOnce, bool, true, "Do a second registration with the output of the first registration as guess. Only done if no guess was provided for the first registration (like on loop closure). It can be useful if the registration approach used can use a guess to get better matches."); RTABMAP_PARAM(Reg, Strategy, int, 0, "0=Vis, 1=Icp, 2=VisIcp"); diff --git a/corelib/include/rtabmap/core/impl/util3d.hpp b/corelib/include/rtabmap/core/impl/util3d.hpp index 1e9e23cb..11fd55aa 100644 --- a/corelib/include/rtabmap/core/impl/util3d.hpp +++ b/corelib/include/rtabmap/core/impl/util3d.hpp @@ -41,11 +41,11 @@ LaserScan laserScanFromPointCloud(const PointCloud2T & cloud, bool filterNaNs, b return LaserScan(); } //determine the output type - int fieldStates[8] = {0}; // x,y,z,normal_x,normal_y,normal_z,rgb,intensity + int fieldStates[10] = {0}; // x,y,z,normal_x,normal_y,normal_z,rgb,intensity,time,ring #if PCL_VERSION_COMPARE(>=, 1, 10, 0) - std::uint32_t fieldOffsets[8] = {0}; + std::uint32_t fieldOffsets[10] = {0}; #else - pcl::uint32_t fieldOffsets[8] = {0}; + pcl::uint32_t fieldOffsets[10] = {0}; #endif for(unsigned int i=0; i + +#ifdef RTABMAP_LIOSAM +#include +#include +#include +namespace lio_sam { class LioSamCore; } +#endif + +namespace rtabmap { + +class RTABMAP_CORE_EXPORT OdometryLIOSAM : public Odometry +{ +public: + OdometryLIOSAM(const rtabmap::ParametersMap & parameters = rtabmap::ParametersMap()); + virtual ~OdometryLIOSAM(); + + virtual void reset(const Transform & initialPose = Transform::getIdentity()); + virtual Odometry::Type getType() {return Odometry::kTypeLIOSAM;} + virtual bool canProcessAsyncIMU() const {return true;} + +private: + virtual Transform computeTransform(SensorData & data, const Transform & guess = Transform(), OdometryInfo * info = 0); + +#ifdef RTABMAP_LIOSAM + bool init(const Transform & imuLocalTransform, const Transform & lidarLocalTransform); +#endif + +private: +#ifdef RTABMAP_LIOSAM + lio_sam::LioSamCore * lioSam_; + Transform lastPose_; + bool lost_; + float linVar_; + float angVar_; + ParametersMap parameters_; + Transform imuLocalTransform_; // base_link -> imu_link (cached for deferred init) + + // Buffered IMU samples received before initialization + struct ImuSample { + double stamp; + Eigen::Vector3d acc; + Eigen::Vector3d gyro; + Eigen::Quaterniond orientation; + }; + std::vector imuBuffer_; +#endif +}; + +} + +#endif /* ODOMETRYLIOSAM_H_ */ diff --git a/corelib/include/rtabmap/core/util3d.h b/corelib/include/rtabmap/core/util3d.h index 58cd82d6..72dc44a5 100644 --- a/corelib/include/rtabmap/core/util3d.h +++ b/corelib/include/rtabmap/core/util3d.h @@ -39,12 +39,26 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include #include #include namespace rtabmap { +// Point type carrying xyz + intensity + ring (laser line index) + time +// (per-point acquisition offset, seconds from the scan start). Matches the +// layout expected by LIO-SAM's Velodyne feature extractor so it can be fed +// directly via util3d::laserScanFromPointCloud(). +struct EIGEN_ALIGN16 PointXYZIRT +{ + PCL_ADD_POINT4D; + float intensity; + std::uint16_t ring; + float time; + EIGEN_MAKE_ALIGNED_OPERATOR_NEW +}; + namespace util3d { @@ -297,6 +311,9 @@ LaserScan RTABMAP_CORE_EXPORT laserScanFromPointCloud(const pcl::PointCloud & cloud, const Transform & transform = Transform(), bool filterNaNs = true); LaserScan RTABMAP_CORE_EXPORT laserScanFromPointCloud(const pcl::PointCloud & cloud, const pcl::IndicesPtr & indices, const Transform & transform = Transform(), bool filterNaNs = true); +// return CV_32FC6 (x,y,z,I,ring,time) +LaserScan RTABMAP_CORE_EXPORT laserScanFromPointCloud(const pcl::PointCloud & cloud, const Transform & transform = Transform(), bool filterNaNs = true); +LaserScan RTABMAP_CORE_EXPORT laserScanFromPointCloud(const pcl::PointCloud & cloud, const pcl::IndicesPtr & indices, const Transform & transform = Transform(), bool filterNaNs = true); // return CV_32FC7 (x,y,z,rgb,normal_x,normal_y,normal_z) LaserScan RTABMAP_CORE_EXPORT laserScanFromPointCloud(const pcl::PointCloud & cloud, const pcl::PointCloud & normals, const Transform & transform = Transform(), bool filterNaNs = true); LaserScan RTABMAP_CORE_EXPORT laserScanFromPointCloud(const pcl::PointCloud & cloud, const Transform & transform = Transform(), bool filterNaNs = true); @@ -512,6 +529,15 @@ LaserScan RTABMAP_CORE_EXPORT deskew( } // namespace util3d } // namespace rtabmap +POINT_CLOUD_REGISTER_POINT_STRUCT(rtabmap::PointXYZIRT, + (float, x, x) + (float, y, y) + (float, z, z) + (float, intensity, intensity) + (std::uint16_t, ring, ring) + (float, time, time) +) + #include "rtabmap/core/impl/util3d.hpp" #endif /* UTIL3D_H_ */ diff --git a/corelib/src/CMakeLists.txt b/corelib/src/CMakeLists.txt index 667b10d7..b275a535 100644 --- a/corelib/src/CMakeLists.txt +++ b/corelib/src/CMakeLists.txt @@ -98,6 +98,7 @@ SET(SRC_FILES odometry/OdometryORBSLAM3.cpp odometry/OdometryLOAM.cpp odometry/OdometryFLOAM.cpp + odometry/OdometryLIOSAM.cpp odometry/OdometryMSCKF.cpp odometry/OdometryVINSFusion.cpp odometry/OdometryOpenVINS.cpp @@ -604,6 +605,18 @@ IF(floam_FOUND) ) ENDIF(floam_FOUND) +IF(lio_sam_FOUND) + SET(INCLUDE_DIRS + ${INCLUDE_DIRS} + ${lio_sam_INCLUDE_DIRS} + ) + link_directories(${lio_sam_LIBRARY_DIRS}) + SET(LIBRARIES + ${LIBRARIES} + lio_sam_core + ) +ENDIF(lio_sam_FOUND) + IF(ZED_FOUND) SET(INCLUDE_DIRS ${INCLUDE_DIRS} diff --git a/corelib/src/LaserScan.cpp b/corelib/src/LaserScan.cpp index 7e875a98..3f136e94 100644 --- a/corelib/src/LaserScan.cpp +++ b/corelib/src/LaserScan.cpp @@ -68,6 +68,9 @@ std::string LaserScan::formatName(const Format & format) case kXYZIT: name = "XYZIT"; break; + case kXYZIRT: + name = "XYZIRT"; + break; default: name = "Unknown"; break; @@ -96,6 +99,7 @@ int LaserScan::channels(const Format & format) break; case kXYZNormal: case kXYINormal: + case kXYZIRT: channels = 6; break; case kXYZINormal: @@ -123,11 +127,15 @@ bool LaserScan::isScanHasRGB(const Format & format) } bool LaserScan::isScanHasIntensity(const Format & format) { - return format==kXYZI || format==kXYZINormal || format == kXYI || format == kXYINormal || format==kXYZIT; + return format==kXYZI || format==kXYZINormal || format == kXYI || format == kXYINormal || format==kXYZIT || format==kXYZIRT; } bool LaserScan::isScanHasTime(const Format & format) { - return format==kXYZIT; + return format==kXYZIT || format==kXYZIRT; +} +bool LaserScan::isScanHasRing(const Format & format) +{ + return format==kXYZIRT; } LaserScan LaserScan::backwardCompatibility( @@ -404,7 +412,7 @@ void LaserScan::init( UASSERT_MSG(data.channels() != 3 || (data.channels() == 3 && (format == kXYZ || format == kXYI)), uFormat("format=%s", LaserScan::formatName(format).c_str()).c_str()); UASSERT_MSG(data.channels() != 4 || (data.channels() == 4 && (format == kXYZI || format == kXYZRGB)), uFormat("format=%s", LaserScan::formatName(format).c_str()).c_str()); UASSERT_MSG(data.channels() != 5 || (data.channels() == 5 && (format == kXYNormal || format == kXYZIT)), uFormat("format=%s", LaserScan::formatName(format).c_str()).c_str()); - UASSERT_MSG(data.channels() != 6 || (data.channels() == 6 && (format == kXYINormal || format == kXYZNormal)), uFormat("format=%s", LaserScan::formatName(format).c_str()).c_str()); + UASSERT_MSG(data.channels() != 6 || (data.channels() == 6 && (format == kXYINormal || format == kXYZNormal || format == kXYZIRT)), uFormat("format=%s", LaserScan::formatName(format).c_str()).c_str()); UASSERT_MSG(data.channels() != 7 || (data.channels() == 7 && (format == kXYZRGBNormal || format == kXYZINormal)), uFormat("format=%s", LaserScan::formatName(format).c_str()).c_str()); } } diff --git a/corelib/src/Odometry.cpp b/corelib/src/Odometry.cpp index 79f4292a..f38ab38c 100644 --- a/corelib/src/Odometry.cpp +++ b/corelib/src/Odometry.cpp @@ -35,6 +35,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/core/odometry/OdometryORBSLAM3.h" #include "rtabmap/core/odometry/OdometryLOAM.h" #include "rtabmap/core/odometry/OdometryFLOAM.h" +#include "rtabmap/core/odometry/OdometryLIOSAM.h" #include "rtabmap/core/odometry/OdometryMSCKF.h" #include "rtabmap/core/odometry/OdometryVINSFusion.h" #include "rtabmap/core/odometry/OdometryOpenVINS.h" @@ -101,6 +102,9 @@ Odometry * Odometry::create(Odometry::Type & type, const ParametersMap & paramet case Odometry::kTypeFLOAM: odometry = new OdometryFLOAM(parameters); break; + case Odometry::kTypeLIOSAM: + odometry = new OdometryLIOSAM(parameters); + break; case Odometry::kTypeMSCKF: odometry = new OdometryMSCKF(parameters); break; diff --git a/corelib/src/Parameters.cpp b/corelib/src/Parameters.cpp index 463478a8..81225ac2 100644 --- a/corelib/src/Parameters.cpp +++ b/corelib/src/Parameters.cpp @@ -903,6 +903,12 @@ ParametersMap Parameters::parseArguments(int argc, char * argv[], bool onlyParam std::cout << str << std::setw(spacing - str.size()) << "true" << std::endl; #else std::cout << str << std::setw(spacing - str.size()) << "false" << std::endl; +#endif + str = "With LIO-SAM:"; +#ifdef RTABMAP_LIOSAM + std::cout << str << std::setw(spacing - str.size()) << "true" << std::endl; +#else + std::cout << str << std::setw(spacing - str.size()) << "false" << std::endl; #endif str = "With FOVIS:"; #ifdef RTABMAP_FOVIS diff --git a/corelib/src/odometry/OdometryLIOSAM.cpp b/corelib/src/odometry/OdometryLIOSAM.cpp new file mode 100644 index 00000000..abd38aa8 --- /dev/null +++ b/corelib/src/odometry/OdometryLIOSAM.cpp @@ -0,0 +1,478 @@ +/* +Copyright (c) 2010-2016, Mathieu Labbe - IntRoLab - Universite de Sherbrooke +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Universite de Sherbrooke nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "rtabmap/core/odometry/OdometryLIOSAM.h" +#include "rtabmap/core/OdometryInfo.h" +#include "rtabmap/core/util3d.h" +#include "rtabmap/utilite/ULogger.h" +#include "rtabmap/utilite/UTimer.h" +#include "rtabmap/utilite/UStl.h" +#include "rtabmap/utilite/UDirectory.h" +#include "rtabmap/utilite/UFile.h" + +#ifdef RTABMAP_LIOSAM +#include +#include +#endif + +namespace rtabmap { + +static ParametersMap disableDeskewing(ParametersMap params) { + // LIO-SAM performs its own internal deskewing via imageProjection. + // The base-class deskew must be disabled so that the original per-point + // timestamps reach LIO-SAM intact. + params[Parameters::kOdomDeskewing()] = "false"; + return params; +} + +OdometryLIOSAM::OdometryLIOSAM(const ParametersMap & parameters) : + Odometry(disableDeskewing(parameters)) +#ifdef RTABMAP_LIOSAM + ,lioSam_(0) + ,lastPose_(Transform::getIdentity()) + ,lost_(false) + ,linVar_(Parameters::defaultOdomLIOSAMLinVar()) + ,angVar_(Parameters::defaultOdomLIOSAMAngVar()) + ,parameters_(parameters) +#endif +{ +#ifdef RTABMAP_LIOSAM + Parameters::parse(parameters, Parameters::kOdomLIOSAMLinVar(), linVar_); + UASSERT(linVar_ > 0.0f); + Parameters::parse(parameters, Parameters::kOdomLIOSAMAngVar(), angVar_); + UASSERT(angVar_ > 0.0f); +#endif +} + +OdometryLIOSAM::~OdometryLIOSAM() +{ +#ifdef RTABMAP_LIOSAM + delete lioSam_; +#endif +} + +void OdometryLIOSAM::reset(const Transform & initialPose) +{ + Odometry::reset(initialPose); +#ifdef RTABMAP_LIOSAM + if(lioSam_) + { + lioSam_->reset(); + } + lastPose_ = Transform::getIdentity(); + lost_ = false; + imuLocalTransform_ = Transform(); + imuBuffer_.clear(); +#endif +} + +#ifdef RTABMAP_LIOSAM +bool OdometryLIOSAM::init(const Transform & imuLocalTransform, const Transform & lidarLocalTransform) +{ + ParamServer config; + + // Check if a config file path was provided + std::string configPath; + Parameters::parse(parameters_, Parameters::kOdomLIOSAMConfigPath(), configPath); + if(!configPath.empty()) + { + configPath = uReplaceChar(configPath, '~', UDirectory::homeDir()); + if(!UFile::exists(configPath)) + { + UERROR("LIO-SAM config file not found: %s", configPath.c_str()); + return false; + } + UINFO("Loading LIO-SAM parameters from config file: %s", configPath.c_str()); + config = loadParamsFromYaml(configPath); + } + else + { + UINFO("No LIO-SAM config file provided, using rtabmap parameters"); + + // Build ParamServer from individual rtabmap parameters + int sensorType = Parameters::defaultOdomLIOSAMSensor(); + Parameters::parse(parameters_, Parameters::kOdomLIOSAMSensor(), sensorType); + if(sensorType == 1) + config.sensor = SensorType::OUSTER; + else if(sensorType == 2) + config.sensor = SensorType::LIVOX; + else + config.sensor = SensorType::VELODYNE; + + config.N_SCAN = Parameters::defaultOdomLIOSAMNScan(); + Parameters::parse(parameters_, Parameters::kOdomLIOSAMNScan(), config.N_SCAN); + + config.Horizon_SCAN = Parameters::defaultOdomLIOSAMHorizonScan(); + Parameters::parse(parameters_, Parameters::kOdomLIOSAMHorizonScan(), config.Horizon_SCAN); + + config.imuAccNoise = Parameters::defaultOdomLIOSAMImuAccNoise(); + Parameters::parse(parameters_, Parameters::kOdomLIOSAMImuAccNoise(), config.imuAccNoise); + + config.imuGyrNoise = Parameters::defaultOdomLIOSAMImuGyrNoise(); + Parameters::parse(parameters_, Parameters::kOdomLIOSAMImuGyrNoise(), config.imuGyrNoise); + + config.imuAccBiasN = Parameters::defaultOdomLIOSAMImuAccBiasN(); + Parameters::parse(parameters_, Parameters::kOdomLIOSAMImuAccBiasN(), config.imuAccBiasN); + + config.imuGyrBiasN = Parameters::defaultOdomLIOSAMImuGyrBiasN(); + Parameters::parse(parameters_, Parameters::kOdomLIOSAMImuGyrBiasN(), config.imuGyrBiasN); + + config.imuGravity = Parameters::defaultOdomLIOSAMImuGravity(); + Parameters::parse(parameters_, Parameters::kOdomLIOSAMImuGravity(), config.imuGravity); + + config.edgeThreshold = Parameters::defaultOdomLIOSAMEdgeThreshold(); + Parameters::parse(parameters_, Parameters::kOdomLIOSAMEdgeThreshold(), config.edgeThreshold); + + config.surfThreshold = Parameters::defaultOdomLIOSAMSurfThreshold(); + Parameters::parse(parameters_, Parameters::kOdomLIOSAMSurfThreshold(), config.surfThreshold); + + // Set reasonable defaults for params not exposed via rtabmap + config.downsampleRate = 1; + config.lidarMinRange = 1.0f; + config.lidarMaxRange = 1000.0f; + config.imuRPYWeight = 0.01f; + config.odometrySurfLeafSize = 0.2f; + config.mappingCornerLeafSize = 0.2f; + config.mappingSurfLeafSize = 0.4f; + config.z_tollerance = FLT_MAX; + config.rotation_tollerance = FLT_MAX; + config.numberOfCores = 4; + config.mappingProcessInterval = 0.01; + config.surroundingkeyframeAddingDistThreshold = 1.0f; + config.surroundingkeyframeAddingAngleThreshold = 0.2f; + config.surroundingKeyframeDensity = 1.0f; + config.surroundingKeyframeSearchRadius = 50.0f; + config.loopClosureEnableFlag = false; // rtabmap handles loop closures + config.loopClosureFrequency = 1.0f; + config.surroundingKeyframeSize = 50; + config.historyKeyframeSearchRadius = 10.0f; + config.historyKeyframeSearchTimeDiff = 30.0f; + config.historyKeyframeSearchNum = 25; + config.historyKeyframeFitnessScore = 0.3f; + config.globalMapVisualizationSearchRadius = 1e3f; + config.globalMapVisualizationPoseDensity = 10.0f; + config.globalMapVisualizationLeafSize = 1.0f; + config.edgeFeatureMinValidNum = 10; + config.surfFeatureMinValidNum = 100; + config.savePCD = false; + config.useImuHeadingInitialization = false; + config.useGpsElevation = false; + config.gpsCovThreshold = 2.0f; + config.poseCovThreshold = 25.0f; + } + + // Always override extrinsics from sensor local transforms when available. + // This ensures the IMU-to-lidar transform matches the actual sensor setup + // regardless of what the config file says. + // imuLocalTransform = T_base_imu (base_link -> imu_link) + // lidarLocalTransform = T_base_lidar (base_link -> lidar_link) + // LIO-SAM's imuConverter() expects T_lidar_imu: + // T_lidar_imu = T_base_lidar^{-1} * T_base_imu + if(!imuLocalTransform.isNull() && !lidarLocalTransform.isNull()) + { + Transform T_lidar_imu = lidarLocalTransform.inverse() * imuLocalTransform; + Eigen::Matrix4d T = T_lidar_imu.toEigen4d(); + Eigen::Matrix3d rot = T.block<3,3>(0,0); + Eigen::Vector3d trans = T.block<3,1>(0,3); + config.extRotV = {rot(0,0), rot(0,1), rot(0,2), + rot(1,0), rot(1,1), rot(1,2), + rot(2,0), rot(2,1), rot(2,2)}; + config.extRPYV = config.extRotV; + config.extTransV = {trans(0), trans(1), trans(2)}; + UINFO("LIO-SAM extrinsics (T_lidar_imu) computed from sensor local transforms: %s", T_lidar_imu.prettyPrint().c_str()); + } + else if(config.extRotV.size() != 9 || config.extTransV.size() != 3) + { + // No valid extrinsics from sensor data or config file + UERROR("Cannot compute IMU-to-lidar extrinsics: IMU local transform %s, lidar local transform %s. " + "Both must be valid, or the config file must contain valid extrinsics.", + imuLocalTransform.isNull() ? "is null" : "is valid", + lidarLocalTransform.isNull() ? "is null" : "is valid"); + return false; + } + else + { + UINFO("Using extrinsics from config file (sensor local transforms not available)"); + } + + // Set the global extrinsics used by imuConverter + extRot = Eigen::Map >(config.extRotV.data()); + extRPY = Eigen::Map >(config.extRPYV.data()); + extTrans = Eigen::Map >(config.extTransV.data()); + extQRPY = Eigen::Quaterniond(extRPY).inverse(); + + lioSam_ = new lio_sam::LioSamCore(config); + + // Replay buffered IMU samples + UINFO("Replaying %d buffered IMU samples into LIO-SAM", (int)imuBuffer_.size()); + for(const ImuSample & s : imuBuffer_) + { + lioSam_->addImu(s.stamp, s.acc, s.gyro, s.orientation); + } + imuBuffer_.clear(); + + return true; +} +#endif + +Transform OdometryLIOSAM::computeTransform( + SensorData & data, + const Transform & guess, + OdometryInfo * info) +{ + Transform t; +#ifdef RTABMAP_LIOSAM + UTimer timer; + UTimer timerTotal; + + // Handle async IMU data (canProcessAsyncIMU() == true means + // the base class sends IMU-only data directly to computeTransform) + if(!data.imu().empty()) + { + Eigen::Quaterniond qd( + data.imu().orientation()[3], // w + data.imu().orientation()[0], // x + data.imu().orientation()[1], // y + data.imu().orientation()[2]); // z + Eigen::Vector3d acc( + data.imu().linearAcceleration()[0], + data.imu().linearAcceleration()[1], + data.imu().linearAcceleration()[2]); + Eigen::Vector3d gyro( + data.imu().angularVelocity()[0], + data.imu().angularVelocity()[1], + data.imu().angularVelocity()[2]); + + // Deferred initialization: need both IMU and lidar local transforms + // to compute T_lidar_imu extrinsics for LIO-SAM. + if(!lioSam_) + { + // Cache IMU local transform when first available + if(imuLocalTransform_.isNull() && !data.imu().localTransform().isNull()) + { + imuLocalTransform_ = data.imu().localTransform(); + } + + // Try to initialize if we have both transforms + if(!imuLocalTransform_.isNull() && !data.laserScanRaw().isEmpty() && + !data.laserScanRaw().localTransform().isNull()) + { + if(!init(imuLocalTransform_, data.laserScanRaw().localTransform())) + { + UERROR("Failed to initialize LIO-SAM"); + return t; + } + } + else + { + // Buffer IMU until we can initialize + ImuSample s; + s.stamp = data.stamp(); + s.acc = acc; + s.gyro = gyro; + s.orientation = qd; + imuBuffer_.push_back(s); + + if(data.laserScanRaw().isEmpty()) + { + return t; + } + } + } + + if(lioSam_) + { + lioSam_->addImu(data.stamp(), acc, gyro, qd); + } + + // IMU-only: no pose to return + if(data.laserScanRaw().isEmpty()) + { + return t; + } + } + + if(!lioSam_) + { + // A scan arrived without IMU in the same message. + // Try to init if the IMU local transform was already cached. + if(!imuLocalTransform_.isNull() && !data.laserScanRaw().isEmpty() && + !data.laserScanRaw().localTransform().isNull()) + { + if(!init(imuLocalTransform_, data.laserScanRaw().localTransform())) + { + UERROR("Failed to initialize LIO-SAM"); + return t; + } + } + else + { + UDEBUG("LIO-SAM not yet initialized, waiting for IMU (have=%s) and lidar (need scan) local transforms...", + imuLocalTransform_.isNull() ? "no" : "yes"); + return t; + } + } + + if(data.laserScanRaw().isEmpty()) + { + UERROR("LIO-SAM requires laser scans and the current input is empty. Aborting odometry update..."); + return t; + } + else if(data.laserScanRaw().is2d()) + { + UERROR("LIO-SAM requires 3D laser scans. Aborting odometry update..."); + return t; + } + + cv::Mat covariance = cv::Mat::eye(6, 6, CV_64FC1) * 9999; + if(!lost_) + { + const LaserScan & scan = data.laserScanRaw(); + if(scan.format() != LaserScan::kXYZIRT) + { + UERROR("LIO-SAM requires a scan in format %s (got %s). " + "Populate the scan via util3d::laserScanFromPointCloud() " + "so that per-point ring and time fields are available.", + LaserScan::formatName(LaserScan::kXYZIRT).c_str(), + scan.formatName().c_str()); + return t; + } + + // Split the kXYZIRT scan into the three parallel buffers LIO-SAM expects. + const int numPoints = scan.size(); + const int ringOffset = scan.getRingOffset(); + const int timeOffset = scan.getTimeOffset(); + pcl::PointCloud::Ptr laserCloudIn(new pcl::PointCloud); + laserCloudIn->reserve(numPoints); + std::vector rings; + std::vector times; + rings.reserve(numPoints); + times.reserve(numPoints); + for(int i=0; i(row, col); + pcl::PointXYZI pt; + pt.x = ptr[0]; + pt.y = ptr[1]; + pt.z = ptr[2]; + pt.intensity = ptr[3]; + laserCloudIn->push_back(pt); + rings.push_back(static_cast(ptr[ringOffset])); + times.push_back(ptr[timeOffset]); + } + UDEBUG("Scan split: %fs, points=%d", timer.ticks(), (int)laserCloudIn->size()); + + // Process scan. Retrieve the deskewed (motion-compensated) cloud + // produced by LIO-SAM's image projection stage so we can propagate + // it back into SensorData: otherwise downstream consumers such as + // loop closure registration would still see the raw pre-deskew scan. + Eigen::Affine3f poseOut; + Eigen::MatrixXd covOut; + pcl::PointCloud::Ptr deskewedCloud(new pcl::PointCloud); + bool ok = lioSam_->processScan(data.stamp(), laserCloudIn, rings, times, poseOut, covOut, deskewedCloud); + UDEBUG("LIO-SAM process: %fs", timer.ticks()); + + if(ok) + { + // Replace the raw scan on SensorData with LIO-SAM's deskewed + // cloud so downstream stages (loop closure registration in + // particular) use the motion-compensated points instead of + // the raw pre-deskew scan. The deskewed cloud is still in the + // lidar frame, so the existing localTransform/rangeMax apply. + if(deskewedCloud && !deskewedCloud->empty()) + { + const LaserScan & rawScan = data.laserScanRaw(); + LaserScan deskewedScan( + util3d::laserScanFromPointCloud(*deskewedCloud), + rawScan.maxPoints(), + rawScan.rangeMax(), + rawScan.localTransform()); + data.setLaserScan(deskewedScan); + UDEBUG("Replaced raw scan with deskewed cloud (%d -> %d points)", + (int)laserCloudIn->size(), (int)deskewedCloud->size()); + } + + Transform pose = Transform::fromEigen3f(poseOut); + + if(!pose.isNull()) + { + covariance = cv::Mat::eye(6, 6, CV_64FC1); + covariance(cv::Range(0, 3), cv::Range(0, 3)) *= linVar_; + covariance(cv::Range(3, 6), cv::Range(3, 6)) *= angVar_; + + t = lastPose_.inverse() * pose; // incremental + lastPose_ = pose; + + const Transform & localTransform = data.laserScanRaw().localTransform(); + if(!t.isNull() && !t.isIdentity() && !localTransform.isIdentity() && !localTransform.isNull()) + { + // from laser frame to base frame + t = localTransform * t * localTransform.inverse(); + } + + if(info) + { + info->type = (int)kTypeLIOSAM; + if(covariance.cols == 6 && covariance.rows == 6 && covariance.type() == CV_64FC1) + { + info->reg.covariance = covariance; + } + + if(this->isInfoDataFilled()) + { + pcl::PointCloud::Ptr localMap = lioSam_->getLocalMap(); + if(localMap && !localMap->empty()) + { + info->localScanMapSize = localMap->size(); + info->localScanMap = LaserScan(util3d::laserScanFromPointCloud(*localMap), 0, data.laserScanRaw().rangeMax(), data.laserScanRaw().localTransform()); + } + UDEBUG("Fill info data: %fs", timer.ticks()); + } + } + } + else + { + lost_ = true; + UWARN("LIO-SAM failed to register the latest scan, odometry should be reset."); + } + } + else + { + UDEBUG("LIO-SAM processScan returned false (may be initializing)"); + } + } + UINFO("LIO-SAM odom update time = %fs, lost=%s", timerTotal.elapsed(), lost_ ? "true" : "false"); + +#else + UERROR("RTAB-Map is not built with LIO-SAM support! Select another odometry approach."); +#endif + return t; +} + +} // namespace rtabmap diff --git a/corelib/src/util3d.cpp b/corelib/src/util3d.cpp index 563e22a6..facb0d6c 100644 --- a/corelib/src/util3d.cpp +++ b/corelib/src/util3d.cpp @@ -1762,6 +1762,55 @@ LaserScan laserScanFromPointCloud(const pcl::PointCloud & cloud, return laserScanFromPointCloud(cloud, pcl::IndicesPtr(), transform, filterNaNs); } +LaserScan laserScanFromPointCloud(const pcl::PointCloud & cloud, const Transform & transform, bool filterNaNs) +{ + return laserScanFromPointCloud(cloud, pcl::IndicesPtr(), transform, filterNaNs); +} + +LaserScan laserScanFromPointCloud(const pcl::PointCloud & cloud, const pcl::IndicesPtr & indices, const Transform & transform, bool filterNaNs) +{ + // Layout: [x, y, z, intensity, ring, time] (ring cast to float, values up to + // ~16M are exactly representable so all realistic laser line counts fit). + cv::Mat laserScan; + bool nullTransform = transform.isNull() || transform.isIdentity(); + Eigen::Affine3f transform3f = transform.toEigen3f(); + int oi = 0; + const int total = indices.get() ? (int)indices->size() : (int)cloud.size(); + laserScan = cv::Mat(1, total, CV_32FC(6)); + for(int i=0; iat(i) : i; + const rtabmap::PointXYZIRT & src = cloud.at(index); + if(filterNaNs && !pcl::isFinite(src)) + { + continue; + } + float * ptr = laserScan.ptr(0, oi++); + if(!nullTransform) + { + pcl::PointXYZ pt(src.x, src.y, src.z); + pt = pcl::transformPoint(pt, transform3f); + ptr[0] = pt.x; + ptr[1] = pt.y; + ptr[2] = pt.z; + } + else + { + ptr[0] = src.x; + ptr[1] = src.y; + ptr[2] = src.z; + } + ptr[3] = src.intensity; + ptr[4] = static_cast(src.ring); + ptr[5] = src.time; + } + if(oi == 0) + { + return LaserScan(); + } + return LaserScan(laserScan(cv::Range::all(), cv::Range(0, oi)), 0, 0.0f, LaserScan::kXYZIRT); +} + LaserScan laserScanFromPointCloud(const pcl::PointCloud & cloud, const pcl::IndicesPtr & indices, const Transform & transform, bool filterNaNs) { cv::Mat laserScan; @@ -2343,7 +2392,7 @@ pcl::PCLPointCloud2::Ptr laserScanToPointCloud2(const LaserScan & laserScan, con { pcl::toPCLPointCloud2(*laserScanToPointCloud(laserScan, transform), *cloud); } - else if(laserScan.format() == LaserScan::kXYI || laserScan.format() == LaserScan::kXYZI || laserScan.format() == LaserScan::kXYZIT) + else if(laserScan.format() == LaserScan::kXYI || laserScan.format() == LaserScan::kXYZI || laserScan.format() == LaserScan::kXYZIT || laserScan.format() == LaserScan::kXYZIRT) { pcl::toPCLPointCloud2(*laserScanToPointCloudI(laserScan, transform), *cloud); } @@ -3807,9 +3856,11 @@ LaserScan deskew( return LaserScan(); } - if(input.format() != LaserScan::kXYZIT) + if(!input.hasTime()) { - UERROR("input scan doesn't have \"time\" channel! Only format \"%s\" supported yet.", LaserScan::formatName(LaserScan::kXYZIT).c_str()); + UERROR("input scan doesn't have a \"time\" channel! Supported formats: \"%s\", \"%s\".", + LaserScan::formatName(LaserScan::kXYZIT).c_str(), + LaserScan::formatName(LaserScan::kXYZIRT).c_str()); return LaserScan(); } @@ -3865,7 +3916,14 @@ LaserScan deskew( double stamp; UTimer processingTime; double scanTime = lastStamp - firstStamp; - cv::Mat output(1, input.size(), CV_32FC4); // XYZI - Dense + // Preserve ring when input carries it (kXYZIRT): the geometric channel is + // still meaningful after deskewing. Per-point time is zeroed because all + // points share the same pose after correction. + const bool preserveRing = input.hasRing(); + const int offsetRing = input.getRingOffset(); + const LaserScan::Format outputFormat = preserveRing ? LaserScan::kXYZIRT : LaserScan::kXYZI; + const int outputChannels = preserveRing ? 6 : 4; + cv::Mat output(1, input.size(), CV_32FC(outputChannels)); int offsetIntensity = input.getIntensityOffset(); bool isLocalTransformIdentity = input.localTransform().isIdentity(); Transform localTransformInv = input.localTransform().inverse(); @@ -3904,7 +3962,12 @@ LaserScan deskew( dataPtr[0] = pt.x; dataPtr[1] = pt.y; dataPtr[2] = pt.z; - dataPtr[3] = input.data().ptr(v, u)[offsetIntensity]; + dataPtr[3] = inputPtr[offsetIntensity]; + if(preserveRing) + { + dataPtr[4] = inputPtr[offsetRing]; + dataPtr[5] = 0.0f; + } } } } @@ -3941,14 +4004,19 @@ LaserScan deskew( dataPtr[0] = pt.x; dataPtr[1] = pt.y; dataPtr[2] = pt.z; - dataPtr[3] = input.data().ptr(v, u)[offsetIntensity]; + dataPtr[3] = inputPtr[offsetIntensity]; + if(preserveRing) + { + dataPtr[4] = inputPtr[offsetRing]; + dataPtr[5] = 0.0f; + } } } } } output = cv::Mat(output, cv::Range::all(), cv::Range(0, oi)); UDEBUG("Lidar deskewing time=%fs", processingTime.elapsed()); - return LaserScan(output, input.maxPoints(), input.rangeMax(), LaserScan::kXYZI, input.localTransform()); + return LaserScan(output, input.maxPoints(), input.rangeMax(), outputFormat, input.localTransform()); } diff --git a/guilib/include/rtabmap/gui/PreferencesDialog.h b/guilib/include/rtabmap/gui/PreferencesDialog.h index 91845101..c6be4694 100644 --- a/guilib/include/rtabmap/gui/PreferencesDialog.h +++ b/guilib/include/rtabmap/gui/PreferencesDialog.h @@ -371,6 +371,7 @@ private Q_SLOTS: void changeOdometryORBSLAMVocabulary(); void changeOdometryOKVISConfigPath(); void changeOdometryVINSFusionConfigPath(); + void changeOdometryLIOSAMConfigPath(); void changeOdometryOpenVINSLeftMask(); void changeOdometryOpenVINSRightMask(); void changeIcpPMConfigPath(); diff --git a/guilib/src/PreferencesDialog.cpp b/guilib/src/PreferencesDialog.cpp index 00bb6e5b..44ea07a3 100644 --- a/guilib/src/PreferencesDialog.cpp +++ b/guilib/src/PreferencesDialog.cpp @@ -243,6 +243,9 @@ PreferencesDialog::PreferencesDialog(QWidget * parent) : #ifndef RTABMAP_CUVSLAM _ui->odom_strategy->setItemData(13, 0, Qt::UserRole - 1); #endif +#ifndef RTABMAP_LIOSAM + _ui->odom_strategy->setItemData(14, 0, Qt::UserRole - 1); +#endif #if CV_MAJOR_VERSION < 3 _ui->stereosgbm_mode->setItemData(2, 0, Qt::UserRole - 1); @@ -1698,6 +1701,22 @@ PreferencesDialog::PreferencesDialog(QWidget * parent) : // Odometry CuVSLAM _ui->odom_cuvslam_multicam_mode->setObjectName(Parameters::kOdomCuVSLAMMulticamMode().c_str()); + // Odometry LIO-SAM + _ui->lineEdit_OdomLIOSAMPath->setObjectName(Parameters::kOdomLIOSAMConfigPath().c_str()); + connect(_ui->toolButton_OdomLIOSAMPath, SIGNAL(clicked()), this, SLOT(changeOdometryLIOSAMConfigPath())); + _ui->odom_liosam_sensor->setObjectName(Parameters::kOdomLIOSAMSensor().c_str()); + _ui->odom_liosam_nscan->setObjectName(Parameters::kOdomLIOSAMNScan().c_str()); + _ui->odom_liosam_horizon_scan->setObjectName(Parameters::kOdomLIOSAMHorizonScan().c_str()); + _ui->odom_liosam_imu_acc_noise->setObjectName(Parameters::kOdomLIOSAMImuAccNoise().c_str()); + _ui->odom_liosam_imu_gyr_noise->setObjectName(Parameters::kOdomLIOSAMImuGyrNoise().c_str()); + _ui->odom_liosam_imu_acc_bias_n->setObjectName(Parameters::kOdomLIOSAMImuAccBiasN().c_str()); + _ui->odom_liosam_imu_gyr_bias_n->setObjectName(Parameters::kOdomLIOSAMImuGyrBiasN().c_str()); + _ui->odom_liosam_imu_gravity->setObjectName(Parameters::kOdomLIOSAMImuGravity().c_str()); + _ui->odom_liosam_edge_threshold->setObjectName(Parameters::kOdomLIOSAMEdgeThreshold().c_str()); + _ui->odom_liosam_surf_threshold->setObjectName(Parameters::kOdomLIOSAMSurfThreshold().c_str()); + _ui->odom_liosam_linvar->setObjectName(Parameters::kOdomLIOSAMLinVar().c_str()); + _ui->odom_liosam_angvar->setObjectName(Parameters::kOdomLIOSAMAngVar().c_str()); + //StereoDense _ui->comboBox_stereoDense_strategy->setObjectName(Parameters::kStereoDenseStrategy().c_str()); connect(_ui->comboBox_stereoDense_strategy, SIGNAL(currentIndexChanged(int)), _ui->stackedWidget_stereoDense, SLOT(setCurrentIndex(int))); @@ -5572,6 +5591,7 @@ void PreferencesDialog::updateOdometryStackedIndex(int index) _ui->groupBox_odomOpenVINS->setVisible(index==10); _ui->groupBox_odomOpen3D->setVisible(index==12); _ui->groupBox_odomCuvslam->setVisible(index==13); + _ui->groupBox_odomLIOSAM->setVisible(index==14); } void PreferencesDialog::useOdomFeatures() @@ -5677,6 +5697,23 @@ void PreferencesDialog::changeOdometryVINSFusionConfigPath() } } +void PreferencesDialog::changeOdometryLIOSAMConfigPath() +{ + QString path; + if(_ui->lineEdit_OdomLIOSAMPath->text().isEmpty()) + { + path = QFileDialog::getOpenFileName(this, tr("LIO-SAM Config"), this->getWorkingDirectory(), tr("LIO-SAM config (*.yaml)")); + } + else + { + path = QFileDialog::getOpenFileName(this, tr("LIO-SAM Config"), _ui->lineEdit_OdomLIOSAMPath->text(), tr("LIO-SAM config (*.yaml)")); + } + if(!path.isEmpty()) + { + _ui->lineEdit_OdomLIOSAMPath->setText(path); + } +} + void PreferencesDialog::changeOdometryOpenVINSLeftMask() { QString path; diff --git a/guilib/src/ui/preferencesDialog.ui b/guilib/src/ui/preferencesDialog.ui index 5787cae7..caf27d52 100644 --- a/guilib/src/ui/preferencesDialog.ui +++ b/guilib/src/ui/preferencesDialog.ui @@ -16137,6 +16137,11 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag CuVSLAM + + + LIO-SAM + + @@ -21847,6 +21852,458 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag + + + + + + LIO-SAM + + + + + + <html><head/><body><p>LIO-SAM: <a href="https://github.com/TixiaoShan/LIO-SAM"><span style=" text-decoration: underline; color:#0000ff;">https://github.com/TixiaoShan/LIO-SAM</span></a></p><p>Tightly-coupled lidar-inertial odometry via smoothing and mapping. Requires 3D lidar with per-point ring and time fields (kXYZIRT scan format) and an IMU.</p><p>If a config file path is provided, the parameters below are ignored and read from the file instead.</p></body></html> + + + true + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + + Optional: path to LIO-SAM config YAML + + + + + + + ... + + + + + + + + + Config file path (overrides parameters below). + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + QComboBox::AdjustToContents + + + + Velodyne + + + + + Ouster + + + + + Livox + + + + + + + + LiDAR sensor type. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + 1 + + + 256 + + + 16 + + + + + + + Number of LiDAR channels (16, 32, 64, 128). + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + 1 + + + 10000 + + + 1800 + + + + + + + Horizontal resolution (Velodyne:1800, Ouster:512/1024/2048). + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + 6 + + + 0.000001000000000 + + + 1.000000000000000 + + + 0.001000000000000 + + + 0.010000000000000 + + + + + + + IMU accelerometer white noise. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + 6 + + + 0.000001000000000 + + + 1.000000000000000 + + + 0.000100000000000 + + + 0.001000000000000 + + + + + + + IMU gyroscope white noise. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + 6 + + + 0.000001000000000 + + + 1.000000000000000 + + + 0.000100000000000 + + + 0.000200000000000 + + + + + + + IMU accelerometer bias noise. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + 6 + + + 0.000001000000000 + + + 1.000000000000000 + + + 0.000010000000000 + + + 0.000030000000000 + + + + + + + IMU gyroscope bias noise. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + 5 + + + 0.000000000000000 + + + 20.000000000000000 + + + 0.010000000000000 + + + 9.805110000000000 + + + + + + + Gravity magnitude. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + 4 + + + 0.000100000000000 + + + 100.000000000000000 + + + 0.100000000000000 + + + 1.000000000000000 + + + + + + + Edge feature curvature threshold. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + 4 + + + 0.000100000000000 + + + 100.000000000000000 + + + 0.010000000000000 + + + 0.100000000000000 + + + + + + + Surface feature curvature threshold. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + 4 + + + 0.000100000000000 + + + 1.000000000000000 + + + 0.001000000000000 + + + 0.010000000000000 + + + + + + + Linear output variance. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + 4 + + + 0.000100000000000 + + + 1.000000000000000 + + + 0.001000000000000 + + + 0.010000000000000 + + + + + + + Angular output variance. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + From a8fd7f043d827d8faf41fcd5c5a5baa9120cb502 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Mon, 13 Apr 2026 01:16:58 +0000 Subject: [PATCH 097/126] CloudViewer: fixed rendering camera rotation not normalized warning --- corelib/src/Odometry.cpp | 2 +- guilib/src/CloudViewer.cpp | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/corelib/src/Odometry.cpp b/corelib/src/Odometry.cpp index f38ab38c..ebcd1db1 100644 --- a/corelib/src/Odometry.cpp +++ b/corelib/src/Odometry.cpp @@ -657,7 +657,7 @@ Transform Odometry::process(SensorData & data, const Transform & guessIn, Odomet UWARN("Could not find imu transform at %f", data.stamp()); } } - else if(!guess.isNull()) { + else if(!guess.isNull() && (!data.imageRaw().empty() || !data.laserScanRaw().isEmpty())) { UDEBUG("Using guess from motion %s", guess.prettyPrint().c_str()); } diff --git a/guilib/src/CloudViewer.cpp b/guilib/src/CloudViewer.cpp index fd042c79..fc5a8b38 100644 --- a/guilib/src/CloudViewer.cpp +++ b/guilib/src/CloudViewer.cpp @@ -2969,18 +2969,19 @@ void CloudViewer::updateCameraTargetPosition(const Transform & pose) Eigen::Vector3f zAxis(cameras.front().view[0], cameras.front().view[1], cameras.front().view[2]); Eigen::Vector3f yAxis = zAxis.cross(vPosToFocal); Eigen::Vector3f xAxis = yAxis.cross(zAxis); - Transform PR(xAxis[0], xAxis[1], xAxis[2],0, - yAxis[0], yAxis[1], yAxis[2],0, - zAxis[0], zAxis[1], zAxis[2],0); - PR.normalizeRotation(); + Eigen::Matrix3f m; + m << xAxis[0], xAxis[1], xAxis[2], + yAxis[0], yAxis[1], yAxis[2], + zAxis[0], zAxis[1], zAxis[2]; - Transform P(PR[0], PR[1], PR[2], cameras.front().pos[0], - PR[4], PR[5], PR[6], cameras.front().pos[1], - PR[8], PR[9], PR[10], cameras.front().pos[2]); - Transform F(PR[0], PR[1], PR[2], cameras.front().focal[0], - PR[4], PR[5], PR[6], cameras.front().focal[1], - PR[8], PR[9], PR[10], cameras.front().focal[2]); + // Make sure it is normalized + Eigen::Quaternionf q = Eigen::Quaternionf(m).normalized(); + + Transform P(cameras.front().pos[0], cameras.front().pos[1], cameras.front().pos[2], + q.x(), q.y(), q.z(), q.w()); + Transform F(cameras.front().focal[0], cameras.front().focal[1], cameras.front().focal[2], + q.x(), q.y(), q.z(), q.w()); Transform N = pose; Transform O = _lastPose; Transform O2N = O.inverse()*N; From aa7546ec62cff71ad345e96914d8791cdfcf0684 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Tue, 14 Apr 2026 12:09:41 -0700 Subject: [PATCH 098/126] Enable WITH_PYTHON on CI (#1689) * Enable WITH_PYTHON on CI * add python-dev * use python3-dev instead of libpython3-dev * use action to instlal python * pybind now missing * Fixed python build on linux * warning * try to install compatible pybind * explicitly set where to find pybind --- .github/workflows/cmake-linux.yml | 18 ++++++++++++++---- corelib/src/python/PythonInterface.cpp | 2 ++ guilib/src/ui/preferencesDialog.ui | 2 +- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cmake-linux.yml b/.github/workflows/cmake-linux.yml index b26409af..8c752db4 100644 --- a/.github/workflows/cmake-linux.yml +++ b/.github/workflows/cmake-linux.yml @@ -27,15 +27,15 @@ jobs: - build_name: ubuntu-22.04 os: ubuntu-22.04 extra_deps: "libunwind-dev libceres-dev" - extra_cmake_def: "-DWITH_CERES=ON" + extra_cmake_def: "-DWITH_CERES=ON -DWITH_PYTHON=ON" - build_name: ubuntu-24.04 os: ubuntu-24.04 extra_deps: "libg2o-dev libceres-dev" - extra_cmake_def: "-DWITH_CERES=ON" + extra_cmake_def: "-DWITH_CERES=ON -DWITH_PYTHON=ON" - build_name: ubuntu-24.04-with-opengv os: ubuntu-24.04 extra_deps: "libg2o-dev libceres-dev" - extra_cmake_def: "-DWITH_CERES=ON -DBUILD_OPENGV=ON" + extra_cmake_def: "-DWITH_CERES=ON -DWITH_PYTHON=ON -DBUILD_OPENGV=ON" steps: - uses: actions/checkout@v4 @@ -46,9 +46,19 @@ jobs: sudo apt-get update sudo apt-get -y install libopencv-dev libpcl-dev git cmake software-properties-common libyaml-cpp-dev ${{ matrix.extra_deps }} + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install Python Dependencies + run: | + python -m pip install --upgrade pip + pip install numpy pybind11 + - name: Configure CMake run: | - cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} ${{ matrix.extra_cmake_def }} + cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DPython3_EXECUTABLE=$(which python3) -Dpybind11_DIR=$(python3 -m pybind11 --cmakedir) ${{ matrix.extra_cmake_def }} - name: Build run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} diff --git a/corelib/src/python/PythonInterface.cpp b/corelib/src/python/PythonInterface.cpp index 5fd0b43a..3b9abc94 100644 --- a/corelib/src/python/PythonInterface.cpp +++ b/corelib/src/python/PythonInterface.cpp @@ -19,9 +19,11 @@ PythonInterface::PythonInterface() guard_ = new pybind11::scoped_interpreter(); // Tell Python to look in this directory for DLLs +#ifdef _WIN32 std::string exe_dir = std::filesystem::current_path().string(); pybind11::module_ os = pybind11::module_::import("os"); os.attr("add_dll_directory")(exe_dir); +#endif pybind11::module::import("threading"); release_ = new pybind11::gil_scoped_release(); diff --git a/guilib/src/ui/preferencesDialog.ui b/guilib/src/ui/preferencesDialog.ui index caf27d52..7be7c900 100644 --- a/guilib/src/ui/preferencesDialog.ui +++ b/guilib/src/ui/preferencesDialog.ui @@ -23649,7 +23649,7 @@ Lower the ratio -> higher the precision. - + Qt::Vertical From 79ff91d2938491e2d010e7e688fd5619562aee9d Mon Sep 17 00:00:00 2001 From: matlabbe Date: Thu, 16 Apr 2026 17:39:58 -0700 Subject: [PATCH 099/126] Added Statisics: Loop/Optimization_max_error_from_id and Loop/Optimization_max_error_to_id to help with debugging rejected loop closures --- corelib/include/rtabmap/core/Statistics.h | 2 ++ corelib/src/Rtabmap.cpp | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/corelib/include/rtabmap/core/Statistics.h b/corelib/include/rtabmap/core/Statistics.h index 8f7a32ce..11937b0e 100644 --- a/corelib/include/rtabmap/core/Statistics.h +++ b/corelib/include/rtabmap/core/Statistics.h @@ -76,6 +76,8 @@ class RTABMAP_CORE_EXPORT Statistics RTABMAP_STATS(Loop, Optimization_max_ang_error_ratio, ); RTABMAP_STATS(Loop, Optimization_error, ); RTABMAP_STATS(Loop, Optimization_iterations, ); + RTABMAP_STATS(Loop, Optimization_max_error_from_id, ); + RTABMAP_STATS(Loop, Optimization_max_error_to_id, ); RTABMAP_STATS(Loop, Linear_variance,); RTABMAP_STATS(Loop, Angular_variance,); RTABMAP_STATS(Loop, Landmark_detected,); diff --git a/corelib/src/Rtabmap.cpp b/corelib/src/Rtabmap.cpp index 16055cf7..37acc399 100644 --- a/corelib/src/Rtabmap.cpp +++ b/corelib/src/Rtabmap.cpp @@ -3186,6 +3186,8 @@ bool Rtabmap::process( float maxLinearErrorRatio = 0.0f; float maxAngularError = 0.0f; float maxAngularErrorRatio = 0.0f; + int maxLinearErrorFromId = 0; + int maxLinearErrorToId = 0; double optimizationError = 0.0; int optimizationIterations = 0; Transform previousMapCorrection; @@ -3354,6 +3356,8 @@ bool Rtabmap::process( if(maxLinearLink) { + maxLinearErrorFromId = maxLinearLink->from(); + maxLinearErrorToId = maxLinearLink->to(); UINFO("Max optimization linear error = %f m (link %d->%d, var=%f, ratio error/std=%f, thr=%f)", maxLinearError, maxLinearLink->from(), @@ -3504,6 +3508,8 @@ bool Rtabmap::process( if(maxLinearLink) { + maxLinearErrorFromId = maxLinearLink->from(); + maxLinearErrorToId = maxLinearLink->to(); UINFO("Max optimization linear error = %f m (link %d->%d, var=%f, ratio error/std=%f, thr=%f)", maxLinearError, maxLinearLink->from(), @@ -3854,6 +3860,8 @@ bool Rtabmap::process( bool reject = false; if(maxLinearLink) { + maxLinearErrorFromId = maxLinearLink->from(); + maxLinearErrorToId = maxLinearLink->to(); UINFO("Max optimization linear error = %f m (link %d->%d, var=%f, ratio error/std=%f)", maxLinearError, maxLinearLink->from(), maxLinearLink->to(), maxLinearLink->transVariance(), maxLinearError/sqrt(maxLinearLink->transVariance())); if(_optimizationMaxError > 0.0f && maxLinearErrorRatio > _optimizationMaxError) { @@ -4075,6 +4083,8 @@ bool Rtabmap::process( statistics_.addStatistic(Statistics::kLoopOptimization_max_error_ratio(), maxLinearErrorRatio); statistics_.addStatistic(Statistics::kLoopOptimization_max_ang_error(), maxAngularError*180.0f/M_PI); statistics_.addStatistic(Statistics::kLoopOptimization_max_ang_error_ratio(), maxAngularErrorRatio); + statistics_.addStatistic(Statistics::kLoopOptimization_max_error_from_id(), maxLinearErrorFromId); + statistics_.addStatistic(Statistics::kLoopOptimization_max_error_to_id(), maxLinearErrorToId); statistics_.addStatistic(Statistics::kLoopOptimization_error(), optimizationError); statistics_.addStatistic(Statistics::kLoopOptimization_iterations(), optimizationIterations); statistics_.addStatistic(Statistics::kLoopLandmark_detected(), landmarksDetected.empty()?0:-landmarksDetected.begin()->first); From cc86af4399f5ad8b14b0de98a742383e48503abb Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sat, 18 Apr 2026 12:57:35 -0700 Subject: [PATCH 100/126] GraphViewer: Fixed ground truth and gps context menu option disabled even if there are there, after hiding them, saving and reopening UI --- guilib/src/GraphViewer.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/guilib/src/GraphViewer.cpp b/guilib/src/GraphViewer.cpp index a09fa461..89f23a86 100644 --- a/guilib/src/GraphViewer.cpp +++ b/guilib/src/GraphViewer.cpp @@ -2270,11 +2270,8 @@ void GraphViewer::contextMenuEvent(QContextMenuEvent * event) aMouseTracking->setChecked(_mouseTracking); aMouseTracking->setEnabled(_viewPlane == XY); aShowHideGraph->setEnabled(_viewPlane == XY); - aShowHideGraphNodes->setEnabled(_graphRoot->isVisible()); aShowHideGlobalPath->setEnabled(_globalPathLinkItems.size()); aShowHideLocalPath->setEnabled(_localPathLinkItems.size()); - aShowHideGtGraph->setEnabled(_gtGraphRoot->isVisible()); - aShowHideGPSGraph->setEnabled(_gpsGraphRoot->isVisible()); aShowHideOdomCacheOverlay->setEnabled(_odomCacheOverlay->rect().width()>0); QMenu * viewPlaneMenu = menu.addMenu("View Plane..."); From 9cbe84e44533f9e14d66118e6c69ba67c291cf38 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 19 Apr 2026 16:08:03 -0700 Subject: [PATCH 101/126] Support intermediate nodes with memory management enabled (#1687) * Support intermediate nodes with memory management enabled * Adding fix from #1686 * Fixed odomCache overlay shown in mapping mode * Fixed local retrieval with intermediate nodes * bumping version for updating API. Fixed getting graph error. * Disable planning if intermediate nodes are there. Updated usage of Kp/BadSignRatio to support intermediate nodes. --- CMakeLists.txt | 2 +- app/android/jni/RTABMapApp.cpp | 2 +- corelib/include/rtabmap/core/Memory.h | 2 +- corelib/include/rtabmap/core/Parameters.h | 2 +- corelib/include/rtabmap/core/RtabmapThread.h | 1 - corelib/src/Memory.cpp | 236 +++++++++++++++---- corelib/src/Optimizer.cpp | 3 +- corelib/src/Rtabmap.cpp | 227 +++++++++++++----- corelib/src/RtabmapThread.cpp | 13 +- corelib/src/odometry/OdometryMono.cpp | 12 +- corelib/src/optimizer/OptimizerGTSAM.cpp | 4 +- guilib/src/DataRecorder.cpp | 2 +- guilib/src/DatabaseViewer.cpp | 3 +- guilib/src/MainWindow.cpp | 14 +- package.xml | 2 +- 15 files changed, 385 insertions(+), 140 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b2f3f51b..101457df 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,7 +22,7 @@ SET(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake_modules") ####################### SET(RTABMAP_MAJOR_VERSION 0) SET(RTABMAP_MINOR_VERSION 23) -SET(RTABMAP_PATCH_VERSION 4) +SET(RTABMAP_PATCH_VERSION 5) SET(RTABMAP_VERSION ${RTABMAP_MAJOR_VERSION}.${RTABMAP_MINOR_VERSION}.${RTABMAP_PATCH_VERSION}) diff --git a/app/android/jni/RTABMapApp.cpp b/app/android/jni/RTABMapApp.cpp index 52ec7e71..2db3e8d3 100644 --- a/app/android/jni/RTABMapApp.cpp +++ b/app/android/jni/RTABMapApp.cpp @@ -3000,7 +3000,7 @@ void RTABMapApp::setTrajectoryMode(bool enabled) void RTABMapApp::setGraphOptimization(bool enabled) { graphOptimization_ = enabled; - if((sensorCaptureThread_ == 0) && rtabmap_ && rtabmap_->getMemory()->getLastWorkingSignature()!=0) + if((sensorCaptureThread_ == 0) && rtabmap_ && rtabmap_->getMemory()->getLastWorkingSignature(false)!=0) { std::map poses; std::multimap links; diff --git a/corelib/include/rtabmap/core/Memory.h b/corelib/include/rtabmap/core/Memory.h index 40b30ad4..f5bc8514 100644 --- a/corelib/include/rtabmap/core/Memory.h +++ b/corelib/include/rtabmap/core/Memory.h @@ -162,7 +162,7 @@ public: float getSimilarityThreshold() const {return _similarityThreshold;} std::map getWeights() const; int getLastSignatureId() const; - const Signature * getLastWorkingSignature() const; + const Signature * getLastWorkingSignature(bool ignoreIntermediateNodes) const; std::map getNodesObservingLandmark(int landmarkId, bool lookInDatabase) const; int getSignatureIdByLabel(const std::string & label, bool lookInDatabase = true) const; bool labelSignature(int id, const std::string & label); diff --git a/corelib/include/rtabmap/core/Parameters.h b/corelib/include/rtabmap/core/Parameters.h index d51c8043..b9e7a27b 100644 --- a/corelib/include/rtabmap/core/Parameters.h +++ b/corelib/include/rtabmap/core/Parameters.h @@ -249,7 +249,7 @@ class RTABMAP_CORE_EXPORT Parameters RTABMAP_PARAM(Kp, MinDepth, float, 0, "Filter extracted keypoints by depth."); RTABMAP_PARAM(Kp, MaxFeatures, int, 500, "Maximum features extracted from the images (0 means not bounded, <0 means no extraction)."); RTABMAP_PARAM(Kp, SSC, bool, false, "If true, SSC (Suppression via Square Covering) is applied to limit keypoints."); - RTABMAP_PARAM(Kp, BadSignRatio, float, 0.5, "Bad signature ratio (less than Ratio x AverageWordsPerImage = bad)."); + RTABMAP_PARAM(Kp, BadSignRatio, float, 0.5, uFormat("Bad signature ratio. If %s=0, the ratio is computed from the average number of words per signature (less than Ratio x AverageWordsPerImage = bad).", kKpMaxFeatures().c_str())); RTABMAP_PARAM(Kp, NndrRatio, float, 0.8, "NNDR ratio (A matching pair is detected, if its distance is closer than X times the distance of the second nearest neighbor.)"); #if CV_MAJOR_VERSION > 2 && !defined(HAVE_OPENCV_XFEATURES2D) // OpenCV>2 without xFeatures2D module doesn't have BRIEF diff --git a/corelib/include/rtabmap/core/RtabmapThread.h b/corelib/include/rtabmap/core/RtabmapThread.h index 96fa214c..9cdecd8b 100644 --- a/corelib/include/rtabmap/core/RtabmapThread.h +++ b/corelib/include/rtabmap/core/RtabmapThread.h @@ -106,7 +106,6 @@ private: unsigned int _dataBufferMaxSize; float _rate; bool _createIntermediateNodes; - UTimer * _frameRateTimer; double _previousStamp; Rtabmap * _rtabmap; diff --git a/corelib/src/Memory.cpp b/corelib/src/Memory.cpp index eebdcc6a..3d880805 100644 --- a/corelib/src/Memory.cpp +++ b/corelib/src/Memory.cpp @@ -1434,23 +1434,30 @@ void Memory::moveSignatureToWMFromSTM(int id, int * reducedToOut) { Signature * s = this->_getSignature(id); UASSERT(s!=0); - std::multimap links = s->getLinks(); - // Setting true to make sure we save all visual - // words that could be referenced in a previously - // transferred node in LTM (#979) - reducedId = reduceNode(s->id(), 0, true); - if(reducedToOut) { - *reducedToOut = reducedId; - } - if(reducedId>0) + if(s->getWeight() == -1) { - for(std::multimap::iterator iter=links.begin(); iter!=links.end(); ++iter) + UERROR("Graph reduction with intermediate nodes is not supported."); + } + else + { + std::multimap links = s->getLinks(); + // Setting true to make sure we save all visual + // words that could be referenced in a previously + // transferred node in LTM (#979) + reducedId = reduceNode(s->id(), 0, true); + if(reducedToOut) { + *reducedToOut = reducedId; + } + if(reducedId>0) { - if(iter->second.type() == Link::kNeighbor) + for(std::multimap::iterator iter=links.begin(); iter!=links.end(); ++iter) { - if(_lastGlobalLoopClosureId == s->id()) + if(iter->second.type() == Link::kNeighbor) { - _lastGlobalLoopClosureId = iter->first; + if(_lastGlobalLoopClosureId == s->id()) + { + _lastGlobalLoopClosureId = iter->first; + } } } } @@ -1674,7 +1681,7 @@ std::map Memory::getNeighborsId( ) const { UASSERT(maxGraphDepth >= 0); - //DEBUG("signatureId=%d maxGraphDepth=%d maxCheckedInDatabase=%d incrementMarginOnLoop=%d " + //UDEBUG("signatureId=%d maxGraphDepth=%d maxCheckedInDatabase=%d incrementMarginOnLoop=%d " // "ignoreLoopIds=%d ignoreIntermediateNodes=%d ignoreLocalSpaceLoopIds=%d", // signatureId, maxGraphDepth, maxCheckedInDatabase, incrementMarginOnLoop?1:0, // ignoreLoopIds?1:0, ignoreIntermediateNodes?1:0, ignoreLocalSpaceLoopIds?1:0); @@ -1711,17 +1718,19 @@ std::map Memory::getNeighborsId( std::map tmpLandmarks; const std::multimap * links = &tmpLinks; const std::map * landmarks = &tmpLandmarks; + bool isIntermediateNode = false; if(s) { - if(!ignoreIntermediateNodes || s->getWeight() != -1) + isIntermediateNode = s->getWeight() == -1; + if(!ignoreIntermediateNodes || !isIntermediateNode) { - ids.insert(std::pair(*jter, m)); + int effectiveMargin = m>0 && isIntermediateNode>0 ? m-1 : m; + ids.insert(std::pair(s->id(), effectiveMargin)); } else { - ignoredIds.insert(*jter); + ignoredIds.insert(s->id()); } - links = &s->getLinks(); if(!ignoreLoopIds) { @@ -1730,8 +1739,22 @@ std::map Memory::getNeighborsId( } else if(maxCheckedInDatabase == -1 || (maxCheckedInDatabase > 0 && _dbDriver && nbLoadedFromDb < maxCheckedInDatabase)) { - ++nbLoadedFromDb; - ids.insert(std::pair(*jter, m)); + int weight = 0; + _dbDriver->getWeight(*jter, weight); + isIntermediateNode = weight == -1; + if(!ignoreIntermediateNodes || !isIntermediateNode) + { + int effectiveMargin = m>0 && isIntermediateNode>0 ? m-1 : m; + ids.insert(std::pair(*jter, effectiveMargin)); + if(!isIntermediateNode) + { + ++nbLoadedFromDb; + } + } + else + { + ignoredIds.insert(*jter); + } UTimer timer; _dbDriver->loadLinks(*jter, tmpLinks, ignoreLoopIds?Link::kAllWithoutLandmarks:Link::kAllWithLandmarks); @@ -1775,7 +1798,7 @@ std::map Memory::getNeighborsId( if(iter->second.type() == Link::kNeighbor || iter->second.type() == Link::kNeighborMerged) { - if(ignoreIntermediateNodes && s->getWeight()==-1) + if(isIntermediateNode) { // stay on the same margin if(currentMargin.insert(iter->first).second) @@ -1906,7 +1929,7 @@ int Memory::getNextId() int Memory::incrementMapId(std::map * reducedIds) { //don't increment if there is no location in the current map - const Signature * s = getLastWorkingSignature(); + const Signature * s = getLastWorkingSignature(false); if(s && s->mapId() == _idMapCount) { // New session! move all signatures from the STM to WM @@ -2286,12 +2309,30 @@ std::list Memory::forget(const std::set & ignoredIds) std::list signatures = this->getRemovableSignatures(1, ignoredIds); if(signatures.size()) { - Signature * s = dynamic_cast(signatures.front()); + Signature * s = dynamic_cast(signatures.front()); if(s) { - signaturesRemoved.push_back(s->id()); + int refId = s->id(); + signaturesRemoved.push_back(refId); + std::multimap neighborLinks = graph::filterLinks(s->getLinks(), Link::kNeighbor, true); this->moveToTrash(s); wordsRemoved = _vwd->getUnusedWordsSize(); + + // Remove all linked intermediate nodes at the same time (in both direction) + std::list idsToCheck(uKeysList(neighborLinks)); + while(!idsToCheck.empty()) + { + int id = idsToCheck.front(); + idsToCheck.pop_front(); + s = this->_getSignature(id); + if(s && s->getWeight() == -1) + { + neighborLinks = graph::filterLinks(s->getLinks(), Link::kNeighbor, true); + uAppend(idsToCheck, uKeysList(neighborLinks)); + signaturesRemoved.push_back(s->id()); + this->moveToTrash(s); + } + } } else { @@ -2310,22 +2351,56 @@ std::list Memory::forget(const std::set & ignoredIds) UDEBUG(""); // Remove one more than total added during the iteration int signaturesAdded = _signaturesAdded; - std::list signatures = getRemovableSignatures(signaturesAdded+1, ignoredIds); - for(std::list::iterator iter=signatures.begin(); iter!=signatures.end(); ++iter) + int intermediateNodesRemoved = 0; + while(int(signaturesRemoved.size()-intermediateNodesRemoved) < signaturesAdded+1) { - signaturesRemoved.push_back((*iter)->id()); - // When a signature is deleted, it notifies the memory - // and it is removed from the memory list - this->moveToTrash(*iter); + std::list signatures = this->getRemovableSignatures(1, ignoredIds); + if(signatures.size()) + { + Signature * s = dynamic_cast(signatures.front()); + if(s) + { + signaturesRemoved.push_back(s->id()); + std::multimap neighborLinks = graph::filterLinks(s->getLinks(), Link::kNeighbor, true); + // When a signature is deleted, it notifies the memory + // and it is removed from the memory list + this->moveToTrash(s); + + // Remove all linked intermediate nodes at the same time (in both direction) + std::list idsToCheck(uKeysList(neighborLinks)); + while(!idsToCheck.empty()) + { + int id = idsToCheck.front(); + idsToCheck.pop_front(); + s = this->_getSignature(id); + if(s && s->getWeight() == -1) + { + ++intermediateNodesRemoved; + neighborLinks = graph::filterLinks(s->getLinks(), Link::kNeighbor, true); + uAppend(idsToCheck, uKeysList(neighborLinks)); + signaturesRemoved.push_back(s->id()); + this->moveToTrash(s); + } + } + } + else + { + break; + } + } + else + { + break; + } } - if((int)signatures.size() < signaturesAdded) + if(int(signaturesRemoved.size() - intermediateNodesRemoved) < signaturesAdded) { - UWARN("Less signatures transferred (%d) than added (%d)! The working memory cannot decrease in size.", - (int)signatures.size(), signaturesAdded); + UWARN("Less signatures transferred (%d, inter=%d) than added (%d)! The working memory cannot decrease in size.", + int(signaturesRemoved.size()-intermediateNodesRemoved), intermediateNodesRemoved, signaturesAdded); } else { - UDEBUG("signaturesRemoved=%d, _signaturesAdded=%d", (int)signatures.size(), signaturesAdded); + UDEBUG("signaturesRemoved=%d (inter=%d), _signaturesAdded=%d", int(signaturesRemoved.size()-intermediateNodesRemoved), intermediateNodesRemoved, signaturesAdded); } } return signaturesRemoved; @@ -2578,9 +2653,10 @@ std::list Memory::getRemovableSignatures(int count, const std::set< break; } } - if(!foundInSTM) + if(!foundInSTM && s->getWeight()>=0) { - // less weighted signature priority to be transferred + // Less weighted signature priority to be transferred + // Ignore intermediate nodes weightAgeIdMap.insert(std::make_pair(WeightAgeIdKey(s->getWeight(), _transferSortingByWeightId?0.0:memIter->second, s->id()), s)); } } @@ -2794,9 +2870,21 @@ int Memory::getLastSignatureId() const return _idCount; } -const Signature * Memory::getLastWorkingSignature() const +const Signature * Memory::getLastWorkingSignature(bool ignoreIntermediateNodes) const { - UDEBUG(""); + if(ignoreIntermediateNodes && _lastSignature && _lastSignature->getWeight()==-1) + { + for(std::map::const_reverse_iterator iter=_signatures.rbegin(); + iter!=_signatures.rend(); + ++iter) + { + if(iter->second->getWeight() != -1) + { + return iter->second; + } + } + return 0; + } return _lastSignature; } @@ -4785,6 +4873,11 @@ void Memory::copyData(const Signature * from, Signature * to) } to->sensorData().setId(to->id()); + if(!from->sensorData().globalDescriptors().empty()) + { + to->sensorData().setGlobalDescriptors(from->sensorData().globalDescriptors()); + } + to->setPose(from->getPose()); } else @@ -4817,6 +4910,12 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor bool isIntermediateNode = data.id() < 0; + if(this->getSignatures().empty() && isIntermediateNode) + { + UWARN("Ignoring input data with stamp %s because the first node in memory cannot be an intermediate node.", inputData.stamp()); + return 0; + } + // uncompress data if needed if(!isIntermediateNode) @@ -5039,7 +5138,7 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor int treeSize= int(_workingMem.size() + _stMem.size()); int meanWordsPerLocation = _feature2D->getMaxFeatures()>0?_feature2D->getMaxFeatures():0; - if(treeSize > 1) + if(meanWordsPerLocation==0 && treeSize > 1) { meanWordsPerLocation = _vwd->getTotalActiveReferences() / (treeSize-1); // ignore virtual signature } @@ -6767,7 +6866,7 @@ void Memory::enableWordsRef(const std::list & signatureIds) { if(keys.at(i)>0) { - if(_vwd->addWordRef(keys.at(i), (*j)->id())) + if(!_vwd->addWordRef(keys.at(i), (*j)->id())) { UERROR("Could not add word ref %d to node %d!?", keys.at(i), (*j)->id()); } @@ -6790,6 +6889,7 @@ std::set Memory::reactivateSignatures(const std::list & ids, unsigned UDEBUG(""); UTimer timer; std::list idsToLoad; + std::list idsInQueue; std::map::iterator wmIter; for(std::list::const_iterator i=ids.begin(); i!=ids.end(); ++i) { @@ -6800,10 +6900,14 @@ std::set Memory::reactivateSignatures(const std::list & ids, unsigned idsToLoad.push_back(*i); UINFO("Loading location %d from database...", *i); } + else if(idsToLoad.size() >= maxLoaded) + { + idsInQueue.push_back(*i); + } } } - UDEBUG("idsToLoad = %d", idsToLoad.size()); + UDEBUG("idsToLoad = %ld (in queue = %ld)", idsToLoad.size(), idsInQueue.size()); std::list reactivatedSigns; if(_dbDriver) @@ -6812,8 +6916,13 @@ std::set Memory::reactivateSignatures(const std::list & ids, unsigned } timeDbAccess = timer.getElapsedTime(); std::list idsLoaded; + int intermediateNodesLoaded = 0; for(std::list::iterator i=reactivatedSigns.begin(); i!=reactivatedSigns.end(); ++i) { + if((*i)->getWeight() == -1) + { + ++intermediateNodesLoaded; + } if(!(*i)->getLandmarks().empty()) { // Update landmark indexes @@ -6857,10 +6966,23 @@ std::set Memory::reactivateSignatures(const std::list & ids, unsigned } this->enableWordsRef(idsLoaded); UDEBUG("time = %fs", timer.ticks()); - return std::set(idsToLoad.begin(), idsToLoad.end()); + + std::set totalLoaded(idsToLoad.begin(), idsToLoad.end()); + + // Ignore intermediate nodes in the total count of signatures loaded, keep loading next in queue + if(intermediateNodesLoaded > 0 && (int)idsInQueue.size() >= intermediateNodesLoaded) + { + double queueTimeDbAccess = 0.0; + std::set queueLoaded = reactivateSignatures(idsInQueue, maxLoaded-intermediateNodesLoaded, queueTimeDbAccess); + timeDbAccess += queueTimeDbAccess; + totalLoaded.insert(queueLoaded.begin(), queueLoaded.end()); + } + + return totalLoaded; } -// return all non-null poses +// returns all non-null poses and links +// if lookInDatabase is false, intermediate nodes are ignored and new neighbor links between non-intermediate nodes are returned // return unique links between nodes (for neighbors: old->new, for loops: parent->child) void Memory::getMetricConstraints( const std::set & ids, @@ -6883,6 +7005,13 @@ void Memory::getMetricConstraints( { if(uContains(poses, *iter)) { + const Signature * s = lookInDatabase?0:this->getSignature(*iter); // If we look in db, we don't ignore intermediate nodes + if(s && s->getWeight() == -1) + { + poses.erase(*iter); + continue; + } + std::multimap tmpLinks = getLinks(*iter, lookInDatabase, true); for(std::multimap::iterator jter=tmpLinks.begin(); jter!=tmpLinks.end(); ++jter) { @@ -6895,14 +7024,15 @@ void Memory::getMetricConstraints( (jter->second.type() == Link::kNeighbor || jter->second.type() == Link::kNeighborMerged)) { - const Signature * s = this->getSignature(jter->first); + s = this->getSignature(jter->first); UASSERT(s!=0); if(s->getWeight() == -1) { + bool validLink = false; Link link = jter->second; while(s && s->getWeight() == -1) { - // skip to next neighbor, well we assume that bad signatures + // skip to next neighbor, well we assume that intermediate signatures // are only linked by max 2 neighbor links. std::multimap n = this->getNeighborLinks(s->id(), false); UASSERT(n.size() <= 2); @@ -6915,15 +7045,29 @@ void Memory::getMetricConstraints( link = link.merge(uter->second, uter->second.type()); poses.erase(s->id()); s = s2; + validLink = s->getWeight() != -1; + } + else + { + validLink = false; + break; } } else { + validLink = false; break; } } - links.insert(std::make_pair(*iter, link)); + if(validLink) + { + links.insert(std::make_pair(*iter, link)); + } + else + { + poses.erase(s->id()); + } } else { diff --git a/corelib/src/Optimizer.cpp b/corelib/src/Optimizer.cpp index 86be75dc..9a76f0a9 100644 --- a/corelib/src/Optimizer.cpp +++ b/corelib/src/Optimizer.cpp @@ -240,7 +240,8 @@ void Optimizer::getConnectedGraph( { UDEBUG("IN: fromId=%d poses=%d links=%d priorsIgnored=%d landmarksIgnored=%d", fromId, (int)posesIn.size(), (int)linksIn.size(), priorsIgnored()?1:0, landmarksIgnored()?1:0); UASSERT(fromId>0); - UASSERT(uContains(posesIn, fromId)); + UASSERT_MSG(uContains(posesIn, fromId), uFormat("poses=%ld (first=%d last=%d) fromId=%d", + posesIn.size(), posesIn.empty()?0:posesIn.begin()->first, posesIn.empty()?0:posesIn.rbegin()->first, fromId).c_str()); posesOut.clear(); linksOut.clear(); diff --git a/corelib/src/Rtabmap.cpp b/corelib/src/Rtabmap.cpp index 37acc399..49b17a5e 100644 --- a/corelib/src/Rtabmap.cpp +++ b/corelib/src/Rtabmap.cpp @@ -382,7 +382,7 @@ void Rtabmap::init(const ParametersMap & parameters, const std::string & databas { cv::Mat cov; this->optimizeCurrentMap( - !_optimizeFromGraphEnd?_memory->getWorkingMem().lower_bound(1)->first:_memory->getWorkingMem().rbegin()->first, + _memory->getLastWorkingSignature(true)->id(), false, _optimizedPoses, cov, &_constraints); } if(_optimizedPoses.lower_bound(1) != _optimizedPoses.end()) @@ -845,7 +845,7 @@ int Rtabmap::getTotalMemSize() const { if(_memory) { - const Signature * s =_memory->getLastWorkingSignature(); + const Signature * s =_memory->getLastWorkingSignature(false); if(s) { return s->id(); @@ -896,11 +896,11 @@ void Rtabmap::setInitialPose(const Transform & initialPose) _mapCorrection.setIdentity(); _mapCorrectionBackup.setNull(); - if(_memory->getLastWorkingSignature()->id() && + if(_memory->getLastWorkingSignature(true)->id() && _optimizedPoses.empty()) { cv::Mat covariance; - this->optimizeCurrentMap(_memory->getLastWorkingSignature()->id(), false, _optimizedPoses, covariance, &_constraints); + this->optimizeCurrentMap(_memory->getLastWorkingSignature(true)->id(), false, _optimizedPoses, covariance, &_constraints); } } else @@ -967,9 +967,9 @@ bool Rtabmap::labelLocation(int id, const std::string & label) { return _memory->labelSignature(id, label); } - else if(_memory->isIncremental() && _memory->getLastWorkingSignature()) + else if(_memory->isIncremental() && _memory->getLastWorkingSignature(true)) { - return _memory->labelSignature(_memory->getLastWorkingSignature()->id(), label); + return _memory->labelSignature(_memory->getLastWorkingSignature(true)->id(), label); } else if(!_memory->isIncremental() && !_lastLocalizationPose.isNull() && !_lastLocalizationPose.isIdentity()) { @@ -1003,9 +1003,9 @@ bool Rtabmap::setUserData(int id, const cv::Mat & data) { return _memory->setUserData(id, data); } - else if(_memory->getLastWorkingSignature()) + else if(_memory->getLastWorkingSignature(true)) { - return _memory->setUserData(_memory->getLastWorkingSignature()->id(), data); + return _memory->setUserData(_memory->getLastWorkingSignature(true)->id(), data); } else { @@ -1049,7 +1049,7 @@ void Rtabmap::generateDOTGraph(const std::string & path, int id, int margin) void Rtabmap::exportPoses(const std::string & path, bool optimized, bool global, int format) { - if(_memory && _memory->getLastWorkingSignature()) + if(_memory && _memory->getLastWorkingSignature(!global)) { std::map poses; std::multimap constraints; @@ -1057,11 +1057,11 @@ void Rtabmap::exportPoses(const std::string & path, bool optimized, bool global, if(optimized) { cv::Mat covariance; - this->optimizeCurrentMap(_memory->getLastWorkingSignature()->id(), global, poses, covariance, &constraints); + this->optimizeCurrentMap(_memory->getLastWorkingSignature(!global)->id(), global, poses, covariance, &constraints); } else { - std::map ids = _memory->getNeighborsId(_memory->getLastWorkingSignature()->id(), 0, global?-1:0, true); + std::map ids = _memory->getNeighborsId(_memory->getLastWorkingSignature(!global)->id(), 0, global?-1:0, true); _memory->getMetricConstraints(uKeysSet(ids), poses, constraints, global); } @@ -1112,11 +1112,15 @@ void Rtabmap::resetMemory() if(_memory) { + if(_memory->isReadOnly()) + { + UWARN("Memory is reset but the database won't be cleared because read-only mode is enabled."); + } _memory->init(_databasePath, true, _parameters, true); - if(_memory->getLastWorkingSignature()) + if(_memory->getLastWorkingSignature(true)) { cv::Mat covariance; - optimizeCurrentMap(_memory->getLastWorkingSignature()->id(), false, _optimizedPoses, covariance, &_constraints); + optimizeCurrentMap(_memory->getLastWorkingSignature(true)->id(), false, _optimizedPoses, covariance, &_constraints); } if(_bayesFilter) { @@ -1424,9 +1428,9 @@ bool Rtabmap::process( else if(_memory->isIncremental()) // only in mapping mode { // Detect if the odometry is reset. If yes, trigger a new map. - if(_memory->getLastWorkingSignature()) + if(_memory->getLastWorkingSignature(false)) { - const Transform & lastPose = _memory->getLastWorkingSignature()->getPose(); // use raw odometry + const Transform & lastPose = _memory->getLastWorkingSignature(false)->getPose(); // use raw odometry // look for identity if(!lastPose.isIdentity() && odomPose.isIdentity()) @@ -1473,14 +1477,14 @@ bool Rtabmap::process( } } - signature = _memory->getLastWorkingSignature(); + signature = _memory->getLastWorkingSignature(false); _currentSessionHasGPS = _currentSessionHasGPS || signature->sensorData().gps().stamp() > 0.0; if(!signature) { UFATAL("Not supposed to be here...last signature is null?!?"); } - ULOGGER_INFO("Processing signature %d w=%d map=%d", signature->id(), signature->getWeight(), signature->mapId()); + ULOGGER_INFO("Processing signature %d (%f) w=%d map=%d", signature->id(), signature->getStamp(), signature->getWeight(), signature->mapId()); timeMemoryUpdate = timer.ticks(); ULOGGER_INFO("timeMemoryUpdate=%fs", timeMemoryUpdate); @@ -1523,8 +1527,8 @@ bool Rtabmap::process( { const Signature * s = _memory->getSignature(links.begin()->second.to()); UASSERT(s!=0); - // don't filter if the new node is not intermediate but previous one is - if(signature->getWeight() < 0 || s->getWeight() >= 0) + // Check small motion if previous node is not an intermediate node (consecutive intermediate nodes are merged later) + if(s->getWeight() >= 0) { t = links.begin()->second.transform(); } @@ -2232,8 +2236,9 @@ bool Rtabmap::process( maxLocalLocationsImmunized = _localImmunizationRatio * float(_memory->getWorkingMem().size()); } // no need to do retrieval or immunization of locations if memory management - // is disabled and all nodes are in WM - if(!(_memory->allNodesInWM() && maxLocalLocationsImmunized == 0)) + // is disabled and all nodes are in WM. + // Also skip memory mangement on intermediate nodes + if(!(_memory->allNodesInWM() && maxLocalLocationsImmunized == 0) && signature->getWeight()>=0) { if(retrievalId > 0) { @@ -2373,11 +2378,13 @@ bool Rtabmap::process( // RETRIEVAL 2/3 : Update planned path and get next nodes to retrieve //============================================================ std::list retrievalLocalIds; - if(_rgbdSlamMode) + if(_rgbdSlamMode && signature->getWeight()>=0) { // Priority on locations on the planned path if(_path.size()) { + // Note: retrieval on path with intermediate nodes is not supported. Note that the planned path would + // eventually fail anyway because intermediate nodes are not in _optimizedPoses. updateGoalIndex(); float distanceSoFar = 0.0f; @@ -2500,11 +2507,13 @@ bool Rtabmap::process( { nearNodesByDist.insert(std::make_pair(iter->second, iter->first)); } - UINFO("near nodes=%d, max local immunized=%d, ratio=%f WM=%d", + UINFO("near nodes=%d, max local immunized=%d (immunized by path so far=%d), ratio=%f WM=%d", (int)nearNodesByDist.size(), maxLocalLocationsImmunized, + immunizedLocally, _localImmunizationRatio, (int)_memory->getWorkingMem().size()); + std::list retrievalLocalIdsIntermediate; for(std::multimap::iterator iter=nearNodesByDist.begin(); iter!=nearNodesByDist.end() && (retrievalLocalIds.size() < _maxLocalRetrieved || immunizedLocally < maxLocalLocationsImmunized); ++iter) @@ -2512,17 +2521,29 @@ bool Rtabmap::process( const Signature * s = _memory->getSignature(iter->second); if(s!=0) { - // If there is a change of direction, better to be retrieving - // ALL nearest signatures than only newest neighbors - const std::multimap & links = s->getLinks(); - for(std::multimap::const_reverse_iterator jter=links.rbegin(); - jter!=links.rend() && retrievalLocalIds.size() < _maxLocalRetrieved; - ++jter) + if(s->getWeight() != -1 && retrievalLocalIds.size() < _maxLocalRetrieved) { - if(_memory->getSignature(jter->first) == 0) + // If there is a change of direction, better to be retrieving + // all nearest signatures than only newest neighbors. + // Use getNeighborsId instead of direct links to support intermediate nodes. + std::map ids = _memory->getNeighborsId(s->id(), 2, _maxLocalRetrieved-retrievalLocalIds.size(), true, false, false); + for(std::map::const_reverse_iterator jter=ids.rbegin(); + jter!=ids.rend() && (retrievalLocalIds.size() < _maxLocalRetrieved || jter->second == 0); + ++jter) { - UINFO("retrieval of node %d on local map", jter->first); - retrievalLocalIds.push_back(jter->first); + if(_memory->getSignature(jter->first) == 0) + { + if(jter->second == 0) + { + UINFO("retrieval of intermediate node %d (margin=%d) on local map (from=%d)", jter->first, jter->second, s->id()); + retrievalLocalIdsIntermediate.push_back(jter->first); + } + else + { + UINFO("retrieval of node %d (margin=%d) on local map (from=%d)", jter->first, jter->second, s->id()); + retrievalLocalIds.push_back(jter->first); + } + } } } if(!_memory->isInSTM(s->id()) && immunizedLocally < maxLocalLocationsImmunized) @@ -2539,20 +2560,29 @@ bool Rtabmap::process( if(retrievalLocalIds.size() < _maxLocalRetrieved) { std::set retrievalLocalIdsSet(retrievalLocalIds.begin(), retrievalLocalIds.end()); + retrievalLocalIdsSet.insert(retrievalLocalIdsIntermediate.begin(), retrievalLocalIdsIntermediate.end()); for(std::list::iterator iter=retrievalLocalIds.begin(); iter!=retrievalLocalIds.end() && retrievalLocalIds.size() < _maxLocalRetrieved; ++iter) { - std::map ids = _memory->getNeighborsId(*iter, 2, _maxLocalRetrieved - (unsigned int)retrievalLocalIds.size() + 1, true, false); + std::map ids = _memory->getNeighborsId(*iter, 2, _maxLocalRetrieved - (unsigned int)retrievalLocalIds.size() + 1, true, false, false); for(std::map::reverse_iterator jter=ids.rbegin(); - jter!=ids.rend() && retrievalLocalIds.size() < _maxLocalRetrieved; + jter!=ids.rend() && (retrievalLocalIds.size() < _maxLocalRetrieved || jter->second == 0); ++jter) { if(_memory->getSignature(jter->first) == 0 && retrievalLocalIdsSet.find(jter->first) == retrievalLocalIdsSet.end()) { - UINFO("retrieval of node %d on local map", jter->first); - retrievalLocalIds.push_back(jter->first); + if(jter->second == 0) + { + UINFO("retrieval of intermediate node %d (margin=%d) on local map (from=%d)", jter->first, jter->second, *iter); + retrievalLocalIdsIntermediate.push_back(jter->first); + } + else + { + UINFO("retrieval of node %d (margin=%d) on local map (from=%d)", jter->first, jter->second, *iter); + retrievalLocalIds.push_back(jter->first); + } retrievalLocalIdsSet.insert(jter->first); } } @@ -2566,6 +2596,7 @@ bool Rtabmap::process( } // insert them first to make sure they are loaded. + reactivatedIds.insert(reactivatedIds.begin(), retrievalLocalIdsIntermediate.begin(), retrievalLocalIdsIntermediate.end()); reactivatedIds.insert(reactivatedIds.begin(), retrievalLocalIds.begin(), retrievalLocalIds.end()); } } @@ -3207,6 +3238,8 @@ bool Rtabmap::process( UDEBUG("Not self ref links: %d", (int)graph::filterLinks(signature->getLinks(), Link::kSelfRefLink).size()); if(_rgbdSlamMode + && + signature->getWeight() != -1 // Ignore graph optimization on intermediate nodes && (_loopClosureHypothesis.first>0 || lastProximitySpaceClosureId>0 || // can be different map of the current one @@ -4292,6 +4325,7 @@ bool Rtabmap::process( { _memory->addLink(Link(signature->id(), signature->id(), Link::kPosePrior, odomPose, odomCovariance.inv())); } + bool lastSignatureWasIntermediateNode = signature->getWeight() == -1; // remove last signature if the memory is not incremental or is a bad signature (if bad signatures are ignored) int signatureRemoved = _memory->cleanup(); @@ -4397,10 +4431,20 @@ bool Rtabmap::process( //============================================================ double totalTime = timerTotal.ticks(); ULOGGER_INFO("Total time processing = %fs...", totalTime); - if((_maxTimeAllowed != 0 && totalTime*1000>_maxTimeAllowed) || - (_maxMemoryAllowed != 0 && _memory->getWorkingMem().size() > _maxMemoryAllowed)) + if(!lastSignatureWasIntermediateNode && // skip memory management on intermediate nodes + ((_maxTimeAllowed != 0 && totalTime*1000>_maxTimeAllowed) || + (_maxMemoryAllowed != 0 && _memory->getWorkingMem().size() > _maxMemoryAllowed))) { - ULOGGER_INFO("Removing old signatures because time limit is reached %f>%f or memory is reached %d>%d...", totalTime*1000, _maxTimeAllowed, _memory->getWorkingMem().size(), _maxMemoryAllowed); + if(_maxTimeAllowed!=0 && totalTime*1000>_maxTimeAllowed) + { + ULOGGER_INFO("Removing old signatures because time limit is reached %f ms > %f ms...", + totalTime*1000, _maxTimeAllowed); + } + if(_maxMemoryAllowed != 0 && _memory->getWorkingMem().size() > _maxMemoryAllowed) + { + ULOGGER_INFO("Removing old signatures because memory limit is reached %d > %d...", + _memory->getWorkingMem().size(), _maxMemoryAllowed); + } immunizedLocations.insert(_lastLocalizationNodeId); // keep the latest localization in working memory std::list transferred = _memory->forget(immunizedLocations); signaturesRemoved.insert(signaturesRemoved.end(), transferred.begin(), transferred.end()); @@ -4448,9 +4492,9 @@ bool Rtabmap::process( } else if(_memory->isIncremental() && _optimizedPoses.size() && - _memory->getLastWorkingSignature()) + _memory->getLastWorkingSignature(true)) { - id = _memory->getLastWorkingSignature()->id(); + id = _memory->getLastWorkingSignature(true)->id(); UDEBUG("Refresh local map from %d", id); } UDEBUG("id=%d _optimizedPoses=%d", id, (int)_optimizedPoses.size()); @@ -4870,9 +4914,14 @@ void Rtabmap::setWorkingDirectory(std::string path) void Rtabmap::rejectLastLoopClosure() { - if(_memory && _memory->getStMem().find(getLastLocationId())!=_memory->getStMem().end()) + if(!_memory) { - std::multimap links = _memory->getLinks(getLastLocationId(), false); + return; + } + const Signature * lastS = _memory->getLastWorkingSignature(true); // last non-intermediate + if(lastS && _memory->getStMem().find(lastS->id())!=_memory->getStMem().end()) + { + std::multimap links = _memory->getLinks(lastS->id(), false); bool linksRemoved = false; for(std::multimap::iterator iter = links.begin(); iter!=links.end(); ++iter) { @@ -4908,7 +4957,7 @@ void Rtabmap::rejectLastLoopClosure() std::map poses = _optimizedPoses; std::multimap constraints; cv::Mat covariance; - optimizeCurrentMap(getLastLocationId(), false, poses, covariance, &constraints); + optimizeCurrentMap(lastS->id(), false, poses, covariance, &constraints); if(poses.empty()) { @@ -4919,7 +4968,7 @@ void Rtabmap::rejectLastLoopClosure() UINFO("Updated local map (old size=%d, new size=%d)", (int)_optimizedPoses.size(), (int)poses.size()); _optimizedPoses = poses; _constraints = constraints; - _mapCorrection = _optimizedPoses.at(_memory->getLastWorkingSignature()->id()) * _memory->getLastWorkingSignature()->getPose().inverse(); + _mapCorrection = _optimizedPoses.at(lastS->id()) * lastS->getPose().inverse(); } } } @@ -4931,6 +4980,13 @@ void Rtabmap::deleteLastLocation() if(_memory && _memory->getStMem().size()) { int lastId = *_memory->getStMem().rbegin(); + const Signature * s = _memory->getSignature(lastId); + UASSERT(s); + if(s->getWeight() == -1) + { + UERROR("Deleting last location with inermediate nodes is not supported. Aborting."); + return; + } _memory->deleteLocation(lastId); // we have to re-optimize the graph without the deleted location if(_memory->isIncremental() && _optimizedPoses.size()) @@ -4959,7 +5015,7 @@ void Rtabmap::deleteLastLocation() { std::multimap constraints; cv::Mat covariance; - optimizeCurrentMap(_memory->getLastWorkingSignature()->id(), false, poses, covariance, &constraints); + optimizeCurrentMap(_memory->getLastWorkingSignature(true)->id(), false, poses, covariance, &constraints); if(poses.empty()) { @@ -4969,7 +5025,7 @@ void Rtabmap::deleteLastLocation() { _optimizedPoses = poses; _constraints = constraints; - _mapCorrection = _optimizedPoses.at(_memory->getLastWorkingSignature()->id()) * _memory->getLastWorkingSignature()->getPose().inverse(); + _mapCorrection = _optimizedPoses.at(_memory->getLastWorkingSignature(true)->id()) * _memory->getLastWorkingSignature(true)->getPose().inverse(); } } } @@ -5211,11 +5267,43 @@ void Rtabmap::optimizeCurrentMap( UINFO("Optimize map: around location %d (lookInDatabase=%s)", id, lookInDatabase?"true":"false"); if(_memory && id > 0) { + if(!lookInDatabase && (!_memory->getSignature(id) || _memory->getSignature(id)->getWeight() == -1)) + { + UERROR("When doing a local optimization, the root id (%d) must exist and not be an intermediate node! Aborting...", id); + optimizedPoses.clear(); + if(constraints) + { + constraints->clear(); + } + return; + } + UTimer timer; std::map ids = _memory->getNeighborsId(id, 0, lookInDatabase?-1:0, true, false); if(!_optimizeFromGraphEnd && ids.size() > 1) { - id = ids.begin()->first; + if(lookInDatabase) + { + id = ids.begin()->first; + } + else + { + // Find first node that is not intermediate + for(auto pair: ids) + { + // Make sure fromId is not an intermediate node + const Signature * s = _memory->getSignature(pair.first); + if(s && s->getWeight() != -1) + { + id = pair.first; + break; + } + else if(!s) + { + UWARN("Not found node %d in memory?!", pair.first); + } + } + } } UINFO("get %d ids time %f s", (int)ids.size(), timer.ticks()); @@ -5569,7 +5657,7 @@ void Rtabmap::getGraph( bool withWords, bool withGlobalDescriptors) const { - if(_memory && _memory->getLastWorkingSignature()) + if(_memory && _memory->getLastWorkingSignature(!global)) { if(_rgbdSlamMode) { @@ -5577,7 +5665,7 @@ void Rtabmap::getGraph( { poses = _optimizedPoses; // guess cv::Mat covariance; - this->optimizeCurrentMap(_memory->getLastWorkingSignature()->id(), global, poses, covariance, &constraints); + this->optimizeCurrentMap(_memory->getLastWorkingSignature(!global)->id(), global, poses, covariance, &constraints); if(!global && !_optimizedPoses.empty()) { // We send directly the already optimized poses if they are set @@ -5587,14 +5675,14 @@ void Rtabmap::getGraph( } else { - std::map ids = _memory->getNeighborsId(_memory->getLastWorkingSignature()->id(), 0, global?-1:0, true); + std::map ids = _memory->getNeighborsId(_memory->getLastWorkingSignature(!global)->id(), 0, global?-1:0, true); _memory->getMetricConstraints(uKeysSet(ids), poses, constraints, global); } } else { // no optimization on appearance-only mode - std::map ids = _memory->getNeighborsId(_memory->getLastWorkingSignature()->id(), 0, global?-1:0, true); + std::map ids = _memory->getNeighborsId(_memory->getLastWorkingSignature(!global)->id(), 0, global?-1:0, true); _memory->getMetricConstraints(uKeysSet(ids), poses, constraints, global); } @@ -6242,7 +6330,7 @@ bool Rtabmap::addLink(const Link & link) std::map poses = _optimizedPoses; std::multimap links; cv::Mat covariance; - optimizeCurrentMap(this->getLastLocationId(), false, poses, covariance, &links); + optimizeCurrentMap(_memory->getLastWorkingSignature(true)->id(), false, poses, covariance, &links); if(poses.find(link.from()) == poses.end()) { @@ -6664,12 +6752,12 @@ bool Rtabmap::computePath(int targetNode, bool global) int currentNode = 0; if(_memory->isIncremental()) { - if(!_memory->getLastWorkingSignature()) + if(!_memory->getLastWorkingSignature(true)) { UWARN("Working memory is empty... cannot compute a path"); return false; } - currentNode = _memory->getLastWorkingSignature()->id(); + currentNode = _memory->getLastWorkingSignature(true)->id(); } else { @@ -6821,12 +6909,12 @@ bool Rtabmap::computePath(const Transform & targetPose, float tolerance) int currentNode = 0; if(_memory->isIncremental()) { - if(!_memory->getLastWorkingSignature()) + if(!_memory->getLastWorkingSignature(true)) { UWARN("Working memory is empty... cannot compute a path"); return false; } - currentNode = _memory->getLastWorkingSignature()->id(); + currentNode = _memory->getLastWorkingSignature(true)->id(); } else { @@ -6980,6 +7068,7 @@ void Rtabmap::updateGoalIndex() if( _memory && _path.size()) { // remove all previous virtual links + bool hasIntermediateNodes = false; for(unsigned int i=0; i<_pathCurrentIndex && i<_path.size(); ++i) { const Signature * s = _memory->getSignature(_path[i].first); @@ -6987,6 +7076,10 @@ void Rtabmap::updateGoalIndex() { _memory->removeVirtualLinks(s->id()); } + if(s->getWeight() == -1) + { + hasIntermediateNodes = true; + } } // for the current index, only keep the newest virtual link @@ -7015,7 +7108,7 @@ void Rtabmap::updateGoalIndex() // Make sure the next signatures on the path are linked together float distanceSoFar = 0.0f; for(unsigned int i=_pathCurrentIndex+1; - i<_path.size(); + i<_path.size() && !hasIntermediateNodes; ++i) { if(i>0) @@ -7030,6 +7123,11 @@ void Rtabmap::updateGoalIndex() const Signature * s = _memory->getSignature(_path[i].first); if(s) { + if(s->getWeight() == -1) + { + hasIntermediateNodes = true; + break; + } if(!s->hasLink(_path[i-1].first) && _memory->getSignature(_path[i-1].first) != 0) { Transform virtualLoop = _path[i].second.inverse() * _path[i-1].second; @@ -7047,18 +7145,25 @@ void Rtabmap::updateGoalIndex() } } + if(hasIntermediateNodes) + { + UERROR("Cannot follow a path with a map containing intermediate nodes (not supported: don't use intermediate nodes if rtabmap's planner has to be used). Aborting current plan!"); + this->clearPath(-1); + return; + } + UDEBUG("current node = %d current goal = %d", _path[_pathCurrentIndex].first, _path[_pathGoalIndex].first); Transform currentPose; if(_memory->isIncremental()) { - if(_memory->getLastWorkingSignature() == 0 || - !uContains(_optimizedPoses, _memory->getLastWorkingSignature()->id())) + if(_memory->getLastWorkingSignature(true) == 0 || + !uContains(_optimizedPoses, _memory->getLastWorkingSignature(true)->id())) { UERROR("Last node is null in memory or not in optimized poses. Aborting the plan..."); this->clearPath(-1); return; } - currentPose = _optimizedPoses.at(_memory->getLastWorkingSignature()->id()); + currentPose = _optimizedPoses.at(_memory->getLastWorkingSignature(true)->id()); } else { diff --git a/corelib/src/RtabmapThread.cpp b/corelib/src/RtabmapThread.cpp index ad4c216b..1196b720 100644 --- a/corelib/src/RtabmapThread.cpp +++ b/corelib/src/RtabmapThread.cpp @@ -47,8 +47,7 @@ RtabmapThread::RtabmapThread(Rtabmap * rtabmap) : _dataBufferMaxSize(Parameters::defaultRtabmapImageBufferSize()), _rate(Parameters::defaultRtabmapDetectionRate()), _createIntermediateNodes(Parameters::defaultRtabmapCreateIntermediateNodes()), - _frameRateTimer(new UTimer()), - _previousStamp(0.0), + _previousStamp(-1.0), _rtabmap(rtabmap), _paused(false), lastPose_(Transform::getIdentity()) @@ -62,8 +61,6 @@ RtabmapThread::~RtabmapThread() UEventsManager::removeHandler(this); close(true); - - delete _frameRateTimer; } void RtabmapThread::pushNewState(State newState, const RtabmapEventCmd & cmdEvent) @@ -88,7 +85,7 @@ void RtabmapThread::clearBufferedData() _newMapEvents.clear(); lastPose_.setIdentity(); covariance_ = cv::Mat(); - _previousStamp = 0; + _previousStamp = -1; } _dataMutex.unlock(); @@ -500,9 +497,10 @@ void RtabmapThread::addData(const OdometryEvent & odomEvent) bool ignoreFrame = false; if(_rate>0.0f) { - if((_previousStamp>=0.0 && odomEvent.data().stamp()>_previousStamp && odomEvent.data().stamp() - _previousStamp < 1.0f/_rate) || - ((_previousStamp<=0.0 || odomEvent.data().stamp()<=_previousStamp) && _frameRateTimer->getElapsedTime() < 1.0f/_rate)) + if((_previousStamp>=0.0 && odomEvent.data().stamp()>_previousStamp && odomEvent.data().stamp() - _previousStamp < 1.0f/_rate)) { + UDEBUG("Ignoring frame %f (previous stamp=%f, period=%f)", + odomEvent.data().stamp(), _previousStamp, 1.0/_rate); ignoreFrame = true; } } @@ -540,7 +538,6 @@ void RtabmapThread::addData(const OdometryEvent & odomEvent) } else if(!ignoreFrame) { - _frameRateTimer->start(); _previousStamp = odomEvent.data().stamp(); } diff --git a/corelib/src/odometry/OdometryMono.cpp b/corelib/src/odometry/OdometryMono.cpp index c8e7f1be..b942aa88 100644 --- a/corelib/src/odometry/OdometryMono.cpp +++ b/corelib/src/odometry/OdometryMono.cpp @@ -239,7 +239,7 @@ Transform OdometryMono::computeTransform(SensorData & data, const Transform & gu { UDEBUG(""); bool newPtsAdded = false; - const Signature * newS = memory_->getLastWorkingSignature(); + const Signature * newS = memory_->getLastWorkingSignature(false); UDEBUG("newWords=%d", (int)newS->getWords().size()); nFeatures = (int)newS->getWords().size(); if((int)newS->getWords().size() > minInliers_) @@ -646,7 +646,7 @@ Transform OdometryMono::computeTransform(SensorData & data, const Transform & gu info->type = 1; } - const Signature * refS = memory_->getLastWorkingSignature(); + const Signature * refS = memory_->getLastWorkingSignature(false); std::vector refCorners(firstFrameGuessCorners_.size()); std::vector refCornersGuess(firstFrameGuessCorners_.size()); @@ -804,10 +804,10 @@ Transform OdometryMono::computeTransform(SensorData & data, const Transform & gu if(!refWords3.empty()) { UDEBUG("Added %d/%d valid 3D features", (int)refWords3.size(), (int)localMap_.size()); - keyFrameWords3D_.insert(std::make_pair(memory_->getLastWorkingSignature()->id(), refWords3)); + keyFrameWords3D_.insert(std::make_pair(memory_->getLastWorkingSignature(false)->id(), refWords3)); } - keyFramePoses_.insert(std::make_pair(memory_->getLastWorkingSignature()->id(), this->getPose())); - keyFrameModels_.insert(std::make_pair(memory_->getLastWorkingSignature()->id(), newModel)); + keyFramePoses_.insert(std::make_pair(memory_->getLastWorkingSignature(false)->id(), this->getPose())); + keyFrameModels_.insert(std::make_pair(memory_->getLastWorkingSignature(false)->id(), newModel)); } } else @@ -829,7 +829,7 @@ Transform OdometryMono::computeTransform(SensorData & data, const Transform & gu // generate kpts if(memory_->update(SensorData(data))) { - const Signature * s = memory_->getLastWorkingSignature(); + const Signature * s = memory_->getLastWorkingSignature(false); const std::multimap & words = s->getWords(); if((int)words.size() > minInliers_ && !s->getWordsKpts().empty()) { diff --git a/corelib/src/optimizer/OptimizerGTSAM.cpp b/corelib/src/optimizer/OptimizerGTSAM.cpp index 53c87edb..64b4106c 100644 --- a/corelib/src/optimizer/OptimizerGTSAM.cpp +++ b/corelib/src/optimizer/OptimizerGTSAM.cpp @@ -379,8 +379,8 @@ std::map OptimizerGTSAM::optimize( int id1 = iter->second.from(); int id2 = iter->second.to(); - UASSERT_MSG(poses.find(id1)!=poses.end(), uFormat("id1=%d", id1).c_str()); - UASSERT_MSG(poses.find(id2)!=poses.end(), uFormat("id2=%d", id2).c_str()); + UASSERT_MSG(poses.find(id1)!=poses.end(), uFormat("id1=%d for constraint %d->%d (type=%d)", id1, id1, id2, iter->second.type()).c_str()); + UASSERT_MSG(poses.find(id2)!=poses.end(), uFormat("id2=%d for constraint %d->%d (type=%d)", id2, id1, id2, iter->second.type()).c_str()); UASSERT(!iter->second.transform().isNull()); if(id1 == id2) diff --git a/guilib/src/DataRecorder.cpp b/guilib/src/DataRecorder.cpp index ebbac03d..940f29f7 100644 --- a/guilib/src/DataRecorder.cpp +++ b/guilib/src/DataRecorder.cpp @@ -140,7 +140,7 @@ void DataRecorder::addData(const rtabmap::SensorData & data, const Transform & p //save to database UTimer time; memory_->update(data, pose, covariance); - const Signature * s = memory_->getLastWorkingSignature(); + const Signature * s = memory_->getLastWorkingSignature(false); totalSizeKB_ += (int)s->sensorData().imageCompressed().total()/1000; totalSizeKB_ += (int)s->sensorData().depthOrRightCompressed().total()/1000; totalSizeKB_ += (int)s->sensorData().laserScanCompressed().data().total()/1000; diff --git a/guilib/src/DatabaseViewer.cpp b/guilib/src/DatabaseViewer.cpp index 303b9baa..b95bdb77 100644 --- a/guilib/src/DatabaseViewer.cpp +++ b/guilib/src/DatabaseViewer.cpp @@ -7930,8 +7930,7 @@ void DatabaseViewer::updateGraphView() // remove intermediate nodes? if(ui_->checkBox_ignoreIntermediateNodes->isVisible() && - ui_->checkBox_ignoreIntermediateNodes->isEnabled() && - ui_->checkBox_ignoreIntermediateNodes->isChecked()) + (ui_->checkBox_ignoreIntermediateNodes->isChecked() || ui_->comboBox_optimizationFlavor->currentIndex() == 2)) { for(std::multimap::iterator iter=links.begin(); iter!=links.end(); ++iter) { diff --git a/guilib/src/MainWindow.cpp b/guilib/src/MainWindow.cpp index 97f5ff5c..5b691262 100644 --- a/guilib/src/MainWindow.cpp +++ b/guilib/src/MainWindow.cpp @@ -585,6 +585,7 @@ MainWindow::MainWindow(PreferencesDialog * prefDialog, QWidget * parent, bool sh // Apply state this->changeState(kIdle); this->applyPrefSettings(PreferencesDialog::kPanelAll); + applyPrefSettings(parameters, false); _ui->statsToolBox->setNewFigureMaxItems(50); _ui->statsToolBox->setWorkingDirectory(_preferencesDialog->getWorkingDirectory()); @@ -709,10 +710,6 @@ MainWindow::MainWindow(PreferencesDialog * prefDialog, QWidget * parent, bool sh this->loadFigures(); connect(_ui->statsToolBox, SIGNAL(figuresSetupChanged()), this, SLOT(configGUIModified())); - // update loop closure viewer parameters - _loopClosureViewer->setDecimation(_preferencesDialog->getCloudDecimation(0)); - _loopClosureViewer->setMaxDepth(_preferencesDialog->getCloudMaxDepth(0)); - if (splash) { splash->close(); @@ -3254,14 +3251,17 @@ void MainWindow::updateMapCloud( } std::map posesWithOdomCache; - + std::set odomCachePosesIds; if(_ui->graphicsView_graphView->isVisible() || ((_preferencesDialog->isGraphsShown() || _preferencesDialog->isFrustumsShown(0)) && _currentPosesMap.size())) { posesWithOdomCache = posesIn; for(std::map::const_iterator iter=odomCachePoses.begin(); iter!=odomCachePoses.end(); ++iter) { - posesWithOdomCache.insert(std::make_pair(iter->first, _odometryCorrection*iter->second)); + if(posesWithOdomCache.insert(std::make_pair(iter->first, _odometryCorrection*iter->second)).second) + { + odomCachePosesIds.insert(iter->first); + } } } @@ -3520,7 +3520,7 @@ void MainWindow::updateMapCloud( std::multimap constraintsWithOdomCache; constraintsWithOdomCache = constraints; constraintsWithOdomCache.insert(odomCacheConstraints.begin(), odomCacheConstraints.end()); - _ui->graphicsView_graphView->updateGraph(posesWithOdomCache, constraintsWithOdomCache, mapIdsIn, std::map(), uKeysSet(odomCachePoses)); + _ui->graphicsView_graphView->updateGraph(posesWithOdomCache, constraintsWithOdomCache, mapIdsIn, std::map(), odomCachePosesIds); if(_preferencesDialog->isGroundTruthAligned() && !mapToGt.isIdentity()) { std::map gtPoses = _currentGTPosesMap; diff --git a/package.xml b/package.xml index c394fa77..4de2235c 100644 --- a/package.xml +++ b/package.xml @@ -1,7 +1,7 @@ rtabmap - 0.22.1 + 0.23.5 RTAB-Map's standalone library. RTAB-Map is a RGB-D SLAM approach with real-time constraints. Mathieu Labbe Mathieu Labbe From aa6d20775f71dcfa597046c5f28ba38c66546d9a Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sat, 25 Apr 2026 14:55:59 -0700 Subject: [PATCH 102/126] Init python interface if built with python (related to #1693) --- tools/ReduceGraph/main.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tools/ReduceGraph/main.cpp b/tools/ReduceGraph/main.cpp index eaabf7a2..5d44ab9e 100644 --- a/tools/ReduceGraph/main.cpp +++ b/tools/ReduceGraph/main.cpp @@ -37,6 +37,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#ifdef RTABMAP_PYTHON +#include +#endif #include #include #include @@ -115,6 +118,10 @@ int main(int argc, char * argv[]) printf(" keep_latest = %s\n", keepLatest?"true":"false"); printf(" keep_linked = %s\n", keepLinked?"true":"false"); printf(" pre_cleanup = %s\n", preCleanup?"true":"false"); + +#ifdef RTABMAP_PYTHON + rtabmap::PythonInterface pythonInterface; +#endif // Just parse logging options Parameters::parseArguments(argc, argv); From ebba7e487897b64857abd0c52900b981099dcfc2 Mon Sep 17 00:00:00 2001 From: Borong Yuan Date: Mon, 27 Apr 2026 06:33:05 +0800 Subject: [PATCH 103/126] Correct the Jacobian calculation of GravityFactor (#1692) * correct the Jacobian calculation of GravityFactor * reimplement GravityFactor's analytical Jacobian * adopt some minor changes from the GTSAM mainline * use GTSAM's native AttitudeFactor instead of GravityFactor * add missing header file and namespace prefix * Removed GravityFactor. Fixed build with latest GTSAM version from source. Require c++17 when building with GTSAM>=4.3.0. * Fixed cmake error when gtsam is not installed --------- Co-authored-by: matlabbe --- CMakeLists.txt | 2 +- corelib/src/CMakeLists.txt | 4 - corelib/src/optimizer/OptimizerGTSAM.cpp | 29 +- corelib/src/optimizer/gtsam/GravityFactor.cpp | 91 ------ corelib/src/optimizer/gtsam/GravityFactor.h | 271 ------------------ corelib/src/optimizer/gtsam/XYFactor.h | 4 +- corelib/src/optimizer/gtsam/XYZFactor.h | 6 +- .../vertigo/gtsam/betweenFactorSwitchable.h | 12 +- .../vertigo/gtsam/switchVariableLinear.h | 5 +- .../vertigo/gtsam/switchVariableSigmoid.h | 5 +- 10 files changed, 36 insertions(+), 393 deletions(-) delete mode 100644 corelib/src/optimizer/gtsam/GravityFactor.cpp delete mode 100644 corelib/src/optimizer/gtsam/GravityFactor.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 101457df..aa3f7fb5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -931,7 +931,7 @@ IF(NOT (APPLE OR WIN32) AND BUILD_WITH_RPATH_NOT_RUNPATH) ENDIF() IF(NOT MSVC) - IF(Qt6_FOUND OR (G2O_FOUND AND G2O_CPP11 EQUAL 1) OR TORCH_FOUND OR MRPT_FOUND) + IF(Qt6_FOUND OR (G2O_FOUND AND G2O_CPP11 EQUAL 1) OR (GTSAM_FOUND AND GTSAM_VERSION VERSION_GREATER_EQUAL "4.3.0") OR TORCH_FOUND OR MRPT_FOUND) # Qt6 requires c++17 include(CheckCXXCompilerFlag) CHECK_CXX_COMPILER_FLAG("-std=c++17" COMPILER_SUPPORTS_CXX17) diff --git a/corelib/src/CMakeLists.txt b/corelib/src/CMakeLists.txt index b275a535..f787c40f 100644 --- a/corelib/src/CMakeLists.txt +++ b/corelib/src/CMakeLists.txt @@ -804,10 +804,6 @@ IF(CUVSLAM_FOUND) ENDIF(CUVSLAM_FOUND) IF(GTSAM_FOUND) - SET(SRC_FILES - ${SRC_FILES} - optimizer/gtsam/GravityFactor.cpp - ) SET(LIBRARIES ${LIBRARIES} gtsam diff --git a/corelib/src/optimizer/OptimizerGTSAM.cpp b/corelib/src/optimizer/OptimizerGTSAM.cpp index 64b4106c..56ac51ba 100644 --- a/corelib/src/optimizer/OptimizerGTSAM.cpp +++ b/corelib/src/optimizer/OptimizerGTSAM.cpp @@ -51,7 +51,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include -#include "gtsam/GravityFactor.h" +#include #include #include #include @@ -121,7 +121,7 @@ void OptimizerGTSAM::parseParameters(const ParametersMap & parameters) params.relinearizeThreshold = threshold; params.relinearizeSkip = skip; params.evaluateNonlinearError = true; - isam2_ = new ISAM2(params); + isam2_ = new gtsam::ISAM2(params); addedPoses_.clear(); lastAddedConstraints_.clear(); @@ -392,7 +392,7 @@ std::map OptimizerGTSAM::optimize( { if(id1 < 0 && !isLandmarkWithRotation.at(id1)) { - noiseModel::Diagonal::shared_ptr model = noiseModel::Diagonal::Variances(Vector2( + gtsam::noiseModel::Diagonal::shared_ptr model = gtsam::noiseModel::Diagonal::Variances(gtsam::Vector2( 1/iter->second.infMatrix().at(0,0), 1/iter->second.infMatrix().at(1,1))); graph.add(XYFactor(id1, gtsam::Point2(iter->second.transform().x(), iter->second.transform().y()), model)); @@ -400,7 +400,7 @@ std::map OptimizerGTSAM::optimize( } else if (1 / static_cast(iter->second.infMatrix().at(5,5)) >= 9999.0) { - noiseModel::Diagonal::shared_ptr model = noiseModel::Diagonal::Variances(Vector2( + gtsam::noiseModel::Diagonal::shared_ptr model = gtsam::noiseModel::Diagonal::Variances(gtsam::Vector2( 1/iter->second.infMatrix().at(0,0), 1/iter->second.infMatrix().at(1,1))); graph.add(XYFactor(id1, gtsam::Point2(iter->second.transform().x(), iter->second.transform().y()), model)); @@ -431,7 +431,7 @@ std::map OptimizerGTSAM::optimize( { if(id1 < 0 && !isLandmarkWithRotation.at(id1)) { - noiseModel::Diagonal::shared_ptr model = noiseModel::Diagonal::Precisions(Vector3( + gtsam::noiseModel::Diagonal::shared_ptr model = gtsam::noiseModel::Diagonal::Precisions(gtsam::Vector3( iter->second.infMatrix().at(0,0), iter->second.infMatrix().at(1,1), iter->second.infMatrix().at(2,2))); @@ -442,7 +442,7 @@ std::map OptimizerGTSAM::optimize( 1 / static_cast(iter->second.infMatrix().at(4,4)) >= 9999.0 || 1 / static_cast(iter->second.infMatrix().at(5,5)) >= 9999.0) { - noiseModel::Diagonal::shared_ptr model = noiseModel::Diagonal::Precisions(Vector3( + gtsam::noiseModel::Diagonal::shared_ptr model = gtsam::noiseModel::Diagonal::Precisions(gtsam::Vector3( iter->second.infMatrix().at(0,0), iter->second.infMatrix().at(1,1), iter->second.infMatrix().at(2,2))); @@ -471,10 +471,17 @@ std::map OptimizerGTSAM::optimize( } else if(!isSlam2d() && gravitySigma() > 0 && iter->second.type() == Link::kGravity && newPoses.find(iter->first) != newPoses.end()) { - Vector3 r = gtsam::Pose3(iter->second.transform().toEigen4d()).rotation().xyz(); - gtsam::Unit3 nG = gtsam::Rot3::RzRyRx(r.x(), r.y(), 0).rotate(gtsam::Unit3(0,0,-1)); - gtsam::SharedNoiseModel model = gtsam::noiseModel::Isotropic::Sigmas(gtsam::Vector2(gravitySigma(), gravitySigma())); - graph.add(Pose3GravityFactor(iter->first, nG, model, Unit3(0,0,1))); + gtsam::Rot3 nRbMeas = gtsam::Pose3(iter->second.transform().toEigen4d()).rotation(); + gtsam::Unit3 nZ(0,0,1); + gtsam::Unit3 bGMeas = nRbMeas.unrotate(nZ); + gtsam::SharedNoiseModel model = gtsam::noiseModel::Isotropic::Sigma(2, gravitySigma()); +#if GTSAM_VERSION_NUMERIC <= 40300 + // Note: till 40301 is officially released, version 40300 with "4.3a1" would fail here. + // Just replace "<=" above by "<" to use AttitudeFactor below. + graph.add(gtsam::Pose3AttitudeFactor(iter->first, nZ, model, bGMeas)); +#else + graph.add(gtsam::AttitudeFactor(iter->first, nZ, model, bGMeas)); +#endif lastAddedConstraints_.push_back(ConstraintToFactor(iter->first, iter->first, -1)); } } @@ -783,7 +790,7 @@ std::map OptimizerGTSAM::optimize( { float x,y,z,roll,pitch,yaw; std::map tmpPoses; - const Values values = isam2_?isam2_->calculateEstimate():optimizer->values(); + const gtsam::Values values = isam2_?isam2_->calculateEstimate():optimizer->values(); #if GTSAM_VERSION_NUMERIC >= 40200 for(gtsam::Values::deref_iterator iter=values.begin(); iter!=values.end(); ++iter) #else diff --git a/corelib/src/optimizer/gtsam/GravityFactor.cpp b/corelib/src/optimizer/gtsam/GravityFactor.cpp deleted file mode 100644 index 81d0da9a..00000000 --- a/corelib/src/optimizer/gtsam/GravityFactor.cpp +++ /dev/null @@ -1,91 +0,0 @@ -/* ---------------------------------------------------------------------------- - - * GTSAM Copyright 2010, Georgia Tech Research Corporation, - * Atlanta, Georgia 30332-0415 - * All Rights Reserved - * Authors: Frank Dellaert, et al. (see THANKS for the full author list) - - * See LICENSE for the license information - - * -------------------------------------------------------------------------- */ - -/** - * Author: Mathieu Labbe - * This file is a copy of AttitudeFactor.cpp of gtsam library but - * with attitudeError() function overridden to ignore yaw errors. - * For the noise model, use Sigmas(Vector2(0.1, 10)) (with second sigma high!) - */ - -/** - * @file GravityFactor.cpp - * @author Frank Dellaert - * @brief Implementation file for Attitude factor - * @date January 28, 2014 - **/ - -#include "GravityFactor.h" - -using namespace std; - -namespace rtabmap { - -//*************************************************************************** -Vector GravityFactor::attitudeError(const Rot3& nRb, - OptionalJacobian<2, 3> H) const { - if (H) { - Matrix23 D_nRef_R; - Matrix22 D_e_nRef; - Vector3 r = nRb.xyz(); - Unit3 nRef = Rot3::RzRyRx(r.x(), r.y(), 0).rotate(bRef_, D_nRef_R); - Vector e = nZ_.error(nRef, D_e_nRef); - (*H) = D_e_nRef * D_nRef_R; - //printf("ref=%f %f %f grav=%f %f %f e= %f %f H=%f %f %f, %f %f %f\n", - // nRef.point3().x(), nRef.point3().y(), nRef.point3().z(), nZ_.point3().x(), nZ_.point3().y(), nZ_.point3().z(), e(0), e(1), - // (*H)(0,0), (*H)(0,1), (*H)(0,2), (*H)(1,0), (*H)(1,1), (*H)(1,2)); - return e; - } else { - Vector3 r = nRb.xyz(); - Unit3 nRef = Rot3::RzRyRx(r.x(), r.y(), 0) * bRef_; - Vector e = nZ_.error(nRef); - //printf("ref=%f %f %f grav=%f %f %f e= %f %f\n", nRef.point3().x(), nRef.point3().y(), nRef.point3().z(), nZ_.point3().x(), nZ_.point3().y(), nZ_.point3().z(), e(0), e(1)); - return e; - } -} - -//*************************************************************************** -void Rot3GravityFactor::print(const string& s, - const KeyFormatter& keyFormatter) const { - cout << s << "Rot3GravityFactor on " << keyFormatter(this->key()) << "\n"; - nZ_.print(" measured direction in nav frame: "); - bRef_.print(" reference direction in body frame: "); - this->noiseModel_->print(" noise model: "); -} - -//*************************************************************************** -bool Rot3GravityFactor::equals(const NonlinearFactor& expected, - double tol) const { - const This* e = dynamic_cast(&expected); - return e != NULL && Base::equals(*e, tol) && this->nZ_.equals(e->nZ_, tol) - && this->bRef_.equals(e->bRef_, tol); -} - -//*************************************************************************** -void Pose3GravityFactor::print(const string& s, - const KeyFormatter& keyFormatter) const { - cout << s << "Pose3GravityFactor on " << keyFormatter(this->key()) << "\n"; - nZ_.print(" measured direction in nav frame: "); - bRef_.print(" reference direction in body frame: "); - this->noiseModel_->print(" noise model: "); -} - -//*************************************************************************** -bool Pose3GravityFactor::equals(const NonlinearFactor& expected, - double tol) const { - const This* e = dynamic_cast(&expected); - return e != NULL && Base::equals(*e, tol) && this->nZ_.equals(e->nZ_, tol) - && this->bRef_.equals(e->bRef_, tol); -} - -//*************************************************************************** - -}/// namespace gtsam diff --git a/corelib/src/optimizer/gtsam/GravityFactor.h b/corelib/src/optimizer/gtsam/GravityFactor.h deleted file mode 100644 index 87fb9312..00000000 --- a/corelib/src/optimizer/gtsam/GravityFactor.h +++ /dev/null @@ -1,271 +0,0 @@ -/* ---------------------------------------------------------------------------- - - * GTSAM Copyright 2010, Georgia Tech Research Corporation, - * Atlanta, Georgia 30332-0415 - * All Rights Reserved - * Authors: Frank Dellaert, et al. (see THANKS for the full author list) - - * See LICENSE for the license information - - * -------------------------------------------------------------------------- */ - -/** - * Author: Mathieu Labbe - * This file is a copy of AttitudeFactor.h of gtsam library but - * with attitudeError() function overridden to ignore yaw errors. - * For the noise model, use Sigmas(Vector2(0.1, 10)) (with second sigma high!) - */ - -/** - * @file Pose3GravityFactor.h - * @author Frank Dellaert - * @brief Header file for Attitude factor - * @date January 28, 2014 - **/ -#pragma once - -#include -#if GTSAM_VERSION_NUMERIC >= 40300 && defined(GTSAM_WITH_NOISE_MODEL_FACTOR_N) -#include -#endif -#include -#include - -using namespace gtsam; - -namespace rtabmap { - -/** - * Base class for prior on gravity - * Example: - * - measurement is direction of gravity in navigation frame nG - * - reference is direction of z axis in body frame bF - * This factor will give zero error if nG is opposite direction of bF - * @addtogroup Navigation - */ -class GravityFactor { - -protected: - - const Unit3 nZ_, bRef_; ///< Position measurement in - -public: - - /** default constructor - only use for serialization */ - GravityFactor() { - } - - /** - * @brief Constructor - * @param nZ measured direction in navigation frame - * @param bRef reference direction in body frame (default Z-axis in NED frame, i.e., [0; 0; 1]) - */ - GravityFactor(const Unit3& nZ, const Unit3& bRef = Unit3(0, 0, 1)) : - nZ_(nZ), bRef_(bRef) { - } - - /** vector of errors */ - Vector attitudeError(const Rot3& p, -#if GTSAM_VERSION_NUMERIC >= 40300 - OptionalJacobian<2,3> H = {}) const; -#else - OptionalJacobian<2,3> H = boost::none) const; -#endif - - /** Serialization function */ -#if defined(GTSAM_ENABLE_BOOST_SERIALIZATION) || GTSAM_VERSION_NUMERIC < 40300 - friend class boost::serialization::access; - template - void serialize(ARCHIVE & ar, const unsigned int /*version*/) { - /*ar & boost::serialization::make_nvp("nZ_", const_cast(nZ_)); - ar & boost::serialization::make_nvp("bRef_", const_cast(bRef_));*/ - } -#endif -}; - -/** - * Version of GravityFactor for Rot3 - * @addtogroup Navigation - */ -class Rot3GravityFactor: public NoiseModelFactor1, public GravityFactor { - - typedef NoiseModelFactor1 Base; - -public: - - /// shorthand for a smart pointer to a factor -#if GTSAM_VERSION_NUMERIC >= 40300 - typedef std::shared_ptr shared_ptr; - #else - typedef boost::shared_ptr shared_ptr; -#endif - - /// Typedef to this class - typedef Rot3GravityFactor This; - - /** default constructor - only use for serialization */ - Rot3GravityFactor() { - } - - virtual ~Rot3GravityFactor() { - } - - /** - * @brief Constructor - * @param key of the Rot3 variable that will be constrained - * @param nZ measured direction in navigation frame (remove yaw before rotating the gravity vector) - * @param model Gaussian noise model - * @param bRef reference direction in body frame (default Z-axis) - */ - Rot3GravityFactor(Key key, const Unit3& nZ, const SharedNoiseModel& model, - const Unit3& bRef = Unit3(0, 0, 1)) : - Base(model, key), GravityFactor(nZ, bRef) { - } - - /// @return a deep copy of this factor - virtual gtsam::NonlinearFactor::shared_ptr clone() const { -#if GTSAM_VERSION_NUMERIC >= 40300 - return std::static_pointer_cast( -#else - return boost::static_pointer_cast( -#endif - gtsam::NonlinearFactor::shared_ptr(new This(*this))); - } - - /** print */ - virtual void print(const std::string& s, const KeyFormatter& keyFormatter = - DefaultKeyFormatter) const; - - /** equals */ - virtual bool equals(const NonlinearFactor& expected, double tol = 1e-9) const; - - /** vector of errors */ - virtual Vector evaluateError(const Rot3& nRb, // -#if GTSAM_VERSION_NUMERIC >= 40300 - OptionalMatrixType H = OptionalNone) const { -#else - boost::optional H = boost::none) const { -#endif - return attitudeError(nRb, H); - } - Unit3 nZ() const { - return nZ_; - } - Unit3 bRef() const { - return bRef_; - } - -private: -#if defined(GTSAM_ENABLE_BOOST_SERIALIZATION) || GTSAM_VERSION_NUMERIC < 40300 - /** Serialization function */ - friend class boost::serialization::access; - template - void serialize(ARCHIVE & ar, const unsigned int /*version*/) { - /*ar & boost::serialization::make_nvp("NoiseModelFactor1", - boost::serialization::base_object(*this)); - ar & boost::serialization::make_nvp("GravityFactor", - boost::serialization::base_object(*this));*/ - } -#endif - -public: - EIGEN_MAKE_ALIGNED_OPERATOR_NEW -}; - - -/** - * Version of GravityFactor for Pose3 - * @addtogroup Navigation - */ -class Pose3GravityFactor: public NoiseModelFactor1, - public GravityFactor { - - typedef NoiseModelFactor1 Base; - -public: - - /// shorthand for a smart pointer to a factor -#if GTSAM_VERSION_NUMERIC >= 40300 - typedef std::shared_ptr shared_ptr; -#else - typedef boost::shared_ptr shared_ptr; -#endif - /// Typedef to this class - typedef Pose3GravityFactor This; - - /** default constructor - only use for serialization */ - Pose3GravityFactor() { - } - - virtual ~Pose3GravityFactor() { - } - - /** - * @brief Constructor - * @param key of the Pose3 variable that will be constrained - * @param nZ measured direction in navigation frame (remove yaw before rotating the gravity vector) - * @param model Gaussian noise model - * @param bRef reference direction in body frame (default Z-axis) - */ - Pose3GravityFactor(Key key, const Unit3& nZ, const SharedNoiseModel& model, - const Unit3& bRef = Unit3(0, 0, 1)) : - Base(model, key), GravityFactor(nZ, bRef) { - } - - /// @return a deep copy of this factor - virtual gtsam::NonlinearFactor::shared_ptr clone() const { -#if GTSAM_VERSION_NUMERIC >= 40300 - return std::static_pointer_cast( -#else - return boost::static_pointer_cast( -#endif - gtsam::NonlinearFactor::shared_ptr(new This(*this))); - } - - /** print */ - virtual void print(const std::string& s, const KeyFormatter& keyFormatter = - DefaultKeyFormatter) const; - - /** equals */ - virtual bool equals(const NonlinearFactor& expected, double tol = 1e-9) const; - - /** vector of errors */ - virtual Vector evaluateError(const Pose3& nTb, // -#if GTSAM_VERSION_NUMERIC >= 40300 - OptionalMatrixType H = OptionalNone) const { -#else - boost::optional H = boost::none) const { -#endif - Vector e = attitudeError(nTb.rotation(), H); - if (H) { - Matrix H23 = *H; - *H = Matrix::Zero(2,6); - H->block<2,3>(0,0) = H23; - } - return e; - } - Unit3 nZ() const { - return nZ_; - } - Unit3 bRef() const { - return bRef_; - } - -private: -#if defined(GTSAM_ENABLE_BOOST_SERIALIZATION) || GTSAM_VERSION_NUMERIC < 40300 - /** Serialization function */ - friend class boost::serialization::access; - template - void serialize(ARCHIVE & ar, const unsigned int /*version*/) { - /*ar & boost::serialization::make_nvp("NoiseModelFactor1", - boost::serialization::base_object(*this)); - ar & boost::serialization::make_nvp("GravityFactor", - boost::serialization::base_object(*this));*/ - } -#endif -public: - EIGEN_MAKE_ALIGNED_OPERATOR_NEW -}; - -} /// namespace gtsam - diff --git a/corelib/src/optimizer/gtsam/XYFactor.h b/corelib/src/optimizer/gtsam/XYFactor.h index 8a7c6558..80192fda 100644 --- a/corelib/src/optimizer/gtsam/XYFactor.h +++ b/corelib/src/optimizer/gtsam/XYFactor.h @@ -43,7 +43,7 @@ public: // @param H the optional Jacobian matrix, which use boost optional and has default null pointer gtsam::Vector evaluateError(const VALUE& p, #if GTSAM_VERSION_NUMERIC >= 40300 - OptionalMatrixType H = OptionalNone) const { + gtsam::OptionalMatrixType H = OptionalNone) const { #else boost::optional H = boost::none) const { #endif @@ -59,5 +59,5 @@ public: }; -} // namespace gtsamexamples +} // namespace rtabmap diff --git a/corelib/src/optimizer/gtsam/XYZFactor.h b/corelib/src/optimizer/gtsam/XYZFactor.h index 42e55d0b..f1826c6f 100644 --- a/corelib/src/optimizer/gtsam/XYZFactor.h +++ b/corelib/src/optimizer/gtsam/XYZFactor.h @@ -43,7 +43,7 @@ public: // @param H the optional Jacobian matrix, which use boost optional and has default null pointer gtsam::Vector evaluateError(const gtsam::Pose3& p, #if GTSAM_VERSION_NUMERIC >= 40300 - OptionalMatrixType H = OptionalNone) const { + gtsam::OptionalMatrixType H = OptionalNone) const { #else boost::optional H = boost::none) const { #endif @@ -55,7 +55,7 @@ public: } gtsam::Vector evaluateError(const gtsam::Point3& p, #if GTSAM_VERSION_NUMERIC >= 40300 - OptionalMatrixType H = OptionalNone) const { + gtsam::OptionalMatrixType H = OptionalNone) const { #else boost::optional H = boost::none) const { #endif @@ -63,5 +63,5 @@ public: } }; -} // namespace gtsamexamples +} // namespace rtabmap diff --git a/corelib/src/optimizer/vertigo/gtsam/betweenFactorSwitchable.h b/corelib/src/optimizer/vertigo/gtsam/betweenFactorSwitchable.h index de1b4d73..71446457 100644 --- a/corelib/src/optimizer/vertigo/gtsam/betweenFactorSwitchable.h +++ b/corelib/src/optimizer/vertigo/gtsam/betweenFactorSwitchable.h @@ -31,9 +31,9 @@ namespace vertigo { gtsam::Vector evaluateError(const VALUE& p1, const VALUE& p2, const SwitchVariableLinear& s, #if GTSAM_VERSION_NUMERIC >= 40300 - OptionalMatrixType H1 = OptionalNone, - OptionalMatrixType H2 = OptionalNone, - OptionalMatrixType H3 = OptionalNone) const + gtsam::OptionalMatrixType H1 = OptionalNone, + gtsam::OptionalMatrixType H2 = OptionalNone, + gtsam::OptionalMatrixType H3 = OptionalNone) const #else boost::optional H1 = boost::none, boost::optional H2 = boost::none, @@ -71,9 +71,9 @@ namespace vertigo { gtsam::Vector evaluateError(const VALUE& p1, const VALUE& p2, const SwitchVariableSigmoid& s, #if GTSAM_VERSION_NUMERIC >= 40300 - OptionalMatrixType H1 = OptionalNone, - OptionalMatrixType H2 = OptionalNone, - OptionalMatrixType H3 = OptionalNone) const + gtsam::OptionalMatrixType H1 = OptionalNone, + gtsam::OptionalMatrixType H2 = OptionalNone, + gtsam::OptionalMatrixType H3 = OptionalNone) const #else boost::optional H1 = boost::none, boost::optional H2 = boost::none, diff --git a/corelib/src/optimizer/vertigo/gtsam/switchVariableLinear.h b/corelib/src/optimizer/vertigo/gtsam/switchVariableLinear.h index e95e0b56..708a7f53 100644 --- a/corelib/src/optimizer/vertigo/gtsam/switchVariableLinear.h +++ b/corelib/src/optimizer/vertigo/gtsam/switchVariableLinear.h @@ -13,6 +13,7 @@ // DerivedValue2.h removed from gtsam repo (Dec 2018): https://github.com/borglab/gtsam/commit/e550f4f2aec423cb3f2791b81cb5858b8826ebac #include "DerivedValue.h" #include +#include namespace vertigo { @@ -77,8 +78,8 @@ namespace vertigo { /** between operation */ inline SwitchVariableLinear between(const SwitchVariableLinear& l2, #if GTSAM_VERSION_NUMERIC >= 40300 - OptionalMatrixType H1=OptionalNone, - OptionalMatrixType H2=OptionalNone) const { + gtsam::OptionalMatrixType H1=OptionalNone, + gtsam::OptionalMatrixType H2=OptionalNone) const { #else boost::optional H1=boost::none, boost::optional H2=boost::none) const { diff --git a/corelib/src/optimizer/vertigo/gtsam/switchVariableSigmoid.h b/corelib/src/optimizer/vertigo/gtsam/switchVariableSigmoid.h index 79e1fca9..a949cc2b 100644 --- a/corelib/src/optimizer/vertigo/gtsam/switchVariableSigmoid.h +++ b/corelib/src/optimizer/vertigo/gtsam/switchVariableSigmoid.h @@ -13,6 +13,7 @@ // DerivedValue.h removed from gtsam repo (Dec 2018): https://github.com/borglab/gtsam/commit/e550f4f2aec423cb3f2791b81cb5858b8826ebac #include "DerivedValue.h" #include +#include namespace vertigo { @@ -77,8 +78,8 @@ namespace vertigo { /** between operation */ inline SwitchVariableSigmoid between(const SwitchVariableSigmoid& l2, #if GTSAM_VERSION_NUMERIC >= 40300 - OptionalMatrixType H1=OptionalNone, - OptionalMatrixType H2=OptionalNone) const { + gtsam::OptionalMatrixType H1=OptionalNone, + gtsam::OptionalMatrixType H2=OptionalNone) const { #else boost::optional H1=boost::none, boost::optional H2=boost::none) const { From 2314c4dfa507631edc2ed1818940aaf731b1e2cb Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 26 Apr 2026 15:40:26 -0700 Subject: [PATCH 104/126] Updated noble dev container with nvidia runtime example --- .devcontainer/noble/devcontainer.json | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.devcontainer/noble/devcontainer.json b/.devcontainer/noble/devcontainer.json index 7684b5f4..387a00c1 100644 --- a/.devcontainer/noble/devcontainer.json +++ b/.devcontainer/noble/devcontainer.json @@ -4,7 +4,7 @@ }, "customizations": { "vscode": { - "extensions": ["ms-vscode.cpptools-themes", "ms-vscode.cmake-tools"] + "extensions": ["ms-vscode.cpptools-themes", "ms-vscode.cmake-tools", "ms-vscode.cpptools-extension-pack"] } }, "workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/rtabmap,type=bind", @@ -14,5 +14,17 @@ "terminal.integrated.defaultProfile.linux": "bash" }, "remoteUser": "vscode", - "runArgs": ["--privileged", "--network=host"] + "hostRequirements": { + "gpu": "optional" + }, + "runArgs": ["--privileged", + "--network=host", + "--gpus=all", + //"--runtime=nvidia", // uncommment this if rtabmap doesn't show up in nvidia-smi on the host computer + "--env=DISPLAY", + "--env=QT_X11_NO_MITSHM=1", + "--volume=/tmp/.X11-unix:/tmp/.X11-unix"], + "containerEnv": { + "NVIDIA_VISIBLE_DEVICES": "all" + } } From f48db0ee519b09a1be64124903bd316a74ff3b9b Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 26 Apr 2026 15:43:04 -0700 Subject: [PATCH 105/126] Fixing GUI warning when SIFT/MaxGaussianThreshold=0 --- guilib/src/ui/preferencesDialog.ui | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/guilib/src/ui/preferencesDialog.ui b/guilib/src/ui/preferencesDialog.ui index 7be7c900..29a6f056 100644 --- a/guilib/src/ui/preferencesDialog.ui +++ b/guilib/src/ui/preferencesDialog.ui @@ -63,7 +63,7 @@ 0 - -96 + 0 684 5218 @@ -95,7 +95,7 @@ QFrame::Raised - 9 + 28 @@ -22139,7 +22139,7 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag 0.010000000000000 - 9.805110000000000 + 9.805110000000001 @@ -27406,7 +27406,7 @@ Lower the ratio -> higher the precision. 2 - 0.010000000000000 + 0.000000000000000 100.000000000000000 From 0cf2e45c811aab76117e4d51544c5f5e376c9be5 Mon Sep 17 00:00:00 2001 From: Borong Yuan Date: Wed, 29 Apr 2026 23:04:37 +0800 Subject: [PATCH 106/126] Preserve the attributes of intermediate nodes when retrieving data from the database (#1694) --- corelib/src/DBReader.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/corelib/src/DBReader.cpp b/corelib/src/DBReader.cpp index 17181c9a..c8b4f85e 100644 --- a/corelib/src/DBReader.cpp +++ b/corelib/src/DBReader.cpp @@ -749,7 +749,7 @@ SensorData DBReader::getNextData(SensorCaptureInfo * info) data.setStereoCameraModels(combinedStereoModels); } } - data.setId(seq); + data.setId(s->getWeight()==-1 ? -1 : seq); data.setStamp(s->getStamp()); data.setGroundTruth(s->getGroundTruthPose()); if(!globalPose.isNull()) From 37f8fa63c5b466eecb4433e28409f359852a795e Mon Sep 17 00:00:00 2001 From: matlabbe Date: Mon, 4 May 2026 16:02:55 -0700 Subject: [PATCH 107/126] Auto graph repairing (#1691) * Auto graph repairing: detect and remove bad loop closures accepted in the past that block new good loop closures to be accepted * Added parameter RGBD/OptimizeMaxErrorRepairRadius: added approach to repair the graph (remove bad loop closures added in the past). Refactored max optimization error function, split repairGraph to its own function. * Handling optimization from graph end in repair * MaxGraphErrors: changed link's pointers to copy directly to avoid seg fault down the road if not used properly * Dont publish optimization statistics if it was not computed. --- corelib/include/rtabmap/core/Graph.h | 17 +- corelib/include/rtabmap/core/Parameters.h | 1 + corelib/include/rtabmap/core/Rtabmap.h | 10 + corelib/include/rtabmap/core/Statistics.h | 5 + corelib/src/Graph.cpp | 50 +- corelib/src/Rtabmap.cpp | 821 +++++++++++++--------- corelib/src/global_map/OccupancyGrid.cpp | 4 +- guilib/src/DatabaseViewer.cpp | 56 +- guilib/src/MainWindow.cpp | 48 +- guilib/src/PreferencesDialog.cpp | 1 + guilib/src/ui/preferencesDialog.ui | 287 ++++---- 11 files changed, 746 insertions(+), 554 deletions(-) diff --git a/corelib/include/rtabmap/core/Graph.h b/corelib/include/rtabmap/core/Graph.h index ffd828ee..76d37b80 100644 --- a/corelib/include/rtabmap/core/Graph.h +++ b/corelib/include/rtabmap/core/Graph.h @@ -118,15 +118,18 @@ Transform RTABMAP_CORE_EXPORT calcRMSE( float & rotational_max, bool align2D = false); -void RTABMAP_CORE_EXPORT computeMaxGraphErrors( +struct MaxGraphErrors +{ + float linear=-1.0f; // absolute error (m) of the link with maximum linear error + float angular=-1.0f; // absolute error (rad) of the link with maximum angular error + float linearRatio=-1.0f; // Ratio = absolute error (m) / linear std (m), of the link with maximum linear error + float angularRatio=-1.0f; // Ratio = absolute error (rad) / angular std (rad), of the link with maximum angular error + Link linearLink; // link with maximum linear error + Link angularLink; // link with maximum angular error +}; +MaxGraphErrors RTABMAP_CORE_EXPORT computeMaxGraphErrors( const std::map & poses, const std::multimap & links, - float & maxLinearErrorRatio, - float & maxAngularErrorRatio, - float & maxLinearError, - float & maxAngularError, - const Link ** maxLinearErrorLink = 0, - const Link ** maxAngularErrorLink = 0, bool for3DoF = false); std::vector RTABMAP_CORE_EXPORT getMaxOdomInf(const std::multimap & links); diff --git a/corelib/include/rtabmap/core/Parameters.h b/corelib/include/rtabmap/core/Parameters.h index b9e7a27b..5d3e315d 100644 --- a/corelib/include/rtabmap/core/Parameters.h +++ b/corelib/include/rtabmap/core/Parameters.h @@ -379,6 +379,7 @@ class RTABMAP_CORE_EXPORT Parameters RTABMAP_PARAM(RGBD, NewMapOdomChangeDistance, float, 0, "A new map is created if a change of odometry translation greater than X m is detected (0 m = disabled)."); RTABMAP_PARAM(RGBD, OptimizeFromGraphEnd, bool, false, "Optimize graph from the newest node. If false, the graph is optimized from the oldest node of the current graph (this adds an overhead computation to detect to oldest node of the current graph, but it can be useful to preserve the map referential from the oldest node). Warning when set to false: when some nodes are transferred, the first referential of the local map may change, resulting in momentary changes in robot/map position (which are annoying in teleoperation)."); RTABMAP_PARAM(RGBD, OptimizeMaxError, float, 3.0, uFormat("Reject loop closures if optimization error ratio is greater than this value (0=disabled). Ratio is computed as absolute error over standard deviation of each link. This will help to detect when a wrong loop closure is added to the graph. If used with \"%s\", the disabled loop closure links will be removed.", kOptimizerRobust().c_str())); + RTABMAP_PARAM(RGBD, OptimizeMaxErrorRepairRadius, float, 0.0, uFormat("If two consecutive loop closures are rejected by %s on the same old loop closure link, we will remove that old link, and other old links under that radius if necessary, until optimization is accepted. When optimization is accepted, the old loop closure links are removed from the graph. This feature is useful to reject bad loop closures that were accepted previously. Set to 0 to disable this feature.", kRGBDOptimizeMaxError().c_str())); RTABMAP_PARAM(RGBD, MaxLoopClosureDistance, float, 0.0, "Reject loop closures/localizations if the distance from the map is over this distance (0=disabled)."); RTABMAP_PARAM(RGBD, ForceOdom3DoF, bool, true, uFormat("Force odometry pose to be 3DoF if %s=true.", kRegForce3DoF().c_str())); RTABMAP_PARAM(RGBD, StartAtOrigin, bool, false, uFormat("If true, rtabmap will assume the robot is starting from origin of the map. If false, rtabmap will assume the robot is restarting from the last saved localization pose from previous session (the place where it shut down previously). Used only in localization mode (%s=false).", kMemIncrementalMemory().c_str())); diff --git a/corelib/include/rtabmap/core/Rtabmap.h b/corelib/include/rtabmap/core/Rtabmap.h index 2905db9c..b3d438e8 100644 --- a/corelib/include/rtabmap/core/Rtabmap.h +++ b/corelib/include/rtabmap/core/Rtabmap.h @@ -35,6 +35,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/core/Statistics.h" #include "rtabmap/core/Link.h" #include "rtabmap/core/ProgressState.h" +#include "rtabmap/core/Graph.h" #include #include @@ -264,6 +265,13 @@ private: std::multimap * constraints = 0, double * error = 0, int * iterationsDone = 0) const; + std::list > repairGraph( + graph::MaxGraphErrors & maxGraphErrors, + std::map & poses, + std::multimap & constraints, + double & optimizationError, + int & optimizationIterations, + cv::Mat & optimizationCovariance); void updateGoalIndex(); bool computePath(int targetNode, std::map nodes, const std::multimap & constraints); @@ -320,6 +328,7 @@ private: std::string _databasePath; bool _optimizeFromGraphEnd; float _optimizationMaxError; + float _optimizationMaxErrorRepairRadius; bool _startNewMapOnLoopClosure; bool _startNewMapOnGoodSignature; float _goalReachedRadius; // meters @@ -379,6 +388,7 @@ private: std::map _odomCachePoses; // used in localization mode to reject loop closures std::multimap _odomCacheConstraints; // used in localization mode to reject loop closures std::map _markerPriors; + std::pair _lastRejectedLoopClosureIds; std::set _nodesToRepublish; diff --git a/corelib/include/rtabmap/core/Statistics.h b/corelib/include/rtabmap/core/Statistics.h index 11937b0e..1cdaf40d 100644 --- a/corelib/include/rtabmap/core/Statistics.h +++ b/corelib/include/rtabmap/core/Statistics.h @@ -78,6 +78,11 @@ class RTABMAP_CORE_EXPORT Statistics RTABMAP_STATS(Loop, Optimization_iterations, ); RTABMAP_STATS(Loop, Optimization_max_error_from_id, ); RTABMAP_STATS(Loop, Optimization_max_error_to_id, ); + RTABMAP_STATS(Loop, Optimization_max_ang_error_from_id, ); + RTABMAP_STATS(Loop, Optimization_max_ang_error_to_id, ); + RTABMAP_STATS(Loop, Optimization_max_error_removed_from_id, ); + RTABMAP_STATS(Loop, Optimization_max_error_removed_to_id, ); + RTABMAP_STATS(Loop, Optimization_max_error_removed_count, ); RTABMAP_STATS(Loop, Linear_variance,); RTABMAP_STATS(Loop, Angular_variance,); RTABMAP_STATS(Loop, Landmark_detected,); diff --git a/corelib/src/Graph.cpp b/corelib/src/Graph.cpp index a887fddb..1cc96a05 100644 --- a/corelib/src/Graph.cpp +++ b/corelib/src/Graph.cpp @@ -927,21 +927,12 @@ Transform calcRMSE ( return t; } -void computeMaxGraphErrors( +MaxGraphErrors computeMaxGraphErrors( const std::map & poses, const std::multimap & links, - float & maxLinearErrorRatio, - float & maxAngularErrorRatio, - float & maxLinearError, - float & maxAngularError, - const Link ** maxLinearErrorLink, - const Link ** maxAngularErrorLink, bool force3DoF) { - maxLinearErrorRatio = -1; - maxAngularErrorRatio = -1; - maxLinearError = -1; - maxAngularError = -1; + MaxGraphErrors maxError; UDEBUG("poses=%d links=%d", (int)poses.size(), (int)links.size()); for(std::multimap::const_iterator iter=links.begin(); iter!=links.end(); ++iter) @@ -963,19 +954,7 @@ void computeMaxGraphErrors( iter->second.to(), t2.prettyPrint().c_str()); - if(maxLinearErrorLink) - { - *maxLinearErrorLink = 0; - } - if(maxAngularErrorLink) - { - *maxAngularErrorLink = 0; - } - maxLinearErrorRatio = -1; - maxAngularErrorRatio = -1; - maxLinearError = -1; - maxAngularError = -1; - return; + return MaxGraphErrors(); } Transform t; @@ -999,14 +978,11 @@ void computeMaxGraphErrors( UASSERT(iter->second.transVariance(false)>0.0); float stddevLinear = sqrt(iter->second.transVariance(false)); float linearErrorRatio = linearError/stddevLinear; - if(linearErrorRatio > maxLinearErrorRatio) + if(linearErrorRatio > maxError.linearRatio) { - maxLinearError = linearError; - maxLinearErrorRatio = linearErrorRatio; - if(maxLinearErrorLink) - { - *maxLinearErrorLink = &iter->second; - } + maxError.linear = linearError; + maxError.linearRatio = linearErrorRatio; + maxError.linearLink = iter->second; } // For landmark links, don't compute angular error if it doesn't estimate orientation @@ -1031,18 +1007,16 @@ void computeMaxGraphErrors( UASSERT(iter->second.rotVariance(false)>0.0); float stddevAngular = sqrt(iter->second.rotVariance(false)); float angularErrorRatio = angularError/stddevAngular; - if(angularErrorRatio > maxAngularErrorRatio) + if(angularErrorRatio > maxError.angularRatio) { - maxAngularError = angularError; - maxAngularErrorRatio = angularErrorRatio; - if(maxAngularErrorLink) - { - *maxAngularErrorLink = &iter->second; - } + maxError.angular = angularError; + maxError.angularRatio = angularErrorRatio; + maxError.angularLink = iter->second; } } } } + return maxError; } std::vector getMaxOdomInf(const std::multimap & links) diff --git a/corelib/src/Rtabmap.cpp b/corelib/src/Rtabmap.cpp index 49b17a5e..c9a3dc40 100644 --- a/corelib/src/Rtabmap.cpp +++ b/corelib/src/Rtabmap.cpp @@ -138,6 +138,7 @@ Rtabmap::Rtabmap() : _databasePath(""), _optimizeFromGraphEnd(Parameters::defaultRGBDOptimizeFromGraphEnd()), _optimizationMaxError(Parameters::defaultRGBDOptimizeMaxError()), + _optimizationMaxErrorRepairRadius(Parameters::defaultRGBDOptimizeMaxErrorRepairRadius()), _startNewMapOnLoopClosure(Parameters::defaultRtabmapStartNewMapOnLoopClosure()), _startNewMapOnGoodSignature(Parameters::defaultRtabmapStartNewMapOnGoodSignature()), _goalReachedRadius(Parameters::defaultRGBDGoalReachedRadius()), @@ -173,6 +174,7 @@ Rtabmap::Rtabmap() : _mapCorrection(Transform::getIdentity()), _lastLocalizationNodeId(0), _currentSessionHasGPS(false), + _lastRejectedLoopClosureIds(0,0), _pathStatus(0), _pathCurrentIndex(0), _pathGoalIndex(0), @@ -621,6 +623,7 @@ void Rtabmap::parseParameters(const ParametersMap & parameters) _optimizeFromGraphEndChanged = true; } Parameters::parse(parameters, Parameters::kRGBDOptimizeMaxError(), _optimizationMaxError); + Parameters::parse(parameters, Parameters::kRGBDOptimizeMaxErrorRepairRadius(), _optimizationMaxErrorRepairRadius); Parameters::parse(parameters, Parameters::kRtabmapStartNewMapOnLoopClosure(), _startNewMapOnLoopClosure); Parameters::parse(parameters, Parameters::kRtabmapStartNewMapOnGoodSignature(), _startNewMapOnGoodSignature); Parameters::parse(parameters, Parameters::kRGBDGoalReachedRadius(), _goalReachedRadius); @@ -936,6 +939,7 @@ int Rtabmap::triggerNewMap() UINFO("New map triggered, new map = %d", mapId); _optimizedPoses.clear(); _constraints.clear(); + _lastRejectedLoopClosureIds = std::make_pair(0,0); if(_bayesFilter) { @@ -1108,6 +1112,7 @@ void Rtabmap::resetMemory() _globalScanMap.clear(); _globalScanMapPoses.clear(); _nodesToRepublish.clear(); + _lastRejectedLoopClosureIds = std::make_pair(0,0); this->clearPath(0); if(_memory) @@ -3213,12 +3218,11 @@ bool Rtabmap::process( //============================================================ // Optimize map graph //============================================================ - float maxLinearError = 0.0f; - float maxLinearErrorRatio = 0.0f; - float maxAngularError = 0.0f; - float maxAngularErrorRatio = 0.0f; - int maxLinearErrorFromId = 0; - int maxLinearErrorToId = 0; + graph::MaxGraphErrors maxGraphErrors; + std::pair maxGraphErrorsLinearIds(0,0); + std::pair maxGraphErrorsAngularIds(0,0); + std::pair maxGraphErrorsRemovedIds(0,0); + int maxGraphErrorsRemovedCount = 0; double optimizationError = 0.0; int optimizationIterations = 0; Transform previousMapCorrection; @@ -3361,6 +3365,7 @@ bool Rtabmap::process( UDEBUG("Opt %d %s", iter->first, iter->second.prettyPrint().c_str()); } + std::list > removedLinks; if(optPoses.empty()) { UWARN("Optimization failed, rejecting localization!"); @@ -3369,78 +3374,105 @@ bool Rtabmap::process( else { UINFO("Compute max graph errors..."); - const Link * maxLinearLink = 0; - const Link * maxAngularLink = 0; - graph::computeMaxGraphErrors( + maxGraphErrors = graph::computeMaxGraphErrors( optPoses, edgeConstraintsOut, - maxLinearErrorRatio, - maxAngularErrorRatio, - maxLinearError, - maxAngularError, - &maxLinearLink, - &maxAngularLink, _graphOptimizer->isSlam2d()); - if(maxLinearLink == 0 && maxAngularLink==0) + if(!maxGraphErrors.linearLink.isValid() && !maxGraphErrors.angularLink.isValid()) { UWARN("Could not compute graph errors! Rejecting localization!"); rejectLocalization = true; } - if(maxLinearLink) + if(maxGraphErrors.linearLink.isValid()) { - maxLinearErrorFromId = maxLinearLink->from(); - maxLinearErrorToId = maxLinearLink->to(); + maxGraphErrorsLinearIds = std::make_pair(maxGraphErrors.linearLink.from(), maxGraphErrors.linearLink.to()); UINFO("Max optimization linear error = %f m (link %d->%d, var=%f, ratio error/std=%f, thr=%f)", - maxLinearError, - maxLinearLink->from(), - maxLinearLink->to(), - maxLinearLink->transVariance(), - maxLinearError/sqrt(maxLinearLink->transVariance()), + maxGraphErrors.linear, + maxGraphErrors.linearLink.from(), + maxGraphErrors.linearLink.to(), + maxGraphErrors.linearLink.transVariance(), + maxGraphErrors.linear/sqrt(maxGraphErrors.linearLink.transVariance()), _optimizationMaxError); - if(_optimizationMaxError > 0.0f && maxLinearErrorRatio > _optimizationMaxError) + if(_optimizationMaxError > 0.0f && maxGraphErrors.linearRatio > _optimizationMaxError) { - UWARN("Rejecting localization (%d <-> %d) in this " - "iteration because a wrong loop closure has been " - "detected after graph optimization, resulting in " - "a maximum graph error ratio of %f (edge %d->%d, type=%d, abs error=%f m, stddev=%f). The " - "maximum error ratio parameter \"%s\" is %f of std deviation.", - localizationLinks.rbegin()->second.from(), - localizationLinks.rbegin()->second.to(), - maxLinearErrorRatio, - maxLinearLink->from(), - maxLinearLink->to(), - maxLinearLink->type(), - maxLinearError, - sqrt(maxLinearLink->transVariance()), - Parameters::kRGBDOptimizeMaxError().c_str(), - _optimizationMaxError); - rejectLocalization = true; + if( _optimizationMaxErrorRepairRadius > 0.0 && + maxGraphErrorsLinearIds == _lastRejectedLoopClosureIds && + graph::findLink(edgeConstraintsOut, maxGraphErrorsLinearIds.first, maxGraphErrorsLinearIds.second) != edgeConstraintsOut.end()) + { + UWARN("We detected 2 consecutive loop closure rejections because of the same loop closure link (%d->%d), trying optimization again without that link...", + maxGraphErrorsLinearIds.first, maxGraphErrorsLinearIds.second); + + UDEBUG("priorsIgnored was %s", priorsIgnored?"true":"false"); + _graphOptimizer->setPriorsIgnored(false); //temporary set false to use priors above to fix nodes of the map + removedLinks = repairGraph( + maxGraphErrors, + optPoses, + edgeConstraintsOut, + optimizationError, + optimizationIterations, + locOptCovariance); + _graphOptimizer->setPriorsIgnored(priorsIgnored); // set back + + if(removedLinks.empty()) + { + UWARN("Optimization failed when trying to repair the graph."); + rejectLocalization = true; + } + } + else { + rejectLocalization = true; + } + + if(rejectLocalization) + { + UWARN("Rejecting localization (%d <-> %d) in this " + "iteration because a wrong loop closure has been " + "detected after graph optimization, resulting in " + "a maximum graph error ratio of %f (edge %d->%d, type=%d, abs error=%f m, stddev=%f). The " + "maximum error ratio parameter \"%s\" is %f of std deviation.", + localizationLinks.rbegin()->second.from(), + localizationLinks.rbegin()->second.to(), + maxGraphErrors.linearRatio, + maxGraphErrors.linearLink.from(), + maxGraphErrors.linearLink.to(), + maxGraphErrors.linearLink.type(), + maxGraphErrors.linear, + sqrt(maxGraphErrors.linearLink.transVariance()), + Parameters::kRGBDOptimizeMaxError().c_str(), + _optimizationMaxError); + + if(maxGraphErrors.linearLink.type() != Link::kNeighbor) + { + _lastRejectedLoopClosureIds = maxGraphErrorsLinearIds; + } + } } - else if(_optimizationMaxError == 0.0f && maxLinearErrorRatio>100 && !_graphOptimizer->isRobust()) + else if(_optimizationMaxError == 0.0f && maxGraphErrors.linearRatio>100 && !_graphOptimizer->isRobust()) { UERROR("Huge optimization error detected!" "Linear error ratio of %f (edge %d->%d, type=%d, abs error=%f m, stddev=%f). You may consider " "enabling \"%s\" to reject those bad optimizations by setting it to a non null value!", - maxLinearErrorRatio, - maxLinearLink->from(), - maxLinearLink->to(), - maxLinearLink->type(), - maxLinearError, - sqrt(maxLinearLink->transVariance()), + maxGraphErrors.linearRatio, + maxGraphErrors.linearLink.from(), + maxGraphErrors.linearLink.to(), + maxGraphErrors.linearLink.type(), + maxGraphErrors.linear, + sqrt(maxGraphErrors.linearLink.transVariance()), Parameters::kRGBDOptimizeMaxError().c_str()); } } - if(maxAngularLink) + if(maxGraphErrors.angularLink.isValid()) { + maxGraphErrorsAngularIds = std::make_pair(maxGraphErrors.angularLink.from(), maxGraphErrors.angularLink.to()); UINFO("Max optimization angular error = %f deg (link %d->%d, var=%f, ratio error/std=%f, thr=%f)", - maxAngularError*180.0f/CV_PI, - maxAngularLink->from(), - maxAngularLink->to(), - maxAngularLink->rotVariance(), - maxAngularError/sqrt(maxAngularLink->rotVariance()), + maxGraphErrors.angular*180.0f/CV_PI, + maxGraphErrors.angularLink.from(), + maxGraphErrors.angularLink.to(), + maxGraphErrors.angularLink.rotVariance(), + maxGraphErrors.angular/sqrt(maxGraphErrors.angularLink.rotVariance()), _optimizationMaxError); - if(_optimizationMaxError > 0.0f && maxAngularErrorRatio > _optimizationMaxError) + if(_optimizationMaxError > 0.0f && maxGraphErrors.angularRatio > _optimizationMaxError) { UWARN("Rejecting localization (%d <-> %d) in this " "iteration because a wrong loop closure has been " @@ -3449,27 +3481,27 @@ bool Rtabmap::process( "maximum error ratio parameter \"%s\" is %f of std deviation.", localizationLinks.rbegin()->second.from(), localizationLinks.rbegin()->second.to(), - maxAngularErrorRatio, - maxAngularLink->from(), - maxAngularLink->to(), - maxAngularLink->type(), - maxAngularError*180.0f/CV_PI, - sqrt(maxAngularLink->rotVariance()), + maxGraphErrors.angularRatio, + maxGraphErrors.angularLink.from(), + maxGraphErrors.angularLink.to(), + maxGraphErrors.angularLink.type(), + maxGraphErrors.angular*180.0f/CV_PI, + sqrt(maxGraphErrors.angularLink.rotVariance()), Parameters::kRGBDOptimizeMaxError().c_str(), _optimizationMaxError); rejectLocalization = true; } - else if(_optimizationMaxError == 0.0f && maxAngularErrorRatio>100 && !_graphOptimizer->isRobust()) + else if(_optimizationMaxError == 0.0f && maxGraphErrors.angularRatio>100 && !_graphOptimizer->isRobust()) { UERROR("Huge optimization error detected!" "Angular error ratio of %f (edge %d->%d, type=%d, abs error=%f m, stddev=%f). You may consider " "enabling \"%s\" to reject those bad optimizations by setting it to a non null value!", - maxAngularErrorRatio, - maxAngularLink->from(), - maxAngularLink->to(), - maxAngularLink->type(), - maxAngularError*180.0f/CV_PI, - sqrt(maxAngularLink->rotVariance()), + maxGraphErrors.angularRatio, + maxGraphErrors.angularLink.from(), + maxGraphErrors.angularLink.to(), + maxGraphErrors.angularLink.type(), + maxGraphErrors.angular*180.0f/CV_PI, + sqrt(maxGraphErrors.angularLink.rotVariance()), Parameters::kRGBDOptimizeMaxError().c_str()); } } @@ -3492,7 +3524,6 @@ bool Rtabmap::process( { rejectLocalization = false; UWARN("Global and loop closures seem not tallying together, try again to optimize without local loop closures..."); - priorsIgnored = _graphOptimizer->priorsIgnored(); UDEBUG("priorsIgnored was %s", priorsIgnored?"true":"false"); _graphOptimizer->setPriorsIgnored(false); //temporary set false to use priors above to fix nodes of the map // If slam2d: get connected graph while keeping original roll,pitch,z values. @@ -3521,36 +3552,27 @@ bool Rtabmap::process( else { UINFO("Compute max graph errors..."); - const Link * maxLinearLink = 0; - const Link * maxAngularLink = 0; - graph::computeMaxGraphErrors( + maxGraphErrors = graph::computeMaxGraphErrors( optPoses, edgeConstraintsOut, - maxLinearErrorRatio, - maxAngularErrorRatio, - maxLinearError, - maxAngularError, - &maxLinearLink, - &maxAngularLink, _graphOptimizer->isSlam2d()); - if(maxLinearLink == 0 && maxAngularLink==0) + if(!maxGraphErrors.linearLink.isValid() && !maxGraphErrors.angularLink.isValid()) { UWARN("Could not compute graph errors! Rejecting localization!"); rejectLocalization = true; } - if(maxLinearLink) + if(maxGraphErrors.linearLink.isValid()) { - maxLinearErrorFromId = maxLinearLink->from(); - maxLinearErrorToId = maxLinearLink->to(); + maxGraphErrorsLinearIds = std::make_pair(maxGraphErrors.linearLink.from(), maxGraphErrors.linearLink.to()); UINFO("Max optimization linear error = %f m (link %d->%d, var=%f, ratio error/std=%f, thr=%f)", - maxLinearError, - maxLinearLink->from(), - maxLinearLink->to(), - maxLinearLink->transVariance(), - maxLinearError/sqrt(maxLinearLink->transVariance()), + maxGraphErrors.linear, + maxGraphErrors.linearLink.from(), + maxGraphErrors.linearLink.to(), + maxGraphErrors.linearLink.transVariance(), + maxGraphErrors.linear/sqrt(maxGraphErrors.linearLink.transVariance()), _optimizationMaxError); - if(_optimizationMaxError > 0.0f && maxLinearErrorRatio > _optimizationMaxError) + if(_optimizationMaxError > 0.0f && maxGraphErrors.linearRatio > _optimizationMaxError) { UWARN("Rejecting localization (%d <-> %d) in this " "iteration because a wrong loop closure has been " @@ -3559,40 +3581,41 @@ bool Rtabmap::process( "maximum error ratio parameter \"%s\" is %f of std deviation.", localizationLinks.rbegin()->second.from(), localizationLinks.rbegin()->second.to(), - maxLinearErrorRatio, - maxLinearLink->from(), - maxLinearLink->to(), - maxLinearLink->type(), - maxLinearError, - sqrt(maxLinearLink->transVariance()), + maxGraphErrors.linearRatio, + maxGraphErrors.linearLink.from(), + maxGraphErrors.linearLink.to(), + maxGraphErrors.linearLink.type(), + maxGraphErrors.linear, + sqrt(maxGraphErrors.linearLink.transVariance()), Parameters::kRGBDOptimizeMaxError().c_str(), _optimizationMaxError); rejectLocalization = true; } - else if(_optimizationMaxError == 0.0f && maxLinearErrorRatio>100 && !_graphOptimizer->isRobust()) + else if(_optimizationMaxError == 0.0f && maxGraphErrors.linearRatio>100 && !_graphOptimizer->isRobust()) { UERROR("Huge optimization error detected!" "Linear error ratio of %f (edge %d->%d, type=%d, abs error=%f m, stddev=%f). You may consider " "enabling \"%s\" to reject those bad optimizations by setting it to a non null value!", - maxLinearErrorRatio, - maxLinearLink->from(), - maxLinearLink->to(), - maxLinearLink->type(), - maxLinearError, - sqrt(maxLinearLink->transVariance()), + maxGraphErrors.linearRatio, + maxGraphErrors.linearLink.from(), + maxGraphErrors.linearLink.to(), + maxGraphErrors.linearLink.type(), + maxGraphErrors.linear, + sqrt(maxGraphErrors.linearLink.transVariance()), Parameters::kRGBDOptimizeMaxError().c_str()); } } - if(maxAngularLink) + if(maxGraphErrors.angularLink.isValid()) { + maxGraphErrorsAngularIds = std::make_pair(maxGraphErrors.angularLink.from(), maxGraphErrors.angularLink.to()); UINFO("Max optimization angular error = %f deg (link %d->%d, var=%f, ratio error/std=%f, thr=%f)", - maxAngularError*180.0f/CV_PI, - maxAngularLink->from(), - maxAngularLink->to(), - maxAngularLink->rotVariance(), - maxAngularError/sqrt(maxAngularLink->rotVariance()), + maxGraphErrors.angular*180.0f/CV_PI, + maxGraphErrors.angularLink.from(), + maxGraphErrors.angularLink.to(), + maxGraphErrors.angularLink.rotVariance(), + maxGraphErrors.angular/sqrt(maxGraphErrors.angularLink.rotVariance()), _optimizationMaxError); - if(_optimizationMaxError > 0.0f && maxAngularErrorRatio > _optimizationMaxError) + if(_optimizationMaxError > 0.0f && maxGraphErrors.angularRatio > _optimizationMaxError) { UWARN("Rejecting localization (%d <-> %d) in this " "iteration because a wrong loop closure has been " @@ -3601,27 +3624,27 @@ bool Rtabmap::process( "maximum error ratio parameter \"%s\" is %f of std deviation.", localizationLinks.rbegin()->second.from(), localizationLinks.rbegin()->second.to(), - maxAngularErrorRatio, - maxAngularLink->from(), - maxAngularLink->to(), - maxAngularLink->type(), - maxAngularError*180.0f/CV_PI, - sqrt(maxAngularLink->rotVariance()), + maxGraphErrors.angularRatio, + maxGraphErrors.angularLink.from(), + maxGraphErrors.angularLink.to(), + maxGraphErrors.angularLink.type(), + maxGraphErrors.angular*180.0f/CV_PI, + sqrt(maxGraphErrors.angularLink.rotVariance()), Parameters::kRGBDOptimizeMaxError().c_str(), _optimizationMaxError); rejectLocalization = true; } - else if(_optimizationMaxError == 0.0f && maxAngularErrorRatio>100 && !_graphOptimizer->isRobust()) + else if(_optimizationMaxError == 0.0f && maxGraphErrors.angularRatio>100 && !_graphOptimizer->isRobust()) { UERROR("Huge optimization error detected!" "Angular error ratio of %f (edge %d->%d, type=%d, abs error=%f m, stddev=%f). You may consider " "enabling \"%s\" to reject those bad optimizations by setting it to a non null value!", - maxAngularErrorRatio, - maxAngularLink->from(), - maxAngularLink->to(), - maxAngularLink->type(), - maxAngularError*180.0f/CV_PI, - sqrt(maxAngularLink->rotVariance()), + maxGraphErrors.angularRatio, + maxGraphErrors.angularLink.from(), + maxGraphErrors.angularLink.to(), + maxGraphErrors.angularLink.type(), + maxGraphErrors.angular*180.0f/CV_PI, + sqrt(maxGraphErrors.angularLink.rotVariance()), Parameters::kRGBDOptimizeMaxError().c_str()); } } @@ -3648,6 +3671,21 @@ bool Rtabmap::process( } odomCacheProximityLinksCleared = before - _odomCacheConstraints.size(); } + else if(!removedLinks.empty()) + { + // If we removed links (repaired the graph) + for(auto link: removedLinks) + { + UWARN("Removing link %d->%d from odometry cache", link.first, link.second); + auto iter = graph::findLink(_odomCacheConstraints, link.first, link.second); + if(iter!=_odomCacheConstraints.end()) { + _odomCacheConstraints.erase(iter); + } + } + UWARN("Successfully repaired the graph."); + maxGraphErrorsRemovedIds = removedLinks.front(); + maxGraphErrorsRemovedCount = removedLinks.size(); + } // Count how many localization links are in the constraints bool hadAlreadyLocalizationLinks = false; @@ -3874,66 +3912,102 @@ bool Rtabmap::process( constraints.size()) { UINFO("Compute max graph errors..."); - const Link * maxLinearLink = 0; - const Link * maxAngularLink = 0; - graph::computeMaxGraphErrors( + maxGraphErrors = graph::computeMaxGraphErrors( poses, - constraints, - maxLinearErrorRatio, - maxAngularErrorRatio, - maxLinearError, - maxAngularError, - &maxLinearLink, - &maxAngularLink); - if(maxLinearLink == 0 && maxAngularLink==0) + constraints); + if(!maxGraphErrors.linearLink.isValid() && !maxGraphErrors.angularLink.isValid()) { UWARN("Could not compute graph errors! Wrong loop closures could be accepted!"); } bool reject = false; - if(maxLinearLink) + if(maxGraphErrors.linearLink.isValid()) { - maxLinearErrorFromId = maxLinearLink->from(); - maxLinearErrorToId = maxLinearLink->to(); - UINFO("Max optimization linear error = %f m (link %d->%d, var=%f, ratio error/std=%f)", maxLinearError, maxLinearLink->from(), maxLinearLink->to(), maxLinearLink->transVariance(), maxLinearError/sqrt(maxLinearLink->transVariance())); - if(_optimizationMaxError > 0.0f && maxLinearErrorRatio > _optimizationMaxError) + maxGraphErrorsLinearIds = std::make_pair(maxGraphErrors.linearLink.from(), maxGraphErrors.linearLink.to()); + UINFO("Max optimization linear error = %f m (link %d->%d, var=%f, ratio error/std=%f)", maxGraphErrors.linear, maxGraphErrors.linearLink.from(), maxGraphErrors.linearLink.to(), maxGraphErrors.linearLink.transVariance(), maxGraphErrors.linear/sqrt(maxGraphErrors.linearLink.transVariance())); + if(_optimizationMaxError > 0.0f && maxGraphErrors.linearRatio > _optimizationMaxError) { - UWARN("Rejecting all added loop closures (%d, first is %d <-> %d) in this " - "iteration because a wrong loop closure has been " - "detected after graph optimization, resulting in " - "a maximum graph error ratio of %f (edge %d->%d, type=%d, abs error=%f m, stddev=%f). The " - "maximum error ratio parameter \"%s\" is %f of std deviation.", - (int)loopClosureLinksAdded.size(), - loopClosureLinksAdded.front().first, - loopClosureLinksAdded.front().second, - maxLinearErrorRatio, - maxLinearLink->from(), - maxLinearLink->to(), - maxLinearLink->type(), - maxLinearError, - sqrt(maxLinearLink->transVariance()), - Parameters::kRGBDOptimizeMaxError().c_str(), - _optimizationMaxError); - reject = true; + if( _optimizationMaxErrorRepairRadius > 0.0 && + maxGraphErrorsLinearIds == _lastRejectedLoopClosureIds && + graph::findLink(constraints, maxGraphErrorsLinearIds.first, maxGraphErrorsLinearIds.second) != constraints.end()) + { + UWARN("We detected 2 consecutive loop closure rejections because of the same loop closure link (%d->%d), trying optimization again without that link...", + maxGraphErrorsLinearIds.first, maxGraphErrorsLinearIds.second); + + std::list > removedLinks = repairGraph( + maxGraphErrors, + poses, + constraints, + optimizationError, + optimizationIterations, + covariance); + + if(removedLinks.empty()) + { + UWARN("Optimization failed when trying to repair the graph."); + reject = true; + } + else + { + for(auto link: removedLinks) + { + UWARN("Removing link %d->%d from memory", link.first, link.second); + _memory->removeLink(link.first, link.second); + } + UWARN("Successfully repaired the graph."); + maxGraphErrorsRemovedIds = removedLinks.front(); + maxGraphErrorsRemovedCount = removedLinks.size(); + } + } + else { + reject = true; + } + + if(reject) + { + UWARN("Rejecting all added loop closures (%d, first is %d <-> %d) in this " + "iteration because a wrong loop closure has been " + "detected after graph optimization, resulting in " + "a maximum graph error ratio of %f (edge %d->%d, type=%d, abs error=%f m, stddev=%f). The " + "maximum error ratio parameter \"%s\" is %f of std deviation.", + (int)loopClosureLinksAdded.size(), + loopClosureLinksAdded.front().first, + loopClosureLinksAdded.front().second, + maxGraphErrors.linearRatio, + maxGraphErrors.linearLink.from(), + maxGraphErrors.linearLink.to(), + maxGraphErrors.linearLink.type(), + maxGraphErrors.linear, + sqrt(maxGraphErrors.linearLink.transVariance()), + Parameters::kRGBDOptimizeMaxError().c_str(), + _optimizationMaxError); + + if(maxGraphErrors.linearLink.type() != Link::kNeighbor) + { + _lastRejectedLoopClosureIds = maxGraphErrorsLinearIds; + } + } } - else if(_optimizationMaxError == 0.0f && maxLinearErrorRatio>100 && !_graphOptimizer->isRobust()) + else if(_optimizationMaxError == 0.0f && maxGraphErrors.linearRatio>100 && !_graphOptimizer->isRobust()) { UERROR("Huge optimization error detected!" "Linear error ratio of %f (edge %d->%d, type=%d, abs error=%f m, stddev=%f). You may consider " "enabling \"%s\" to reject those bad optimizations by setting it to a non null value!", - maxLinearErrorRatio, - maxLinearLink->from(), - maxLinearLink->to(), - maxLinearLink->type(), - maxLinearError, - sqrt(maxLinearLink->transVariance()), + maxGraphErrors.linearRatio, + maxGraphErrors.linearLink.from(), + maxGraphErrors.linearLink.to(), + maxGraphErrors.linearLink.type(), + maxGraphErrors.linear, + sqrt(maxGraphErrors.linearLink.transVariance()), Parameters::kRGBDOptimizeMaxError().c_str()); } } - if(maxAngularLink) + + if(maxGraphErrors.angularLink.isValid()) { - UINFO("Max optimization angular error = %f deg (link %d->%d, var=%f, ratio error/std=%f)", maxAngularError*180.0f/CV_PI, maxAngularLink->from(), maxAngularLink->to(), maxAngularLink->rotVariance(), maxAngularError/sqrt(maxAngularLink->rotVariance())); - if(_optimizationMaxError > 0.0f && maxAngularErrorRatio > _optimizationMaxError) + maxGraphErrorsAngularIds = std::make_pair(maxGraphErrors.angularLink.from(), maxGraphErrors.angularLink.to()); + UINFO("Max optimization angular error = %f deg (link %d->%d, var=%f, ratio error/std=%f)", maxGraphErrors.angular*180.0f/CV_PI, maxGraphErrors.angularLink.from(), maxGraphErrors.angularLink.to(), maxGraphErrors.angularLink.rotVariance(), maxGraphErrors.angular/sqrt(maxGraphErrors.angularLink.rotVariance())); + if(_optimizationMaxError > 0.0f && maxGraphErrors.angularRatio > _optimizationMaxError) { UWARN("Rejecting all added loop closures (%d, first is %d <-> %d) in this " "iteration because a wrong loop closure has been " @@ -3943,27 +4017,27 @@ bool Rtabmap::process( (int)loopClosureLinksAdded.size(), loopClosureLinksAdded.front().first, loopClosureLinksAdded.front().second, - maxAngularErrorRatio, - maxAngularLink->from(), - maxAngularLink->to(), - maxAngularLink->type(), - maxAngularError*180.0f/CV_PI, - sqrt(maxAngularLink->rotVariance()), + maxGraphErrors.angularRatio, + maxGraphErrors.angularLink.from(), + maxGraphErrors.angularLink.to(), + maxGraphErrors.angularLink.type(), + maxGraphErrors.angular*180.0f/CV_PI, + sqrt(maxGraphErrors.angularLink.rotVariance()), Parameters::kRGBDOptimizeMaxError().c_str(), _optimizationMaxError); reject = true; } - else if(_optimizationMaxError == 0.0f && maxAngularErrorRatio>100 && !_graphOptimizer->isRobust()) + else if(_optimizationMaxError == 0.0f && maxGraphErrors.angularRatio>100 && !_graphOptimizer->isRobust()) { UERROR("Huge optimization error detected!" "Angular error ratio of %f (edge %d->%d, type=%d, abs error=%f m, stddev=%f). You may consider " "enabling \"%s\" to reject those bad optimizations by setting it to a non null value!", - maxAngularErrorRatio, - maxAngularLink->from(), - maxAngularLink->to(), - maxAngularLink->type(), - maxAngularError*180.0f/CV_PI, - sqrt(maxAngularLink->rotVariance()), + maxGraphErrors.angularRatio, + maxGraphErrors.angularLink.from(), + maxGraphErrors.angularLink.to(), + maxGraphErrors.angularLink.type(), + maxGraphErrors.angular*180.0f/CV_PI, + sqrt(maxGraphErrors.angularLink.rotVariance()), Parameters::kRGBDOptimizeMaxError().c_str()); } } @@ -4112,12 +4186,24 @@ bool Rtabmap::process( statistics_.addStatistic(Statistics::kLoopLinear_variance(), loopClosureLinearVariance); statistics_.addStatistic(Statistics::kLoopAngular_variance(), loopClosureAngularVariance); statistics_.addStatistic(Statistics::kLoopLast_id(), _memory->getLastGlobalLoopClosureId()); - statistics_.addStatistic(Statistics::kLoopOptimization_max_error(), maxLinearError); - statistics_.addStatistic(Statistics::kLoopOptimization_max_error_ratio(), maxLinearErrorRatio); - statistics_.addStatistic(Statistics::kLoopOptimization_max_ang_error(), maxAngularError*180.0f/M_PI); - statistics_.addStatistic(Statistics::kLoopOptimization_max_ang_error_ratio(), maxAngularErrorRatio); - statistics_.addStatistic(Statistics::kLoopOptimization_max_error_from_id(), maxLinearErrorFromId); - statistics_.addStatistic(Statistics::kLoopOptimization_max_error_to_id(), maxLinearErrorToId); + if(maxGraphErrors.linear>=0 || maxGraphErrors.angular>=0) + { + statistics_.addStatistic(Statistics::kLoopOptimization_max_error(), maxGraphErrors.linear); + statistics_.addStatistic(Statistics::kLoopOptimization_max_error_ratio(), maxGraphErrors.linearRatio); + statistics_.addStatistic(Statistics::kLoopOptimization_max_ang_error(), maxGraphErrors.angular*180.0f/M_PI); + statistics_.addStatistic(Statistics::kLoopOptimization_max_ang_error_ratio(), maxGraphErrors.angularRatio); + statistics_.addStatistic(Statistics::kLoopOptimization_max_error_from_id(), maxGraphErrorsLinearIds.first); + statistics_.addStatistic(Statistics::kLoopOptimization_max_error_to_id(), maxGraphErrorsLinearIds.second); + statistics_.addStatistic(Statistics::kLoopOptimization_max_ang_error_from_id(), maxGraphErrorsAngularIds.first); + statistics_.addStatistic(Statistics::kLoopOptimization_max_ang_error_to_id(), maxGraphErrorsAngularIds.second); + + if(_optimizationMaxErrorRepairRadius > 0) + { + statistics_.addStatistic(Statistics::kLoopOptimization_max_error_removed_from_id(), maxGraphErrorsRemovedIds.first); + statistics_.addStatistic(Statistics::kLoopOptimization_max_error_removed_to_id(), maxGraphErrorsRemovedIds.second); + statistics_.addStatistic(Statistics::kLoopOptimization_max_error_removed_count(), maxGraphErrorsRemovedCount); + } + } statistics_.addStatistic(Statistics::kLoopOptimization_error(), optimizationError); statistics_.addStatistic(Statistics::kLoopOptimization_iterations(), optimizationIterations); statistics_.addStatistic(Statistics::kLoopLandmark_detected(), landmarksDetected.empty()?0:-landmarksDetected.begin()->first); @@ -5425,6 +5511,141 @@ std::map Rtabmap::optimizeGraph( return optimizedPoses; } +// If repairing works, all input arguments are updated accordingly to new graph +// Returns IDs of the links removed from constraints +std::list > Rtabmap::repairGraph( + graph::MaxGraphErrors & maxGraphErrors, + std::map & poses, + std::multimap & constraints, + double & optimizationError, + int & optimizationIterations, + cv::Mat & optimizationCovariance) +{ + UASSERT(maxGraphErrors.linearLink.isValid()); + UASSERT(graph::findLink(constraints, maxGraphErrors.linearLink.from(), maxGraphErrors.linearLink.to()) != constraints.end()); + + int originalMaxErrorLinkFrom = maxGraphErrors.linearLink.from(); + int originalMaxErrorLinkTo = maxGraphErrors.linearLink.to(); + + graph::MaxGraphErrors subMaxGraphErrors = maxGraphErrors; + std::list > removedLinks; + std::map subPoses = poses; + std::multimap subConstraints = constraints; + while(subMaxGraphErrors.linearLink.isValid() && subMaxGraphErrors.linearRatio > _optimizationMaxError) + { + removedLinks.push_back(std::make_pair(subMaxGraphErrors.linearLink.from(), subMaxGraphErrors.linearLink.to())); + subConstraints.erase(graph::findLink(subConstraints, subMaxGraphErrors.linearLink.from(), subMaxGraphErrors.linearLink.to())); + subMaxGraphErrors.linearLink = Link(); + + // Get connected graph just to check if a node got disconnected (localization mode) + std::map posesOut; + std::multimap subConstraintsOut; + _graphOptimizer->getConnectedGraph(subPoses.rbegin()->first, subPoses, subConstraints, posesOut, subConstraintsOut); + if(subPoses.size() - posesOut.size() > 1) + { + UWARN("More than one pose filtered in one iteration when trying to repair graph. Aborting repairing."); + break; + } + else if(subPoses.size() != posesOut.size()) + { + for(std::map::iterator pter=subPoses.begin(); pter!=subPoses.end(); ++pter) + { + if(posesOut.find(pter->first) == posesOut.end()) + { + subPoses.erase(pter); + break; // should be only one if different, break now + } + } + } + cv::Mat subOptimizationCovariance; + double subOptimizationError = 0.0; + int subOptimizationIterations = 0; + + int fromId = subPoses.rbegin()->first; + if(!_optimizeFromGraphEnd) + { + // Find first node that is not intermediate + for(std::map::iterator iter = subPoses.lower_bound(1); iter!=subPoses.end(); ++iter) + { + // Make sure fromId is not an intermediate node + const Signature * s = _memory->getSignature(iter->first); + if(s && s->getWeight() != -1) + { + fromId = iter->first; + break; + } + else if(!s) + { + UWARN("Not found node %d in memory?!", iter->first); + } + } + } + + subPoses = _graphOptimizer->optimize( + fromId, + subPoses, + subConstraintsOut, + subOptimizationCovariance, + 0, + &subOptimizationError, + &subOptimizationIterations); + + if(subPoses.empty()) + { + UWARN("Optimization failed when trying to repair graph."); + } + else + { + subMaxGraphErrors = graph::computeMaxGraphErrors( + subPoses, + subConstraintsOut); + if(!subMaxGraphErrors.linearLink.isValid()) + { + UWARN("Could not compute graph errors! Wrong loop closures could be accepted!"); + } + else if(subMaxGraphErrors.linearRatio > _optimizationMaxError) + { + float distance = poses.at(originalMaxErrorLinkFrom).getDistance(poses.at(subMaxGraphErrors.linearLink.from())); + if(subMaxGraphErrors.linearLink.type() != Link::kNeighbor && distance < _optimizationMaxErrorRepairRadius) + { + UWARN("Optimization error is still high (%f, on link %d->%d type=%d) after removing the loop closure with the highest error. " + "As it is close (%f m < %s=%f m) to original loop closure with high error (%d->%d), we will reject again this one to see if it helps.", + subMaxGraphErrors.linearRatio, subMaxGraphErrors.linearLink.from(), subMaxGraphErrors.linearLink.to(), subMaxGraphErrors.linearLink.type(), + distance, Parameters::kRGBDOptimizeMaxErrorRepairRadius().c_str(), _optimizationMaxErrorRepairRadius, + originalMaxErrorLinkFrom, originalMaxErrorLinkTo); + } + else + { + UWARN("Optimization error is still high (%f, on link %d->%d) after removing loop closure with highest error.", + subMaxGraphErrors.linearRatio, subMaxGraphErrors.linearLink.from(), subMaxGraphErrors.linearLink.to()); + subMaxGraphErrors.linearLink = Link(); + } + } + else + { + UWARN("Optimization error is lower (%f, on link %d->%d) after removing loop " + "closure with highest error. We will remove the old link (%d->%d collaterals=%ld) and accept the new one.", + subMaxGraphErrors.linearRatio, subMaxGraphErrors.linearLink.from(), subMaxGraphErrors.linearLink.to(), + removedLinks.front().first, removedLinks.front().second, + removedLinks.size()-1); + + maxGraphErrors = subMaxGraphErrors; + + // Make sure the link pointers in maxGraphErrors point on same constraints + std::swap(poses, subPoses); + std::swap(constraints, subConstraintsOut); + + optimizationCovariance = subOptimizationCovariance; + optimizationError = subOptimizationError; + optimizationIterations = subOptimizationIterations; + + return removedLinks; + } + } + } + return std::list >(); +} + void Rtabmap::adjustLikelihood(std::map & likelihood) const { ULOGGER_DEBUG("likelihood.size()=%d", likelihood.size()); @@ -5960,12 +6181,7 @@ int Rtabmap::detectMoreLoopClosures( } std::multimap linksIn = links; linksIn.insert(std::make_pair(from, Link(from, to, Link::kUserClosure, t, getInformation(info.covariance)))); - const Link * maxLinearLink = 0; - const Link * maxAngularLink = 0; - float maxLinearError = 0.0f; - float maxAngularError = 0.0f; - float maxLinearErrorRatio = 0.0f; - float maxAngularErrorRatio = 0.0f; + graph::MaxGraphErrors maxGraphErrors; std::map optimizedPoses; std::multimap linksOut; UASSERT(poses.find(fromId) != poses.end()); @@ -5980,76 +6196,70 @@ int Rtabmap::detectMoreLoopClosures( std::string msg; if(optimizedPoses.size()) { - graph::computeMaxGraphErrors( + maxGraphErrors = graph::computeMaxGraphErrors( optimizedPoses, - linksOut, - maxLinearErrorRatio, - maxAngularErrorRatio, - maxLinearError, - maxAngularError, - &maxLinearLink, - &maxAngularLink); - if(maxLinearLink) + linksOut); + if(maxGraphErrors.linearLink.isValid()) { - UINFO("Max optimization linear error = %f m (link %d->%d)", maxLinearError, maxLinearLink->from(), maxLinearLink->to()); - if(_optimizationMaxError > 0.0f && maxLinearErrorRatio > _optimizationMaxError) + UINFO("Max optimization linear error = %f m (link %d->%d)", maxGraphErrors.linear, maxGraphErrors.linearLink.from(), maxGraphErrors.linearLink.to()); + if(_optimizationMaxError > 0.0f && maxGraphErrors.linearRatio > _optimizationMaxError) { msg = uFormat("Rejecting edge %d->%d because " "graph error is too large after optimization (%f m for edge %d->%d with ratio %f > std=%f m). " "\"%s\" is %f.", from, to, - maxLinearError, - maxLinearLink->from(), - maxLinearLink->to(), - maxLinearErrorRatio, - sqrt(maxLinearLink->transVariance()), + maxGraphErrors.linear, + maxGraphErrors.linearLink.from(), + maxGraphErrors.linearLink.to(), + maxGraphErrors.linearRatio, + sqrt(maxGraphErrors.linearLink.transVariance()), Parameters::kRGBDOptimizeMaxError().c_str(), _optimizationMaxError); } - else if(_optimizationMaxError == 0.0f && maxLinearErrorRatio>100 && !_graphOptimizer->isRobust()) + else if(_optimizationMaxError == 0.0f && maxGraphErrors.linearRatio>100 && !_graphOptimizer->isRobust()) { UERROR("Huge optimization error detected!" "Linear error ratio of %f (edge %d->%d, type=%d, abs error=%f m, stddev=%f). You may consider " "enabling \"%s\" to reject those bad optimizations by setting it to a non null value!", - maxLinearErrorRatio, - maxLinearLink->from(), - maxLinearLink->to(), - maxLinearLink->type(), - maxLinearError, - sqrt(maxLinearLink->transVariance()), + maxGraphErrors.linearRatio, + maxGraphErrors.linearLink.from(), + maxGraphErrors.linearLink.to(), + maxGraphErrors.linearLink.type(), + maxGraphErrors.linear, + sqrt(maxGraphErrors.linearLink.transVariance()), Parameters::kRGBDOptimizeMaxError().c_str()); } } - else if(maxAngularLink) + else if(maxGraphErrors.angularLink.isValid()) { - UINFO("Max optimization angular error = %f deg (link %d->%d)", maxAngularError*180.0f/M_PI, maxAngularLink->from(), maxAngularLink->to()); - if(_optimizationMaxError > 0.0f && maxAngularErrorRatio > _optimizationMaxError) + UINFO("Max optimization angular error = %f deg (link %d->%d)", maxGraphErrors.angular*180.0f/M_PI, maxGraphErrors.angularLink.from(), maxGraphErrors.angularLink.to()); + if(_optimizationMaxError > 0.0f && maxGraphErrors.angularRatio > _optimizationMaxError) { msg = uFormat("Rejecting edge %d->%d because " "graph error is too large after optimization (%f deg for edge %d->%d with ratio %f > std=%f deg). " "\"%s\" is %f m.", from, to, - maxAngularError*180.0f/M_PI, - maxAngularLink->from(), - maxAngularLink->to(), - maxAngularErrorRatio, - sqrt(maxAngularLink->rotVariance()), + maxGraphErrors.angular*180.0f/M_PI, + maxGraphErrors.angularLink.from(), + maxGraphErrors.angularLink.to(), + maxGraphErrors.angularRatio, + sqrt(maxGraphErrors.angularLink.rotVariance()), Parameters::kRGBDOptimizeMaxError().c_str(), _optimizationMaxError); } - else if(_optimizationMaxError == 0.0f && maxAngularErrorRatio>100 && !_graphOptimizer->isRobust()) + else if(_optimizationMaxError == 0.0f && maxGraphErrors.angularRatio>100 && !_graphOptimizer->isRobust()) { UERROR("Huge optimization error detected!" "Angular error ratio of %f (edge %d->%d, type=%d, abs error=%f m, stddev=%f). You may consider " "enabling \"%s\" to reject those bad optimizations by setting it to a non null value!", - maxAngularErrorRatio, - maxAngularLink->from(), - maxAngularLink->to(), - maxAngularLink->type(), - maxAngularError*180.0f/CV_PI, - sqrt(maxAngularLink->rotVariance()), + maxGraphErrors.angularRatio, + maxGraphErrors.angularLink.from(), + maxGraphErrors.angularLink.to(), + maxGraphErrors.angularLink.type(), + maxGraphErrors.angular*180.0f/CV_PI, + sqrt(maxGraphErrors.angularLink.rotVariance()), Parameters::kRGBDOptimizeMaxError().c_str()); } } @@ -6352,83 +6562,72 @@ bool Rtabmap::addLink(const Link & link) } else { - float maxLinearError = 0.0f; - float maxLinearErrorRatio = 0.0f; - float maxAngularError = 0.0f; - float maxAngularErrorRatio = 0.0f; - const Link * maxLinearLink = 0; - const Link * maxAngularLink = 0; + graph::MaxGraphErrors maxGraphErrors; - graph::computeMaxGraphErrors( + maxGraphErrors = graph::computeMaxGraphErrors( poses, - links, - maxLinearErrorRatio, - maxAngularErrorRatio, - maxLinearError, - maxAngularError, - &maxLinearLink, - &maxAngularLink); - if(maxLinearLink) + links); + if(maxGraphErrors.linearLink.isValid()) { - UINFO("Max optimization linear error = %f m (link %d->%d)", maxLinearError, maxLinearLink->from(), maxLinearLink->to()); - if(_optimizationMaxError > 0.0f && maxLinearErrorRatio > _optimizationMaxError) + UINFO("Max optimization linear error = %f m (link %d->%d)", maxGraphErrors.linear, maxGraphErrors.linearLink.from(), maxGraphErrors.linearLink.to()); + if(_optimizationMaxError > 0.0f && maxGraphErrors.linearRatio > _optimizationMaxError) { msg = uFormat("Rejecting edge %d->%d because " "graph error is too large after optimization (%f m for edge %d->%d with ratio %f > std=%f m). " "\"%s\" is %f.", link.from(), link.to(), - maxLinearError, - maxLinearLink->from(), - maxLinearLink->to(), - maxLinearErrorRatio, - sqrt(maxLinearLink->transVariance()), + maxGraphErrors.linear, + maxGraphErrors.linearLink.from(), + maxGraphErrors.linearLink.to(), + maxGraphErrors.linearRatio, + sqrt(maxGraphErrors.linearLink.transVariance()), Parameters::kRGBDOptimizeMaxError().c_str(), _optimizationMaxError); } - else if(_optimizationMaxError == 0.0f && maxLinearErrorRatio>100 && !_graphOptimizer->isRobust()) + else if(_optimizationMaxError == 0.0f && maxGraphErrors.linearRatio>100 && !_graphOptimizer->isRobust()) { UERROR("Huge optimization error detected!" "Linear error ratio of %f (edge %d->%d, type=%d, abs error=%f m, stddev=%f). You may consider " "enabling \"%s\" to reject those bad optimizations by setting it to a non null value!", - maxLinearErrorRatio, - maxLinearLink->from(), - maxLinearLink->to(), - maxLinearLink->type(), - maxLinearError, - sqrt(maxLinearLink->transVariance()), + maxGraphErrors.linearRatio, + maxGraphErrors.linearLink.from(), + maxGraphErrors.linearLink.to(), + maxGraphErrors.linearLink.type(), + maxGraphErrors.linear, + sqrt(maxGraphErrors.linearLink.transVariance()), Parameters::kRGBDOptimizeMaxError().c_str()); } } - else if(maxAngularLink) + else if(maxGraphErrors.angularLink.isValid()) { - UINFO("Max optimization angular error = %f deg (link %d->%d)", maxAngularError*180.0f/M_PI, maxAngularLink->from(), maxAngularLink->to()); - if(_optimizationMaxError > 0.0f && maxAngularErrorRatio > _optimizationMaxError) + UINFO("Max optimization angular error = %f deg (link %d->%d)", maxGraphErrors.angular*180.0f/M_PI, maxGraphErrors.angularLink.from(), maxGraphErrors.angularLink.to()); + if(_optimizationMaxError > 0.0f && maxGraphErrors.angularRatio > _optimizationMaxError) { msg = uFormat("Rejecting edge %d->%d because " "graph error is too large after optimization (%f deg for edge %d->%d with ratio %f > std=%f deg). " "\"%s\" is %f m.", link.from(), link.to(), - maxAngularError*180.0f/M_PI, - maxAngularLink->from(), - maxAngularLink->to(), - maxAngularErrorRatio, - sqrt(maxAngularLink->rotVariance()), + maxGraphErrors.angular*180.0f/M_PI, + maxGraphErrors.angularLink.from(), + maxGraphErrors.angularLink.to(), + maxGraphErrors.angularRatio, + sqrt(maxGraphErrors.angularLink.rotVariance()), Parameters::kRGBDOptimizeMaxError().c_str(), _optimizationMaxError); } - else if(_optimizationMaxError == 0.0f && maxAngularErrorRatio>100 && !_graphOptimizer->isRobust()) + else if(_optimizationMaxError == 0.0f && maxGraphErrors.angularRatio>100 && !_graphOptimizer->isRobust()) { UERROR("Huge optimization error detected!" "Angular error ratio of %f (edge %d->%d, type=%d, abs error=%f m, stddev=%f). You may consider " "enabling \"%s\" to reject those bad optimizations by setting it to a non null value!", - maxAngularErrorRatio, - maxAngularLink->from(), - maxAngularLink->to(), - maxAngularLink->type(), - maxAngularError*180.0f/CV_PI, - sqrt(maxAngularLink->rotVariance()), + maxGraphErrors.angularRatio, + maxGraphErrors.angularLink.from(), + maxGraphErrors.angularLink.to(), + maxGraphErrors.angularLink.type(), + maxGraphErrors.angular*180.0f/CV_PI, + sqrt(maxGraphErrors.angularLink.rotVariance()), Parameters::kRGBDOptimizeMaxError().c_str()); } } @@ -6539,31 +6738,19 @@ bool Rtabmap::addLink(const Link & link) else { UINFO("Compute max graph errors..."); - float maxLinearError = 0.0f; - float maxLinearErrorRatio = 0.0f; - float maxAngularError = 0.0f; - float maxAngularErrorRatio = 0.0f; - const Link * maxLinearLink = 0; - const Link * maxAngularLink = 0; - graph::computeMaxGraphErrors( + graph::MaxGraphErrors maxGraphErrors = graph::computeMaxGraphErrors( optPoses, edgeConstraintsOut, - maxLinearErrorRatio, - maxAngularErrorRatio, - maxLinearError, - maxAngularError, - &maxLinearLink, - &maxAngularLink, _graphOptimizer->isSlam2d()); - if(maxLinearLink == 0 && maxAngularLink==0) + if(!maxGraphErrors.linearLink.isValid() && !maxGraphErrors.angularLink.isValid()) { UWARN("Could not compute graph errors! Wrong loop closures could be accepted!"); } - if(maxLinearLink) + if(maxGraphErrors.linearLink.isValid()) { - UINFO("Max optimization linear error = %f m (link %d->%d, var=%f, ratio error/std=%f)", maxLinearError, maxLinearLink->from(), maxLinearLink->to(), maxLinearLink->transVariance(), maxLinearError/sqrt(maxLinearLink->transVariance())); - if(_optimizationMaxError > 0.0f && maxLinearErrorRatio > _optimizationMaxError) + UINFO("Max optimization linear error = %f m (link %d->%d, var=%f, ratio error/std=%f)", maxGraphErrors.linear, maxGraphErrors.linearLink.from(), maxGraphErrors.linearLink.to(), maxGraphErrors.linearLink.transVariance(), maxGraphErrors.linear/sqrt(maxGraphErrors.linearLink.transVariance())); + if(_optimizationMaxError > 0.0f && maxGraphErrors.linearRatio > _optimizationMaxError) { UWARN("Rejecting localization (%d <-> %d) in this " "iteration because a wrong loop closure has been " @@ -6572,34 +6759,34 @@ bool Rtabmap::addLink(const Link & link) "maximum error ratio parameter \"%s\" is %f of std deviation.", link.from(), link.to(), - maxLinearErrorRatio, - maxLinearLink->from(), - maxLinearLink->to(), - maxLinearLink->type(), - maxLinearError, - sqrt(maxLinearLink->transVariance()), + maxGraphErrors.linearRatio, + maxGraphErrors.linearLink.from(), + maxGraphErrors.linearLink.to(), + maxGraphErrors.linearLink.type(), + maxGraphErrors.linear, + sqrt(maxGraphErrors.linearLink.transVariance()), Parameters::kRGBDOptimizeMaxError().c_str(), _optimizationMaxError); rejectLocalization = true; } - else if(_optimizationMaxError == 0.0f && maxLinearErrorRatio>100 && !_graphOptimizer->isRobust()) + else if(_optimizationMaxError == 0.0f && maxGraphErrors.linearRatio>100 && !_graphOptimizer->isRobust()) { UERROR("Huge optimization error detected!" "Linear error ratio of %f (edge %d->%d, type=%d, abs error=%f m, stddev=%f). You may consider " "enabling \"%s\" to reject those bad optimizations by setting it to a non null value!", - maxLinearErrorRatio, - maxLinearLink->from(), - maxLinearLink->to(), - maxLinearLink->type(), - maxLinearError, - sqrt(maxLinearLink->transVariance()), + maxGraphErrors.linearRatio, + maxGraphErrors.linearLink.from(), + maxGraphErrors.linearLink.to(), + maxGraphErrors.linearLink.type(), + maxGraphErrors.linear, + sqrt(maxGraphErrors.linearLink.transVariance()), Parameters::kRGBDOptimizeMaxError().c_str()); } } - if(maxAngularLink) + if(maxGraphErrors.angularLink.isValid()) { - UINFO("Max optimization angular error = %f deg (link %d->%d, var=%f, ratio error/std=%f)", maxAngularError*180.0f/CV_PI, maxAngularLink->from(), maxAngularLink->to(), maxAngularLink->rotVariance(), maxAngularError/sqrt(maxAngularLink->rotVariance())); - if(_optimizationMaxError > 0.0f && maxAngularErrorRatio > _optimizationMaxError) + UINFO("Max optimization angular error = %f deg (link %d->%d, var=%f, ratio error/std=%f)", maxGraphErrors.angular*180.0f/CV_PI, maxGraphErrors.angularLink.from(), maxGraphErrors.angularLink.to(), maxGraphErrors.angularLink.rotVariance(), maxGraphErrors.angular/sqrt(maxGraphErrors.angularLink.rotVariance())); + if(_optimizationMaxError > 0.0f && maxGraphErrors.angularRatio > _optimizationMaxError) { UWARN("Rejecting localization (%d <-> %d) in this " "iteration because a wrong loop closure has been " @@ -6608,27 +6795,27 @@ bool Rtabmap::addLink(const Link & link) "maximum error ratio parameter \"%s\" is %f of std deviation.", link.from(), link.to(), - maxAngularErrorRatio, - maxAngularLink->from(), - maxAngularLink->to(), - maxAngularLink->type(), - maxAngularError*180.0f/CV_PI, - sqrt(maxAngularLink->rotVariance()), + maxGraphErrors.angularRatio, + maxGraphErrors.angularLink.from(), + maxGraphErrors.angularLink.to(), + maxGraphErrors.angularLink.type(), + maxGraphErrors.angular*180.0f/CV_PI, + sqrt(maxGraphErrors.angularLink.rotVariance()), Parameters::kRGBDOptimizeMaxError().c_str(), _optimizationMaxError); rejectLocalization = true; } - else if(_optimizationMaxError == 0.0f && maxAngularErrorRatio>100 && !_graphOptimizer->isRobust()) + else if(_optimizationMaxError == 0.0f && maxGraphErrors.angularRatio>100 && !_graphOptimizer->isRobust()) { UERROR("Huge optimization error detected!" "Angular error ratio of %f (edge %d->%d, type=%d, abs error=%f m, stddev=%f). You may consider " "enabling \"%s\" to reject those bad optimizations by setting it to a non null value!", - maxAngularErrorRatio, - maxAngularLink->from(), - maxAngularLink->to(), - maxAngularLink->type(), - maxAngularError*180.0f/CV_PI, - sqrt(maxAngularLink->rotVariance()), + maxGraphErrors.angularRatio, + maxGraphErrors.angularLink.from(), + maxGraphErrors.angularLink.to(), + maxGraphErrors.angularLink.type(), + maxGraphErrors.angular*180.0f/CV_PI, + sqrt(maxGraphErrors.angularLink.rotVariance()), Parameters::kRGBDOptimizeMaxError().c_str()); } } diff --git a/corelib/src/global_map/OccupancyGrid.cpp b/corelib/src/global_map/OccupancyGrid.cpp index 19c1aa4d..0590bc45 100644 --- a/corelib/src/global_map/OccupancyGrid.cpp +++ b/corelib/src/global_map/OccupancyGrid.cpp @@ -222,14 +222,14 @@ void OccupancyGrid::assemble(const std::list > & newPo if(!cache().empty()) { - UDEBUG("Updating from cache"); + UDEBUG("Updating %ld poses from cache", newPoses.size()); for(std::list >::const_iterator iter = newPoses.begin(); iter!=newPoses.end(); ++iter) { if(uContains(cache(), iter->first)) { const LocalGrid & localGrid = cache().at(iter->first); - UDEBUG("Adding grid %d: ground=%d obstacles=%d empty=%d", iter->first, localGrid.groundCells.cols, localGrid.obstacleCells.cols, localGrid.emptyCells.cols); + //UDEBUG("Adding grid %d: ground=%d obstacles=%d empty=%d", iter->first, localGrid.groundCells.cols, localGrid.obstacleCells.cols, localGrid.emptyCells.cols); //ground cv::Mat ground; diff --git a/guilib/src/DatabaseViewer.cpp b/guilib/src/DatabaseViewer.cpp index b95bdb77..8951266a 100644 --- a/guilib/src/DatabaseViewer.cpp +++ b/guilib/src/DatabaseViewer.cpp @@ -9128,10 +9128,6 @@ bool DatabaseViewer::addConstraint(int from, int to, Registration * reg, bool si int fromId = newLink.from(); std::multimap linksIn = updateLinksWithModifications(links_); linksIn.insert(std::make_pair(newLink.from(), newLink)); - const Link * maxLinearLink = 0; - const Link * maxAngularLink = 0; - float maxLinearErrorRatio = 0.0f; - float maxAngularErrorRatio = 0.0f; Optimizer * optimizer = Optimizer::create(ui_->parameters_toolbox->getParameters()); std::map poses; std::multimap links; @@ -9164,51 +9160,43 @@ bool DatabaseViewer::addConstraint(int from, int to, Registration * reg, bool si std::string msg; if(poses.size()) { - float maxLinearError = 0.0f; - float maxAngularError = 0.0f; - graph::computeMaxGraphErrors( + graph::MaxGraphErrors maxGraphErrors = graph::computeMaxGraphErrors( poses, - links, - maxLinearErrorRatio, - maxAngularErrorRatio, - maxLinearError, - maxAngularError, - &maxLinearLink, - &maxAngularLink); - if(maxLinearLink) + links); + if(maxGraphErrors.linearLink.isValid()) { - UINFO("Max optimization linear error = %f m (link %d->%d, var=%f, ratio error/std=%f)", maxLinearError, maxLinearLink->from(), maxLinearLink->to(), maxLinearLink->transVariance(), maxLinearError/sqrt(maxLinearLink->transVariance())); - if(maxLinearErrorRatio > maxOptimizationError) + UINFO("Max optimization linear error = %f m (link %d->%d, var=%f, ratio error/std=%f)", maxGraphErrors.linear, maxGraphErrors.linearLink.from(), maxGraphErrors.linearLink.to(), maxGraphErrors.linearLink.transVariance(), maxGraphErrors.linear/sqrt(maxGraphErrors.linearLink.transVariance())); + if(maxGraphErrors.linearRatio > maxOptimizationError) { msg = uFormat("Rejecting edge %d->%d because " "graph error is too large (abs=%f m) after optimization (ratio %f for edge %d->%d, stddev=%f m). " "\"%s\" is %f.", newLink.from(), newLink.to(), - maxLinearError, - maxLinearErrorRatio, - maxLinearLink->from(), - maxLinearLink->to(), - sqrt(maxLinearLink->transVariance()), + maxGraphErrors.linear, + maxGraphErrors.linearRatio, + maxGraphErrors.linearLink.from(), + maxGraphErrors.linearLink.to(), + sqrt(maxGraphErrors.linearLink.transVariance()), Parameters::kRGBDOptimizeMaxError().c_str(), maxOptimizationError); } } - if(maxAngularLink) + if(maxGraphErrors.angularLink.isValid()) { - UINFO("Max optimization angular error = %f deg (link %d->%d, var=%f, ratio error/std=%f)", maxAngularError*180.0f/CV_PI, maxAngularLink->from(), maxAngularLink->to(), maxAngularLink->rotVariance(), maxAngularError/sqrt(maxAngularLink->rotVariance())); - if(maxAngularErrorRatio > maxOptimizationError) + UINFO("Max optimization angular error = %f deg (link %d->%d, var=%f, ratio error/std=%f)", maxGraphErrors.angular*180.0f/CV_PI, maxGraphErrors.angularLink.from(), maxGraphErrors.angularLink.to(), maxGraphErrors.angularLink.rotVariance(), maxGraphErrors.angular/sqrt(maxGraphErrors.angularLink.rotVariance())); + if(maxGraphErrors.angularRatio > maxOptimizationError) { msg = uFormat("Rejecting edge %d->%d because " "graph error is too large (abs=%f deg) after optimization (ratio %f for edge %d->%d, stddev=%f deg). " "\"%s\" is %f.", newLink.from(), newLink.to(), - maxAngularError*180.0f/CV_PI, - maxAngularErrorRatio, - maxAngularLink->from(), - maxAngularLink->to(), - sqrt(maxAngularLink->rotVariance()), + maxGraphErrors.angular*180.0f/CV_PI, + maxGraphErrors.angularRatio, + maxGraphErrors.angularLink.from(), + maxGraphErrors.angularLink.to(), + sqrt(maxGraphErrors.angularLink.rotVariance()), Parameters::kRGBDOptimizeMaxError().c_str(), maxOptimizationError); } @@ -9450,7 +9438,7 @@ std::multimap DatabaseViewer::updateLinksWithModifications( findIter = rtabmap::graph::findLink(linksRemoved_, iter->second.from(), iter->second.to()); if(findIter != linksRemoved_.end()) { - UDEBUG("Removed link (%d->%d, %d)", iter->second.from(), iter->second.to(), iter->second.type()); + //UDEBUG("Removed link (%d->%d, %d)", iter->second.from(), iter->second.to(), iter->second.type()); continue; // don't add this link } @@ -9467,7 +9455,7 @@ std::multimap DatabaseViewer::updateLinksWithModifications( { links.insert(*findIter); } - UDEBUG("Updated link (%d->%d, %d)", iter->second.from(), iter->second.to(), iter->second.type()); + //UDEBUG("Updated link (%d->%d, %d)", iter->second.from(), iter->second.to(), iter->second.type()); continue; } @@ -9486,11 +9474,11 @@ std::multimap DatabaseViewer::updateLinksWithModifications( if(findIter->second.from() != findIter->second.to()) { links.insert(std::make_pair(findIter->second.to(), findIter->second.inverse())); // return both ways } - UDEBUG("Added refined link (%d->%d, %d)", findIter->second.from(), findIter->second.to(), findIter->second.type()); + //UDEBUG("Added refined link (%d->%d, %d)", findIter->second.from(), findIter->second.to(), findIter->second.type()); continue; } - UDEBUG("Added link (%d->%d, %d)", iter->second.from(), iter->second.to(), iter->second.type()); + //UDEBUG("Added link (%d->%d, %d)", iter->second.from(), iter->second.to(), iter->second.type()); links.insert(*iter); if(iter->second.from() != iter->second.to()) { links.insert(std::make_pair(iter->second.to(), iter->second.inverse())); // return both ways diff --git a/guilib/src/MainWindow.cpp b/guilib/src/MainWindow.cpp index 5b691262..989d9aee 100644 --- a/guilib/src/MainWindow.cpp +++ b/guilib/src/MainWindow.cpp @@ -6862,10 +6862,6 @@ void MainWindow::postProcessing( } std::multimap linksIn = _currentLinksMap; linksIn.insert(std::make_pair(from, Link(from, to, Link::kUserClosure, transform, information))); - const Link * maxLinearLink = 0; - const Link * maxAngularLink = 0; - float maxLinearError = 0.0f; - float maxAngularError = 0.0f; std::map poses; std::multimap links; UASSERT(_currentPosesMap.find(fromId) != _currentPosesMap.end()); @@ -6880,51 +6876,43 @@ void MainWindow::postProcessing( std::string msg; if(poses.size()) { - float maxLinearErrorRatio = 0.0f; - float maxAngularErrorRatio = 0.0f; - graph::computeMaxGraphErrors( + graph::MaxGraphErrors maxGraphErrors = graph::computeMaxGraphErrors( poses, - links, - maxLinearErrorRatio, - maxAngularErrorRatio, - maxLinearError, - maxAngularError, - &maxLinearLink, - &maxAngularLink); - if(maxLinearLink) + links); + if(maxGraphErrors.linearLink.isValid()) { - UINFO("Max optimization linear error = %f m (link %d->%d)", maxLinearError, maxLinearLink->from(), maxLinearLink->to()); - if(maxLinearErrorRatio > optimizeMaxError) + UINFO("Max optimization linear error = %f m (link %d->%d)", maxGraphErrors.linear, maxGraphErrors.linearLink.from(), maxGraphErrors.linearLink.to()); + if(maxGraphErrors.linearRatio > optimizeMaxError) { msg = uFormat("Rejecting edge %d->%d because " "graph error is too large after optimization (%f m for edge %d->%d with ratio %f > std=%f m). " "\"%s\" is %f.", from, to, - maxLinearError, - maxLinearLink->from(), - maxLinearLink->to(), - maxLinearErrorRatio, - sqrt(maxLinearLink->transVariance()), + maxGraphErrors.linear, + maxGraphErrors.linearLink.from(), + maxGraphErrors.linearLink.to(), + maxGraphErrors.linearRatio, + sqrt(maxGraphErrors.linearLink.transVariance()), Parameters::kRGBDOptimizeMaxError().c_str(), optimizeMaxError); } } - else if(maxAngularLink) + else if(maxGraphErrors.angularLink.isValid()) { - UINFO("Max optimization angular error = %f deg (link %d->%d)", maxAngularError*180.0f/M_PI, maxAngularLink->from(), maxAngularLink->to()); - if(maxAngularErrorRatio > optimizeMaxError) + UINFO("Max optimization angular error = %f deg (link %d->%d)", maxGraphErrors.angular*180.0f/M_PI, maxGraphErrors.angularLink.from(), maxGraphErrors.angularLink.to()); + if(maxGraphErrors.angularRatio > optimizeMaxError) { msg = uFormat("Rejecting edge %d->%d because " "graph error is too large after optimization (%f deg for edge %d->%d with ratio %f > std=%f deg). " "\"%s\" is %f m.", from, to, - maxAngularError*180.0f/M_PI, - maxAngularLink->from(), - maxAngularLink->to(), - maxAngularErrorRatio, - sqrt(maxAngularLink->rotVariance()), + maxGraphErrors.angular*180.0f/M_PI, + maxGraphErrors.angularLink.from(), + maxGraphErrors.angularLink.to(), + maxGraphErrors.angularRatio, + sqrt(maxGraphErrors.angularLink.rotVariance()), Parameters::kRGBDOptimizeMaxError().c_str(), optimizeMaxError); } diff --git a/guilib/src/PreferencesDialog.cpp b/guilib/src/PreferencesDialog.cpp index 44ea07a3..b71a39a2 100644 --- a/guilib/src/PreferencesDialog.cpp +++ b/guilib/src/PreferencesDialog.cpp @@ -1272,6 +1272,7 @@ PreferencesDialog::PreferencesDialog(QWidget * parent) : _ui->graphOptimization_covarianceIgnored->setObjectName(Parameters::kOptimizerVarianceIgnored().c_str()); _ui->graphOptimization_fromGraphEnd->setObjectName(Parameters::kRGBDOptimizeFromGraphEnd().c_str()); _ui->graphOptimization_maxError->setObjectName(Parameters::kRGBDOptimizeMaxError().c_str()); + _ui->graphOptimization_maxErrorRepairRadius->setObjectName(Parameters::kRGBDOptimizeMaxErrorRepairRadius().c_str()); _ui->graphOptimization_gravitySigma->setObjectName(Parameters::kOptimizerGravitySigma().c_str()); _ui->graphOptimization_stopEpsilon->setObjectName(Parameters::kOptimizerEpsilon().c_str()); _ui->graphOptimization_robust->setObjectName(Parameters::kOptimizerRobust().c_str()); diff --git a/guilib/src/ui/preferencesDialog.ui b/guilib/src/ui/preferencesDialog.ui index 29a6f056..36972d70 100644 --- a/guilib/src/ui/preferencesDialog.ui +++ b/guilib/src/ui/preferencesDialog.ui @@ -95,7 +95,7 @@ QFrame::Raised - 28 + 14 @@ -13813,6 +13813,152 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag Graph Optimization + + + + + + + 2 + + + 0.000000000000000 + + + 100.000000000000000 + + + 1.000000000000000 + + + 1.000000000000000 + + + + + + + Qt::Horizontal + + + + + + + Ignore landmarks. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Gravity sigma value (>=0, typically between 0.1 and 0.3). Optimization is done while preserving gravity orientation of the poses. This should be used only with visual/lidar inertial odometry approaches, for which we assume that all odometry poses are aligned with gravity. Set to 0 to disable gravity constraints. Currently supported only with g2o and GTSAM optimization strategies. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + + + + + Optimize graph from the newest node. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Reject loop closures if optimization error ratio is greater than this value (0=disabled). Ratio is computed as absolute error over standard deviation of each link. This will help to detect when a wrong loop closure is added to the graph. If used with robust graph optimization, the disabled loop closure links will be removed. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + 4 + + + 0.000000000000000 + + + 10.000000000000000 + + + 0.100000000000000 + + + 0.000000000000000 + + + + + + + Graph optimization algorithm. See Optimizer panel. + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + -If true, there is no odometry correction computed. All previous poses in the map are corrected instead, not the last one (which corresponds to latest odometry value). So, the transform between frames /map to /odom will be always Identity even on loop closures. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Ignore pose priors. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + @@ -13837,18 +13983,8 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - - - - Graph optimization algorithm. See Optimizer panel. - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - + + @@ -13867,82 +14003,21 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - - - + + + - - 2 - - - 0.000000000000000 - - - 100.000000000000000 - - - 1.000000000000000 - - - 1.000000000000000 - - - - - Reject loop closures if optimization error ratio is greater than this value (0=disabled). Ratio is computed as absolute error over standard deviation of each link. This will help to detect when a wrong loop closure is added to the graph. If used with robust graph optimization, the disabled loop closure links will be removed. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - + - - - - Qt::Horizontal - - - - - - - Optimize graph from the newest node. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - -If true, there is no odometry correction computed. All previous poses in the map are corrected instead, not the last one (which corresponds to latest odometry value). So, the transform between frames /map to /odom will be always Identity even on loop closures. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - + -If false, the graph is optimized from the oldest node of the current graph. It can be useful to preserve the map referential from the oldest node. An odometry correction between frames /map to /odom is computed. Warning: when some nodes are transferred, the first referential of the local map may change, resulting in momentary changes in robot/map position (which are annoying in teleoperation). @@ -13955,50 +14030,10 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - - - - Ignore pose priors. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - - - - - - - - - - - - - - - Ignore landmarks. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - + - Gravity sigma value (>=0, typically between 0.1 and 0.3). Optimization is done while preserving gravity orientation of the poses. This should be used only with visual/lidar inertial odometry approaches, for which we assume that all odometry poses are aligned with gravity. Set to 0 to disable gravity constraints. Currently supported only with g2o and GTSAM optimization strategies. + Repairing graph radius. If two consecutive loop closures are rejected by the optimization error ratio on the same old loop closure link, we will remove that old link, and other old links under that radius if necessary, until optimization is accepted. When optimization is accepted, the old loop closure links are removed from the graph. This feature is useful to reject bad loop closures that were accepted previously. Set to 0 to disable this feature. true @@ -14009,24 +14044,24 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - + - 4 + 1 0.000000000000000 - 10.000000000000000 + 999.000000000000000 - 0.100000000000000 + 1.000000000000000 - 0.000000000000000 + 1.000000000000000 From c1ef13a8d6edf5116ff9644d17c4dc1f3adeb5d9 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Mon, 4 May 2026 20:12:31 -0700 Subject: [PATCH 108/126] Various QoL (refactored RTABMAP_VERSION_COMPARE) (#1695) --- CMakeLists.txt | 8 ++++++++ Version.h.in | 2 +- corelib/src/Parameters.cpp | 6 +++--- guilib/include/rtabmap/gui/CloudViewer.h | 3 ++- guilib/src/CloudViewer.cpp | 4 +++- utilite/include/rtabmap/utilite/UStl.h | 2 +- 6 files changed, 18 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index aa3f7fb5..f80448f5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,6 +25,14 @@ SET(RTABMAP_MINOR_VERSION 23) SET(RTABMAP_PATCH_VERSION 5) SET(RTABMAP_VERSION ${RTABMAP_MAJOR_VERSION}.${RTABMAP_MINOR_VERSION}.${RTABMAP_PATCH_VERSION}) + +# Make sure we have valid version so that RTABMAP_VERSION_COMPARE logic in Version.h works +IF(RTABMAP_MINOR_VERSION GREATER 99) +MESSAGE(FATAL_ERROR "RTABMAP_MINOR_VERSION must be < 100, bump major version and restart minor to 0!") +ENDIF() +IF(RTABMAP_PATCH_VERSION GREATER 99) +MESSAGE(FATAL_ERROR "RTABMAP_PATCH_VERSION must be < 100, bump minor version and restart patch to 0!") +ENDIF() SET(PROJECT_VERSION "${RTABMAP_VERSION}") diff --git a/Version.h.in b/Version.h.in index 77b5e6f7..381dcaa3 100644 --- a/Version.h.in +++ b/Version.h.in @@ -35,7 +35,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #define RTABMAP_VERSION_MINOR @PROJECT_VERSION_MINOR@ #define RTABMAP_VERSION_PATCH @PROJECT_VERSION_PATCH@ -#define RTABMAP_VERSION_COMPARE(major, minor, patch) (major>=@PROJECT_VERSION_MAJOR@ || (major==@PROJECT_VERSION_MAJOR@ && minor>=@PROJECT_VERSION_MINOR@) || (major==@PROJECT_VERSION_MAJOR@ && minor==@PROJECT_VERSION_MINOR@ && patch >=@PROJECT_VERSION_PATCH@)) +#define RTABMAP_VERSION_COMPARE(OP, MAJOR, MINOR, PATCH) (RTABMAP_VERSION_MAJOR*10000+RTABMAP_VERSION_MINOR*100+RTABMAP_VERSION_PATCH OP MAJOR*10000+MINOR*100+PATCH) @NONFREE@#define RTABMAP_NONFREE @TORO@#define RTABMAP_TORO diff --git a/corelib/src/Parameters.cpp b/corelib/src/Parameters.cpp index 81225ac2..844f4534 100644 --- a/corelib/src/Parameters.cpp +++ b/corelib/src/Parameters.cpp @@ -1233,13 +1233,13 @@ void readINIImpl(const CSimpleIniA & ini, const std::string & configFilePath, Pa std::vector version = uListToVector(uSplit((*iter).second, '.')); if(version.size() == 3) { - if(!RTABMAP_VERSION_COMPARE(std::atoi(version[0].c_str()), std::atoi(version[1].c_str()), std::atoi(version[2].c_str()))) + if(RTABMAP_VERSION_COMPARE(<, std::atoi(version[0].c_str()), std::atoi(version[1].c_str()), std::atoi(version[2].c_str()))) { if(configFilePath.find(".rtabmap") != std::string::npos) { UWARN("Version in the config file \"%s\" is more recent (\"%s\") than " - "current RTAB-Map version used (\"%s\"). The config file will be upgraded " - "to new version.", + "current RTAB-Map version used (\"%s\"). The config file will be downgraded " + "to current RTAB-Map version if saved.", configFilePath.c_str(), (*iter).second, RTABMAP_VERSION); diff --git a/guilib/include/rtabmap/gui/CloudViewer.h b/guilib/include/rtabmap/gui/CloudViewer.h index eb16c4b1..996619d5 100644 --- a/guilib/include/rtabmap/gui/CloudViewer.h +++ b/guilib/include/rtabmap/gui/CloudViewer.h @@ -231,7 +231,8 @@ public: const Transform & to, const QColor & color, bool arrow = false, - bool foreground = false); + bool foreground = false, + double width = 1.0); void removeLine(const std::string & id); void removeAllLines(); const std::set & getAddedLines() const {return _lines;} diff --git a/guilib/src/CloudViewer.cpp b/guilib/src/CloudViewer.cpp index fc5a8b38..6b6f04d5 100644 --- a/guilib/src/CloudViewer.cpp +++ b/guilib/src/CloudViewer.cpp @@ -1732,7 +1732,8 @@ void CloudViewer::addOrUpdateLine( const Transform & to, const QColor & color, bool arrow, - bool foreground) + bool foreground, + double width) { if(id.empty()) { @@ -1764,6 +1765,7 @@ void CloudViewer::addOrUpdateLine( _visualizer->addLine(pt2, pt1, c.redF(), c.greenF(), c.blueF(), id, foreground?3:2); } _visualizer->setShapeRenderingProperties(pcl::visualization::PCL_VISUALIZER_OPACITY, c.alphaF(), id); + _visualizer->setShapeRenderingProperties(pcl::visualization::PCL_VISUALIZER_LINE_WIDTH, width, id); } } diff --git a/utilite/include/rtabmap/utilite/UStl.h b/utilite/include/rtabmap/utilite/UStl.h index 81b4a55b..3c6bdb4d 100644 --- a/utilite/include/rtabmap/utilite/UStl.h +++ b/utilite/include/rtabmap/utilite/UStl.h @@ -591,7 +591,7 @@ inline std::list uSplit(const std::string & str, char separator = ' * std::list v; * v.push_back("Hello"); * v.push_back("world!"); - * std::string joined = split(v, " "); + * std::string joined = uJoin(v, " "); * @endcode * The output string is "Hello world!" * @param strings a list of strings From f04067fa16c7699a8b934644a7eda8bc9b188578 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Tue, 5 May 2026 11:15:34 -0700 Subject: [PATCH 109/126] Fixing Rolling CI on Noble (#1696) * Fixing Rolling CI on Noble * setup env var correctly --- .devcontainer/rolling/Dockerfile | 7 ++++--- .devcontainer/rolling/devcontainer.json | 14 +++++++++++++- .github/workflows/cmake-ros.yml | 7 ++++++- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/.devcontainer/rolling/Dockerfile b/.devcontainer/rolling/Dockerfile index 38434a50..576b75fe 100644 --- a/.devcontainer/rolling/Dockerfile +++ b/.devcontainer/rolling/Dockerfile @@ -6,11 +6,12 @@ ENV DEBIAN_FRONTEND=noninteractive # Install ROS2 RUN apt update && \ apt install software-properties-common -y && \ - add-apt-repository universe && \ + add-apt-repository universe -y && \ apt update && \ apt install curl -y && \ - curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg && \ - echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(. /etc/os-release && echo $UBUNTU_CODENAME) main" | tee /etc/apt/sources.list.d/ros2.list > /dev/null && \ + export ROS_APT_SOURCE_VERSION=$(curl -s https://api.github.com/repos/ros-infrastructure/ros-apt-source/releases/latest | grep -F "tag_name" | awk -F\" '{print $4}') && \ + curl -L -o /tmp/ros2-apt-source.deb "https://github.com/ros-infrastructure/ros-apt-source/releases/download/${ROS_APT_SOURCE_VERSION}/ros2-apt-source_${ROS_APT_SOURCE_VERSION}.$(. /etc/os-release && echo ${UBUNTU_CODENAME:-${VERSION_CODENAME}})_all.deb" && \ + apt install /tmp/ros2-apt-source.deb && \ apt-get clean && rm -rf /var/lib/apt/lists/ # Install build dependencies diff --git a/.devcontainer/rolling/devcontainer.json b/.devcontainer/rolling/devcontainer.json index 7684b5f4..1f59ad49 100644 --- a/.devcontainer/rolling/devcontainer.json +++ b/.devcontainer/rolling/devcontainer.json @@ -14,5 +14,17 @@ "terminal.integrated.defaultProfile.linux": "bash" }, "remoteUser": "vscode", - "runArgs": ["--privileged", "--network=host"] + "hostRequirements": { + "gpu": "optional" + }, + "runArgs": ["--privileged", + "--network=host", + "--gpus=all", + //"--runtime=nvidia", // uncommment this if rtabmap doesn't show up in nvidia-smi on the host computer + "--env=DISPLAY", + "--env=QT_X11_NO_MITSHM=1", + "--volume=/tmp/.X11-unix:/tmp/.X11-unix"], + "containerEnv": { + "NVIDIA_VISIBLE_DEVICES": "all" + } } diff --git a/.github/workflows/cmake-ros.yml b/.github/workflows/cmake-ros.yml index 9274cc06..5c6d704f 100644 --- a/.github/workflows/cmake-ros.yml +++ b/.github/workflows/cmake-ros.yml @@ -46,7 +46,7 @@ jobs: sudo add-apt-repository universe sudo apt update && sudo apt install curl -y export ROS_APT_SOURCE_VERSION=$(curl -s https://api.github.com/repos/ros-infrastructure/ros-apt-source/releases/latest | grep -F "tag_name" | awk -F\" '{print $4}') - curl -L -o /tmp/ros2-apt-source.deb "https://github.com/ros-infrastructure/ros-apt-source/releases/download/${ROS_APT_SOURCE_VERSION}/ros2-apt-source_${ROS_APT_SOURCE_VERSION}.$(. /etc/os-release && echo $VERSION_CODENAME)_all.deb" + curl -L -o /tmp/ros2-apt-source.deb "https://github.com/ros-infrastructure/ros-apt-source/releases/download/${ROS_APT_SOURCE_VERSION}/ros2-apt-source_${ROS_APT_SOURCE_VERSION}.$(. /etc/os-release && echo ${UBUNTU_CODENAME:-${VERSION_CODENAME}})_all.deb" sudo apt install /tmp/ros2-apt-source.deb sudo apt update @@ -56,6 +56,11 @@ jobs: - uses: actions/checkout@v4 + - name: Point rosdep to latest Rolling on Noble until github runners support ubuntu 26.04 + if: matrix.ros_distribution == 'rolling' + run: | + echo "ROSDISTRO_INDEX_URL=https://raw.githubusercontent.com/ros/rosdistro/6527fa694360d609c1491528ed50437bd853af5b/index-v4.yaml" >> $GITHUB_ENV + - name: Install dependencies run: | source /opt/ros/${{ matrix.ros_distribution }}/setup.bash From 72833c89bb7c0cd042c5ffbc99356f5cf8ef8b28 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 10 May 2026 20:04:30 -0700 Subject: [PATCH 110/126] Adding macos CI (#1699) * Added macos-15-intel and macos-15 build * Cache brew pkgs * added tahoe build, adding debug logs * g2o csparse optional * removed ls * build package on * disabling bundle for now * updated cache name --- .github/workflows/cmake-macos.yml | 91 +++++++++++++++++++++++++++++++ cmake_modules/FindG2O.cmake | 10 ++-- 2 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/cmake-macos.yml diff --git a/.github/workflows/cmake-macos.yml b/.github/workflows/cmake-macos.yml new file mode 100644 index 00000000..866b3faa --- /dev/null +++ b/.github/workflows/cmake-macos.yml @@ -0,0 +1,91 @@ +name: CMake-MacOS + +on: + push: + branches: + - master + pull_request: + branches: + - '**' + +env: + BUILD_TYPE: Release + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + build: + name: ${{ matrix.build_name }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + build_name: [macos-sequoia-intel, macos-sequoia-apple-silicon, macos-tahoe-intel, macos-tahoe-apple-silicon] + include: + - build_name: macos-sequoia-intel + os: macos-15-intel + extra_deps: "" + extra_cmake_def: '-DBUILD_AS_BUNDLE=ON' + - build_name: macos-sequoia-apple-silicon + os: macos-15 + extra_deps: "" + extra_cmake_def: '-DBUILD_AS_BUNDLE=ON' + - build_name: macos-tahoe-intel + os: macos-26-intel + extra_deps: "" + extra_cmake_def: '-DBUILD_AS_BUNDLE=ON' + - build_name: macos-tahoe-apple-silicon + os: macos-26 + extra_deps: "" + extra_cmake_def: '-DBUILD_AS_BUNDLE=ON' + + steps: + - uses: actions/checkout@v4 + + - name: Cache Homebrew + uses: actions/cache@v4 + with: + path: ~/Library/Caches/Homebrew + key: ${{ matrix.os }}-brew-${{ hashFiles('**/Brewfile') }} + restore-keys: | + ${{ matrix.os }}-brew- + + - name: Install Brew Dependencies + run: | + # Update brew and install from Brewfile if present, or specific packages + brew bundle || brew install pcl opencv octomap g2o pdal + + - name: Configure CMake + run: | + cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} ${{ matrix.extra_cmake_def }} + + - name: Build + run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} + + - name: Info + working-directory: ${{github.workspace}}/build/bin + run: | + ./rtabmap-console --version + +# - name: Build MacOS Package +# run: | +# cmake --build ${{ github.workspace }}/build --config ${{ env.BUILD_TYPE }} --target package + +# - name: Upload RTABMap Artifacts (DMG) +# uses: actions/upload-artifact@v4 +# with: +# name: RTABMap-Binaries-${{ matrix.build_name }}-zip +# path: | +# build/RTABMap-*.dmg +# compression-level: 0 +# if-no-files-found: warn +# retention-days: ${{ github.event_name == 'pull_request' && 1 || 90 }} + +# - name: Test +# working-directory: ${{github.workspace}}/build +# # Execute tests defined by the CMake configuration. +# # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail +# run: ctest -C ${{env.BUILD_TYPE}} + diff --git a/cmake_modules/FindG2O.cmake b/cmake_modules/FindG2O.cmake index 3adc2c4d..1790a26c 100644 --- a/cmake_modules/FindG2O.cmake +++ b/cmake_modules/FindG2O.cmake @@ -101,16 +101,16 @@ IF(G2O_STUFF_LIBRARY AND G2O_CORE_LIBRARY AND G2O_INCLUDE_DIR AND G2O_CONFIG_FIL ${G2O_TYPES_SBA} ${G2O_STUFF_LIBRARY}) - IF(CSPARSE_FOUND) + IF(CSPARSE_FOUND AND G2O_SOLVER_CSPARSE AND G2O_SOLVER_CSPARSE_EXTENSION) SET(G2O_INCLUDE_DIRS ${G2O_INCLUDE_DIRS} ${CSPARSE_INCLUDE_DIR}) SET(G2O_LIBRARIES ${G2O_LIBRARIES} - ${G2O_SOLVER_CSPARSE} - ${G2O_SOLVER_CSPARSE_EXTENSION} - ${CSPARSE_LIBRARY}) - ENDIF(CSPARSE_FOUND) + ${G2O_SOLVER_CSPARSE} + ${G2O_SOLVER_CSPARSE_EXTENSION} + ${CSPARSE_LIBRARY}) + ENDIF(CSPARSE_FOUND AND G2O_SOLVER_CSPARSE AND G2O_SOLVER_CSPARSE_EXTENSION) IF(G2O_SOLVER_CHOLMOD) SET(G2O_INCLUDE_DIRS From 440e143d87377a8e5cd37d23cfe255e0f80178b6 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 10 May 2026 20:07:46 -0700 Subject: [PATCH 111/126] Adding CI Macos status in readme (#1700) * Added macos-15-intel and macos-15 build * Cache brew pkgs * added tahoe build, adding debug logs * g2o csparse optional * removed ls * build package on * disabling bundle for now * updated cache name * Added MacOS CI status in README.md --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b13adcf7..427c7322 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,12 @@ This project is supported by [IntRoLab - Intelligent / Interactive / Integrated
LinuxBuild Status
Build Status
Build Status -
WindowsBuild Status + CMake Linux Build Status
CMake Windows Build Status
CMake ROS Build Status
Docker Build Status
Build Status
ROS 2ROS 2 Humble Build Status
Jazzy Build Status
KiltedBuild Status
Rolling Build Status
- From afe8aadff1e5b80e2032da9313b5f6ff208d3ec7 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 10 May 2026 21:46:37 -0700 Subject: [PATCH 112/126] Remove macos ci cache (#1701) * removed macos caching --- .github/workflows/cmake-macos.yml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/cmake-macos.yml b/.github/workflows/cmake-macos.yml index 866b3faa..a8d37a3e 100644 --- a/.github/workflows/cmake-macos.yml +++ b/.github/workflows/cmake-macos.yml @@ -44,18 +44,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Cache Homebrew - uses: actions/cache@v4 - with: - path: ~/Library/Caches/Homebrew - key: ${{ matrix.os }}-brew-${{ hashFiles('**/Brewfile') }} - restore-keys: | - ${{ matrix.os }}-brew- - - name: Install Brew Dependencies run: | # Update brew and install from Brewfile if present, or specific packages - brew bundle || brew install pcl opencv octomap g2o pdal + brew install pcl opencv octomap g2o pdal - name: Configure CMake run: | From 8d0692eab0034783a47bf4f994955ba56ac7e151 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Thu, 14 May 2026 09:20:09 -0700 Subject: [PATCH 113/126] Save intermediate node input features (#1698) * Avoid removing nodes from STM when using CreateIntermediateNodes * Clean up the data of the intermediate node when IntermediateNodeDataKept is false * Save intermediate node input features * Don't delete intermediate nodes * comment typo * Updated parameter description, bump patch for DBReader API chang * When ID is not udpated to new one, don't disable words because that signature is still used in the bayes filter * updated comment * rtabmap-reprocess: support Rtabmap/DetectionRate when intermediate nodes are created and -skip option is not used * Fixed smallMovement when rehearsal is not trigdered at the same time * GUI: visualize rehearsal darkblue when intermediate nodes are used * Fixed RtabmapThread not transfering input features on intermediate nodes * Do not override smallMovement and fastMovement when converting to intermediate nodes to keep GUI visualization color working --------- Co-authored-by: Borong Yuan --- CMakeLists.txt | 2 +- corelib/include/rtabmap/core/DBReader.h | 3 + corelib/include/rtabmap/core/Memory.h | 1 + corelib/include/rtabmap/core/Parameters.h | 2 +- corelib/include/rtabmap/core/SensorData.h | 12 +- corelib/src/DBReader.cpp | 6 +- corelib/src/Memory.cpp | 635 ++++++++++++---------- corelib/src/Rtabmap.cpp | 70 ++- corelib/src/RtabmapThread.cpp | 1 - corelib/src/SensorData.cpp | 20 +- guilib/src/MainWindow.cpp | 15 +- guilib/src/PreferencesDialog.cpp | 7 +- guilib/src/ui/preferencesDialog.ui | 384 ++++++------- tools/Reprocess/main.cpp | 51 +- 14 files changed, 704 insertions(+), 505 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f80448f5..7844ae1c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,7 +22,7 @@ SET(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake_modules") ####################### SET(RTABMAP_MAJOR_VERSION 0) SET(RTABMAP_MINOR_VERSION 23) -SET(RTABMAP_PATCH_VERSION 5) +SET(RTABMAP_PATCH_VERSION 6) SET(RTABMAP_VERSION ${RTABMAP_MAJOR_VERSION}.${RTABMAP_MINOR_VERSION}.${RTABMAP_PATCH_VERSION}) diff --git a/corelib/include/rtabmap/core/DBReader.h b/corelib/include/rtabmap/core/DBReader.h index 7bbeaa8a..c31da3f4 100644 --- a/corelib/include/rtabmap/core/DBReader.h +++ b/corelib/include/rtabmap/core/DBReader.h @@ -60,6 +60,7 @@ public: int stopMapId = -1, bool priorsIgnored = false, bool imuIgnored = false, + bool intermediateNodesAreNormalNodes = false, const std::vector & cameraLocalTransformOverrides = std::vector()); DBReader(const std::list & databasePaths, float frameRate = 0.0f, // -1 = use Database stamps, 0 = inf @@ -76,6 +77,7 @@ public: int stopMapId = -1, bool priorsIgnored = false, bool imuIgnored = false, + bool intermediateNodesAreNormalNodes = false, const std::vector & cameraLocalTransformOverrides = std::vector()); virtual ~DBReader(); @@ -106,6 +108,7 @@ private: int _stopId; std::vector _cameraIndices; bool _intermediateNodesIgnored; + bool _intermediateNodesAreNormalNodes; bool _landmarksIgnored; bool _featuresIgnored; bool _priorsIgnored; diff --git a/corelib/include/rtabmap/core/Memory.h b/corelib/include/rtabmap/core/Memory.h index f5bc8514..52979c14 100644 --- a/corelib/include/rtabmap/core/Memory.h +++ b/corelib/include/rtabmap/core/Memory.h @@ -140,6 +140,7 @@ public: float radius, const std::map & optimizedPoses, int maxGraphDepth) const; + void convertToIntermediate(int locationId); void deleteLocation(int locationId, std::list * deletedWords = 0); void saveLocationData(int locationId); void removeLink(int idA, int idB); diff --git a/corelib/include/rtabmap/core/Parameters.h b/corelib/include/rtabmap/core/Parameters.h index 5d3e315d..23cace9e 100644 --- a/corelib/include/rtabmap/core/Parameters.h +++ b/corelib/include/rtabmap/core/Parameters.h @@ -218,7 +218,7 @@ class RTABMAP_CORE_EXPORT Parameters RTABMAP_PARAM(Mem, ReduceGraph, bool, false, uFormat("Reduce graph. Merge nodes when loop closures are added (ignoring those with user data). Note that this approach assumes that 100%% of the loop closures accepted are good, so it is highly recommended to enable \"%s\" at the same time.", kRGBDOptimizeMaxError().c_str())); RTABMAP_PARAM(Mem, RecentWmRatio, float, 0.2, "Ratio of locations after the last loop closure in WM that cannot be transferred."); RTABMAP_PARAM(Mem, TransferSortingByWeightId, bool, false, "On transfer, signatures are sorted by weight->ID only (i.e. the oldest of the lowest weighted signatures are transferred first). If false, the signatures are sorted by weight->Age->ID (i.e. the oldest inserted in WM of the lowest weighted signatures are transferred first). Note that retrieval updates the age, not the ID."); - RTABMAP_PARAM(Mem, RehearsalIdUpdatedToNewOne, bool, false, "On merge, update to new id. When false, no copy."); + RTABMAP_PARAM(Mem, RehearsalIdUpdatedToNewOne, bool, false, uFormat("On merge, update to new id. When false, no copy. Keep this disable if %s=true.", kRtabmapCreateIntermediateNodes().c_str())); RTABMAP_PARAM(Mem, RehearsalWeightIgnoredWhileMoving, bool, false, "When the robot is moving, weights are not updated on rehearsal."); RTABMAP_PARAM(Mem, GenerateIds, bool, true, "True=Generate location IDs, False=use input image IDs."); RTABMAP_PARAM(Mem, BadSignaturesIgnored, bool, false, "Bad signatures are ignored."); diff --git a/corelib/include/rtabmap/core/SensorData.h b/corelib/include/rtabmap/core/SensorData.h index 18fc5d36..ea5a5dee 100644 --- a/corelib/include/rtabmap/core/SensorData.h +++ b/corelib/include/rtabmap/core/SensorData.h @@ -212,6 +212,12 @@ public: _userDataCompressed.empty() && _keypoints.size() == 0 && _descriptors.empty() && + _groundCellsRaw.empty() && + _groundCellsCompressed.empty() && + _obstacleCellsRaw.empty() && + _obstacleCellsCompressed.empty() && + _emptyCellsRaw.empty() && + _emptyCellsCompressed.empty() && imu_.empty()); } @@ -309,8 +315,6 @@ public: const cv::Mat & empty, float cellSize, const cv::Point3f & viewPoint); - // remove raw occupancy grids - void clearOccupancyGridRaw() {_groundCellsRaw = cv::Mat(); _obstacleCellsRaw = cv::Mat();} const cv::Mat & gridGroundCellsRaw() const {return _groundCellsRaw;} const cv::Mat & gridGroundCellsCompressed() const {return _groundCellsCompressed;} const cv::Mat & gridObstacleCellsRaw() const {return _obstacleCellsRaw;} @@ -355,12 +359,12 @@ public: * Clear compressed rgb/depth (left/right) images, compressed laser scan and compressed user data. * Raw data are kept is set. */ - void clearCompressedData(bool images = true, bool scan = true, bool userData = true); + void clearCompressedData(bool images = true, bool scan = true, bool userData = true, bool occupancyGrid = true); /** * Clear raw rgb/depth (left/right) images, raw laser scan and raw user data. * Compressed data are kept is set. */ - void clearRawData(bool images = true, bool scan = true, bool userData = true); + void clearRawData(bool images = true, bool scan = true, bool userData = true, bool occupancyGrid = true); bool isPointVisibleFromCameras(const cv::Point3f & pt) const; // assuming point is in robot frame diff --git a/corelib/src/DBReader.cpp b/corelib/src/DBReader.cpp index c8b4f85e..5f5c0540 100644 --- a/corelib/src/DBReader.cpp +++ b/corelib/src/DBReader.cpp @@ -57,6 +57,7 @@ DBReader::DBReader(const std::string & databasePath, int stopMapId, bool priorsIgnored, bool imuIgnored, + bool intermediateNodesAreNormalNodes, const std::vector & cameraLocalTransformOverrides) : Camera(frameRate), _paths(uSplit(databasePath, ';')), @@ -67,6 +68,7 @@ DBReader::DBReader(const std::string & databasePath, _stopId(stopId), _cameraIndices(cameraIndices), _intermediateNodesIgnored(intermediateNodesIgnored), + _intermediateNodesAreNormalNodes(intermediateNodesAreNormalNodes), _landmarksIgnored(landmarksIgnored), _featuresIgnored(featuresIgnored), _priorsIgnored(priorsIgnored), @@ -99,6 +101,7 @@ DBReader::DBReader(const std::list & databasePaths, int stopMapId, bool priorsIgnored, bool imuIgnored, + bool intermediateNodesAreNormalNodes, const std::vector & cameraLocalTransformOverrides) : Camera(frameRate), _paths(databasePaths), @@ -109,6 +112,7 @@ DBReader::DBReader(const std::list & databasePaths, _stopId(stopId), _cameraIndices(cameraIndices), _intermediateNodesIgnored(intermediateNodesIgnored), + _intermediateNodesAreNormalNodes(intermediateNodesAreNormalNodes), _landmarksIgnored(landmarksIgnored), _featuresIgnored(featuresIgnored), _priorsIgnored(priorsIgnored), @@ -749,7 +753,7 @@ SensorData DBReader::getNextData(SensorCaptureInfo * info) data.setStereoCameraModels(combinedStereoModels); } } - data.setId(s->getWeight()==-1 ? -1 : seq); + data.setId(!_intermediateNodesAreNormalNodes && s->getWeight()==-1 ? -1 : seq); data.setStamp(s->getStamp()); data.setGroundTruth(s->getGroundTruthPose()); if(!globalPose.isNull()) diff --git a/corelib/src/Memory.cpp b/corelib/src/Memory.cpp index 3d880805..d3d8cdd0 100644 --- a/corelib/src/Memory.cpp +++ b/corelib/src/Memory.cpp @@ -2413,7 +2413,9 @@ int Memory::cleanup() int signatureRemoved = 0; // bad signature - if(_lastSignature && ((_lastSignature->isBadSignature() && _badSignaturesIgnored) || !_incrementalMemory)) + if(_lastSignature && + ((_lastSignature->isBadSignature() && _badSignaturesIgnored && _lastSignature->getWeight()!=-1) || + !_incrementalMemory)) { if(_lastSignature->isBadSignature()) { @@ -2744,7 +2746,11 @@ void Memory::moveToTrash(Signature * s, bool keepLinkedToGraph, std::list * } // it is a bad signature (not saved), remove links! - if(keepLinkedToGraph && (!s->isSaved() && s->isBadSignature() && _badSignaturesIgnored)) + if(keepLinkedToGraph && + !s->isSaved() && + s->isBadSignature() && + _badSignaturesIgnored && + s->getWeight()!=-1) { keepLinkedToGraph = false; } @@ -3042,6 +3048,29 @@ bool Memory::setUserData(int id, const cv::Mat & data) return false; } +void Memory::convertToIntermediate(int locationId) +{ + UDEBUG("Converting location %d to intermediate node", locationId); + Signature * location = _getSignature(locationId); + if(location) + { + location->setWeight(-1); + location->sensorData().setFeatures(std::vector(), std::vector(), cv::Mat()); + this->disableWordsRef(locationId); // won't be used for loop closure detection anymore + if(!_saveIntermediateNodeData) + { + location->removeAllWords(); + location->sensorData().clearGlobalDescriptors(); + } + + location->sensorData().clearRawData(); + if(!_saveIntermediateNodeData || !this->isBinDataKept()) + { + location->sensorData().clearCompressedData(); + } + } +} + void Memory::deleteLocation(int locationId, std::list * deletedWords) { UDEBUG("Deleting location %d", locationId); @@ -4317,18 +4346,46 @@ bool Memory::rehearsalMerge(int oldId, int newId) // just update weight int w = oldS->getWeight()>=0?oldS->getWeight():0; newS->setWeight(w + newS->getWeight() + 1); - oldS->setWeight(intermediateMerge?-1:0); // convert to intermediate node + oldS->setWeight(0); if(_lastGlobalLoopClosureId == oldS->id()) { _lastGlobalLoopClosureId = newS->id(); } + if(intermediateMerge) + { + static bool warned = false; + if(!warned) + { + UWARN("A rehearsal was accepted (%d->%d) while not moving but " + "there are intermediate nodes between them in the graph. " + "Because %s=true, the node %d cannot be converted " + "into an intermediate node so it will be kept in the graph " + "even if we are not moving. Set %s=false to handle intermediate " + "nodes with rehearsal enabled so that loop closure hypotheses " + "are propagated correctly. This message is only " + "printed once.", + oldS->id(), + newS->id(), + Parameters::kMemRehearsalIdUpdatedToNewOne().c_str(), + oldS->id(), + Parameters::kMemRehearsalIdUpdatedToNewOne().c_str()); + warned = true; + } + } } else // !_idUpdatedToNewOneRehearsal { int w = newS->getWeight()>=0?newS->getWeight():0; oldS->setWeight(w + oldS->getWeight() + 1); - newS->setWeight(intermediateMerge?-1:0); // convert to intermediate node + if(intermediateMerge) + { + this->convertToIntermediate(newS->id()); + } + else + { + newS->setWeight(0); + } } } } @@ -5584,7 +5641,7 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor UDEBUG("Intermediate node detected, don't extract features!"); } } - else if(_feature2D->getMaxFeatures() >= 0 && !isIntermediateNode) + else { _receivingOdometryFeatures = true; UINFO("Use odometry features: kpts=%d 3d=%d desc=%d (dim=%d, type=%d)", @@ -5600,133 +5657,75 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor UASSERT(descriptors.empty() || descriptors.rows == (int)keypoints.size()); UASSERT(keypoints3D.empty() || keypoints3D.size() == keypoints.size()); - int maxFeatures = _rawDescriptorsKept&&!pose.isNull()&&_feature2D->getMaxFeatures()>0&&_feature2D->getMaxFeatures()<_visMaxFeatures?_visMaxFeatures:_feature2D->getMaxFeatures(); - bool ssc = _rawDescriptorsKept&&!pose.isNull()&&_feature2D->getMaxFeatures()>0&&_feature2D->getMaxFeatures()<_visMaxFeatures?_visSSC:_feature2D->getSSC(); - if((int)keypoints.size() > maxFeatures) + if(_feature2D->getMaxFeatures() >= 0 && !isIntermediateNode) { - if(data.cameraModels().size()>=1 || data.stereoCameraModels().size()>=1) - _feature2D->limitKeypoints(keypoints, keypoints3D, descriptors, maxFeatures, data.cameraModels().size()?cv::Size(data.cameraModels()[0].imageWidth()*data.cameraModels().size(), data.cameraModels()[0].imageHeight()):cv::Size(data.stereoCameraModels()[0].left().imageWidth()*data.stereoCameraModels().size(), data.stereoCameraModels()[0].left().imageHeight()), ssc); - else - _feature2D->limitKeypoints(keypoints, keypoints3D, descriptors, maxFeatures); - } - t = timer.ticks(); - if(stats) stats->addStatistic(Statistics::kTimingMemKeypoints_detection(), t*1000.0f); - UDEBUG("time keypoints (%d) = %fs", (int)keypoints.size(), t); - - if(descriptors.empty()) - { - cv::Mat imageMono; - if(data.imageRaw().channels() == 3) + int maxFeatures = _rawDescriptorsKept&&!pose.isNull()&&_feature2D->getMaxFeatures()>0&&_feature2D->getMaxFeatures()<_visMaxFeatures?_visMaxFeatures:_feature2D->getMaxFeatures(); + if((int)keypoints.size() > maxFeatures) { - cv::cvtColor(data.imageRaw(), imageMono, CV_BGR2GRAY); - } - else - { - imageMono = data.imageRaw(); - } - - UASSERT_MSG(imagesRectified, "Cannot extract descriptors on not rectified image from keypoints which assumed to be undistorted"); - descriptors = _feature2D->generateDescriptors(imageMono, keypoints); - } - else if(!imagesRectified && !data.cameraModels().empty()) - { - std::vector keypointsValid; - keypointsValid.reserve(keypoints.size()); - cv::Mat descriptorsValid; - descriptorsValid.reserve(descriptors.rows); - std::vector keypoints3DValid; - keypoints3DValid.reserve(keypoints3D.size()); - - //undistort keypoints before projection (RGB-D) - if(data.cameraModels().size() == 1) - { - std::vector pointsIn, pointsOut; - cv::KeyPoint::convert(keypoints,pointsIn); - if(data.cameraModels()[0].D_raw().cols == 6) - { -#if CV_MAJOR_VERSION > 2 or (CV_MAJOR_VERSION == 2 and (CV_MINOR_VERSION >4 or (CV_MINOR_VERSION == 4 and CV_SUBMINOR_VERSION >=10))) - // Equidistant / FishEye - // get only k parameters (k1,k2,p1,p2,k3,k4) - cv::Mat D(1, 4, CV_64FC1); - D.at(0,0) = data.cameraModels()[0].D_raw().at(0,0); - D.at(0,1) = data.cameraModels()[0].D_raw().at(0,1); - D.at(0,2) = data.cameraModels()[0].D_raw().at(0,4); - D.at(0,3) = data.cameraModels()[0].D_raw().at(0,5); - cv::fisheye::undistortPoints(pointsIn, pointsOut, - data.cameraModels()[0].K_raw(), - D, - data.cameraModels()[0].R(), - data.cameraModels()[0].P()); - } + bool ssc = _rawDescriptorsKept&&!pose.isNull()&&_feature2D->getMaxFeatures()>0&&_feature2D->getMaxFeatures()<_visMaxFeatures?_visSSC:_feature2D->getSSC(); + if(data.cameraModels().size()>=1 || data.stereoCameraModels().size()>=1) + _feature2D->limitKeypoints(keypoints, + keypoints3D, + descriptors, + maxFeatures, + data.cameraModels().size()?cv::Size(data.cameraModels()[0].imageWidth()*data.cameraModels().size(), + data.cameraModels()[0].imageHeight()):cv::Size(data.stereoCameraModels()[0].left().imageWidth()*data.stereoCameraModels().size(), + data.stereoCameraModels()[0].left().imageHeight()), + ssc); else -#else - UWARN("Too old opencv version (%d,%d,%d) to support fisheye model (min 2.4.10 required)!", - CV_MAJOR_VERSION, CV_MINOR_VERSION, CV_SUBMINOR_VERSION); - } -#endif - { - //RadialTangential - cv::undistortPoints(pointsIn, pointsOut, - data.cameraModels()[0].K_raw(), - data.cameraModels()[0].D_raw(), - data.cameraModels()[0].R(), - data.cameraModels()[0].P()); - } - UASSERT(pointsOut.size() == keypoints.size()); - for(unsigned int i=0; i=0 && pointsOut.at(i).x=0 && pointsOut.at(i).ylimitKeypoints(keypoints, + keypoints3D, + descriptors, + maxFeatures); } - else + t = timer.ticks(); + if(stats) stats->addStatistic(Statistics::kTimingMemKeypoints_detection(), t*1000.0f); + UDEBUG("time keypoints (%d) = %fs", (int)keypoints.size(), t); + + if(descriptors.empty()) { - float subImageWidth; - if(!data.imageRaw().empty()) + cv::Mat imageMono; + if(data.imageRaw().channels() == 3) { - UASSERT(int((data.imageRaw().cols/data.cameraModels().size())*data.cameraModels().size()) == data.imageRaw().cols); - subImageWidth = data.imageRaw().cols/data.cameraModels().size(); + cv::cvtColor(data.imageRaw(), imageMono, CV_BGR2GRAY); } else { - UASSERT(data.cameraModels()[0].imageWidth()>0); - subImageWidth = data.cameraModels()[0].imageWidth(); + imageMono = data.imageRaw(); } - for(unsigned int i=0; i= 0 && cameraIndex < (int)data.cameraModels().size(), - uFormat("cameraIndex=%d, models=%d, kpt.x=%f, subImageWidth=%f (Camera model image width=%d)", - cameraIndex, (int)data.cameraModels().size(), keypoints[i].pt.x, subImageWidth, data.cameraModels()[0].imageWidth()).c_str()); + UASSERT_MSG(imagesRectified, "Cannot extract descriptors on not rectified image from keypoints which assumed to be undistorted"); + descriptors = _feature2D->generateDescriptors(imageMono, keypoints); + } + else if(!imagesRectified && !data.cameraModels().empty()) + { + std::vector keypointsValid; + keypointsValid.reserve(keypoints.size()); + cv::Mat descriptorsValid; + descriptorsValid.reserve(descriptors.rows); + std::vector keypoints3DValid; + keypoints3DValid.reserve(keypoints3D.size()); + //undistort keypoints before projection (RGB-D) + if(data.cameraModels().size() == 1) + { std::vector pointsIn, pointsOut; - pointsIn.push_back(cv::Point2f(keypoints.at(i).pt.x-subImageWidth*cameraIndex, keypoints.at(i).pt.y)); - if(data.cameraModels()[cameraIndex].D_raw().cols == 6) + cv::KeyPoint::convert(keypoints,pointsIn); + if(data.cameraModels()[0].D_raw().cols == 6) { #if CV_MAJOR_VERSION > 2 or (CV_MAJOR_VERSION == 2 and (CV_MINOR_VERSION >4 or (CV_MINOR_VERSION == 4 and CV_SUBMINOR_VERSION >=10))) // Equidistant / FishEye // get only k parameters (k1,k2,p1,p2,k3,k4) cv::Mat D(1, 4, CV_64FC1); - D.at(0,0) = data.cameraModels()[cameraIndex].D_raw().at(0,0); - D.at(0,1) = data.cameraModels()[cameraIndex].D_raw().at(0,1); - D.at(0,2) = data.cameraModels()[cameraIndex].D_raw().at(0,4); - D.at(0,3) = data.cameraModels()[cameraIndex].D_raw().at(0,5); + D.at(0,0) = data.cameraModels()[0].D_raw().at(0,0); + D.at(0,1) = data.cameraModels()[0].D_raw().at(0,1); + D.at(0,2) = data.cameraModels()[0].D_raw().at(0,4); + D.at(0,3) = data.cameraModels()[0].D_raw().at(0,5); cv::fisheye::undistortPoints(pointsIn, pointsOut, - data.cameraModels()[cameraIndex].K_raw(), + data.cameraModels()[0].K_raw(), D, - data.cameraModels()[cameraIndex].R(), - data.cameraModels()[cameraIndex].P()); + data.cameraModels()[0].R(), + data.cameraModels()[0].P()); } else #else @@ -5737,57 +5736,128 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor { //RadialTangential cv::undistortPoints(pointsIn, pointsOut, - data.cameraModels()[cameraIndex].K_raw(), - data.cameraModels()[cameraIndex].D_raw(), - data.cameraModels()[cameraIndex].R(), - data.cameraModels()[cameraIndex].P()); + data.cameraModels()[0].K_raw(), + data.cameraModels()[0].D_raw(), + data.cameraModels()[0].R(), + data.cameraModels()[0].P()); } - - if(pointsOut[0].x>=0 && pointsOut[0].x=0 && pointsOut[0].y=0 && pointsOut.at(i).x=0 && pointsOut.at(i).y0); + subImageWidth = data.cameraModels()[0].imageWidth(); + } + + for(unsigned int i=0; i= 0 && cameraIndex < (int)data.cameraModels().size(), + uFormat("cameraIndex=%d, models=%d, kpt.x=%f, subImageWidth=%f (Camera model image width=%d)", + cameraIndex, (int)data.cameraModels().size(), keypoints[i].pt.x, subImageWidth, data.cameraModels()[0].imageWidth()).c_str()); + + std::vector pointsIn, pointsOut; + pointsIn.push_back(cv::Point2f(keypoints.at(i).pt.x-subImageWidth*cameraIndex, keypoints.at(i).pt.y)); + if(data.cameraModels()[cameraIndex].D_raw().cols == 6) + { +#if CV_MAJOR_VERSION > 2 or (CV_MAJOR_VERSION == 2 and (CV_MINOR_VERSION >4 or (CV_MINOR_VERSION == 4 and CV_SUBMINOR_VERSION >=10))) + // Equidistant / FishEye + // get only k parameters (k1,k2,p1,p2,k3,k4) + cv::Mat D(1, 4, CV_64FC1); + D.at(0,0) = data.cameraModels()[cameraIndex].D_raw().at(0,0); + D.at(0,1) = data.cameraModels()[cameraIndex].D_raw().at(0,1); + D.at(0,2) = data.cameraModels()[cameraIndex].D_raw().at(0,4); + D.at(0,3) = data.cameraModels()[cameraIndex].D_raw().at(0,5); + cv::fisheye::undistortPoints(pointsIn, pointsOut, + data.cameraModels()[cameraIndex].K_raw(), + D, + data.cameraModels()[cameraIndex].R(), + data.cameraModels()[cameraIndex].P()); + } + else +#else + UWARN("Too old opencv version (%d,%d,%d) to support fisheye model (min 2.4.10 required)!", + CV_MAJOR_VERSION, CV_MINOR_VERSION, CV_SUBMINOR_VERSION); + } +#endif + { + //RadialTangential + cv::undistortPoints(pointsIn, pointsOut, + data.cameraModels()[cameraIndex].K_raw(), + data.cameraModels()[cameraIndex].D_raw(), + data.cameraModels()[cameraIndex].R(), + data.cameraModels()[cameraIndex].P()); + } + + if(pointsOut[0].x>=0 && pointsOut[0].x=0 && pointsOut[0].yaddStatistic(Statistics::kTimingMemRectification(), t*1000.0f); + UDEBUG("time rectification = %fs", t); } - - keypoints = keypointsValid; - descriptors = descriptorsValid; - keypoints3D = keypoints3DValid; - t = timer.ticks(); - if(stats) stats->addStatistic(Statistics::kTimingMemRectification(), t*1000.0f); - UDEBUG("time rectification = %fs", t); - } - t = timer.ticks(); - if(stats) stats->addStatistic(Statistics::kTimingMemDescriptors_extraction(), t*1000.0f); - UDEBUG("time descriptors (%d) = %fs", descriptors.rows, t); + if(stats) stats->addStatistic(Statistics::kTimingMemDescriptors_extraction(), t*1000.0f); + UDEBUG("time descriptors (%d) = %fs", descriptors.rows, t); - if(keypoints3D.empty() && - ((!data.depthRaw().empty() && data.cameraModels().size() && data.cameraModels()[0].isValidForProjection()) || - (!data.rightRaw().empty() && data.stereoCameraModels().size() && data.stereoCameraModels()[0].isValidForProjection()))) - { - keypoints3D = _feature2D->generateKeypoints3D(data, keypoints); - } - if(_feature2D->getMinDepth() > 0.0f || _feature2D->getMaxDepth() > 0.0f) - { - _feature2D->filterKeypointsByDepth(keypoints, descriptors, keypoints3D, _feature2D->getMinDepth(), _feature2D->getMaxDepth()); - } - t = timer.ticks(); - if(stats) stats->addStatistic(Statistics::kTimingMemKeypoints_3D(), t*1000.0f); - UDEBUG("time keypoints 3D (%d) = %fs", (int)keypoints3D.size(), t); + if(keypoints3D.empty() && + ((!data.depthRaw().empty() && data.cameraModels().size() && data.cameraModels()[0].isValidForProjection()) || + (!data.rightRaw().empty() && data.stereoCameraModels().size() && data.stereoCameraModels()[0].isValidForProjection()))) + { + keypoints3D = _feature2D->generateKeypoints3D(data, keypoints); + } + if(_feature2D->getMinDepth() > 0.0f || _feature2D->getMaxDepth() > 0.0f) + { + _feature2D->filterKeypointsByDepth(keypoints, descriptors, keypoints3D, _feature2D->getMinDepth(), _feature2D->getMaxDepth()); + } + t = timer.ticks(); + if(stats) stats->addStatistic(Statistics::kTimingMemKeypoints_3D(), t*1000.0f); + UDEBUG("time keypoints 3D (%d) = %fs", (int)keypoints3D.size(), t); - UDEBUG("ratio=%f, meanWordsPerLocation=%d", _badSignRatio, meanWordsPerLocation); - if(descriptors.rows && descriptors.rows < _badSignRatio * float(meanWordsPerLocation)) - { - descriptors = cv::Mat(); + UDEBUG("ratio=%f, meanWordsPerLocation=%d", _badSignRatio, meanWordsPerLocation); + if(descriptors.rows && descriptors.rows < _badSignRatio * float(meanWordsPerLocation)) + { + descriptors = cv::Mat(); + } } } @@ -5810,107 +5880,117 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor } std::list wordIds; - if(descriptors.rows) + bool addedToDictionary = false; + if(!keypoints.empty()) { - // In case the number of features we want to do quantization is lower - // than extracted ones (that would be used for transform estimation) - std::vector inliers; - cv::Mat descriptorsForQuantization = descriptors; - std::vector quantizedToRawIndices; - if(_feature2D->getMaxFeatures()>0 && descriptors.rows > _feature2D->getMaxFeatures()) + if(descriptors.rows && !isIntermediateNode) { - UASSERT((int)keypoints.size() == descriptors.rows); - int inliersCount = 0; - if((_feature2D->getGridRows() > 1 || _feature2D->getGridCols() > 1) && - (decimatedData.cameraModels().size()==1 || decimatedData.stereoCameraModels().size()==1 || - data.cameraModels().size()==1 || data.stereoCameraModels().size()==1)) + // In case the number of features we want to do quantization is lower + // than extracted ones (that would be used for transform estimation) + std::vector inliers; + cv::Mat descriptorsForQuantization = descriptors; + std::vector quantizedToRawIndices; + if(_feature2D->getMaxFeatures()>0 && descriptors.rows > _feature2D->getMaxFeatures()) { - Feature2D::limitKeypoints(keypoints, inliers, _feature2D->getMaxFeatures(), - decimatedData.cameraModels().size()?decimatedData.cameraModels()[0].imageSize(): - decimatedData.stereoCameraModels().size()?decimatedData.stereoCameraModels()[0].left().imageSize(): - data.cameraModels().size()?data.cameraModels()[0].imageSize():data.stereoCameraModels()[0].left().imageSize(), - _feature2D->getGridRows(), _feature2D->getGridCols(), _feature2D->getSSC()); - } - else - { - if(_feature2D->getGridRows() > 1 || _feature2D->getGridCols() > 1) + UASSERT((int)keypoints.size() == descriptors.rows); + int inliersCount = 0; + if((_feature2D->getGridRows() > 1 || _feature2D->getGridCols() > 1) && + (decimatedData.cameraModels().size()==1 || decimatedData.stereoCameraModels().size()==1 || + data.cameraModels().size()==1 || data.stereoCameraModels().size()==1)) { - UWARN("Ignored %s and %s parameters as they cannot be used for multi-cameras setup or uncalibrated camera.", - Parameters::kKpGridCols().c_str(), Parameters::kKpGridRows().c_str()); - } - if(decimatedData.cameraModels().size()>=1 || decimatedData.stereoCameraModels().size()>=1 || - data.cameraModels().size()>=1 || data.stereoCameraModels().size()>=1) - { - Feature2D::limitKeypoints( - keypoints, - inliers, - _feature2D->getMaxFeatures(), - decimatedData.cameraModels().size()?cv::Size(decimatedData.cameraModels()[0].imageWidth()*decimatedData.cameraModels().size(), decimatedData.cameraModels()[0].imageHeight()): - decimatedData.stereoCameraModels().size()?cv::Size(decimatedData.stereoCameraModels()[0].left().imageWidth()*decimatedData.stereoCameraModels().size(), decimatedData.stereoCameraModels()[0].left().imageWidth()): - data.cameraModels().size()?cv::Size(data.cameraModels()[0].imageWidth()*data.cameraModels().size(), data.cameraModels()[0].imageHeight()): - cv::Size(data.stereoCameraModels()[0].left().imageWidth()*data.stereoCameraModels().size(), data.stereoCameraModels()[0].left().imageHeight()), - _feature2D->getSSC()); + Feature2D::limitKeypoints(keypoints, inliers, _feature2D->getMaxFeatures(), + decimatedData.cameraModels().size()?decimatedData.cameraModels()[0].imageSize(): + decimatedData.stereoCameraModels().size()?decimatedData.stereoCameraModels()[0].left().imageSize(): + data.cameraModels().size()?data.cameraModels()[0].imageSize():data.stereoCameraModels()[0].left().imageSize(), + _feature2D->getGridRows(), _feature2D->getGridCols(), _feature2D->getSSC()); } else { - Feature2D::limitKeypoints(keypoints, inliers, _feature2D->getMaxFeatures()); - } - } - for(size_t i=0; igetGridRows() > 1 || _feature2D->getGridCols() > 1) { - memcpy(descriptorsForQuantization.ptr(oi), descriptors.ptr(k), descriptors.cols*sizeof(float)); + UWARN("Ignored %s and %s parameters as they cannot be used for multi-cameras setup or uncalibrated camera.", + Parameters::kKpGridCols().c_str(), Parameters::kKpGridRows().c_str()); + } + if(decimatedData.cameraModels().size()>=1 || decimatedData.stereoCameraModels().size()>=1 || + data.cameraModels().size()>=1 || data.stereoCameraModels().size()>=1) + { + Feature2D::limitKeypoints( + keypoints, + inliers, + _feature2D->getMaxFeatures(), + decimatedData.cameraModels().size()?cv::Size(decimatedData.cameraModels()[0].imageWidth()*decimatedData.cameraModels().size(), decimatedData.cameraModels()[0].imageHeight()): + decimatedData.stereoCameraModels().size()?cv::Size(decimatedData.stereoCameraModels()[0].left().imageWidth()*decimatedData.stereoCameraModels().size(), decimatedData.stereoCameraModels()[0].left().imageWidth()): + data.cameraModels().size()?cv::Size(data.cameraModels()[0].imageWidth()*data.cameraModels().size(), data.cameraModels()[0].imageHeight()): + cv::Size(data.stereoCameraModels()[0].left().imageWidth()*data.stereoCameraModels().size(), data.stereoCameraModels()[0].left().imageHeight()), + _feature2D->getSSC()); } else { - memcpy(descriptorsForQuantization.ptr(oi), descriptors.ptr(k), descriptors.cols*sizeof(char)); + Feature2D::limitKeypoints(keypoints, inliers, _feature2D->getMaxFeatures()); } - quantizedToRawIndices[oi] = k; - ++oi; } - } - UASSERT_MSG((int)oi == inliersCount, - uFormat("oi=%d inliersCount=%d (maxFeatures=%d, grid=%dx%d)", - oi, inliersCount, _feature2D->getMaxFeatures(), _feature2D->getGridCols(), _feature2D->getGridRows()).c_str()); - } - - // Quantization to vocabulary - wordIds = _vwd->addNewWords(descriptorsForQuantization, id); - - // Set ID -1 to features not used for quantization - if(wordIds.size() < keypoints.size()) - { - std::vector allWordIds; - allWordIds.resize(keypoints.size(),-1); - int i=0; - for(std::list::iterator iter=wordIds.begin(); iter!=wordIds.end(); ++iter) - { - allWordIds[quantizedToRawIndices[i]] = *iter; - ++i; - } - int negIndex = -1; - for(i=0; i<(int)allWordIds.size(); ++i) - { - if(allWordIds[i] < 0) + for(size_t i=0; i(oi), descriptors.ptr(k), descriptors.cols*sizeof(float)); + } + else + { + memcpy(descriptorsForQuantization.ptr(oi), descriptors.ptr(k), descriptors.cols*sizeof(char)); + } + quantizedToRawIndices[oi] = k; + ++oi; + } + } + UASSERT_MSG((int)oi == inliersCount, + uFormat("oi=%d inliersCount=%d (maxFeatures=%d, grid=%dx%d)", + oi, inliersCount, _feature2D->getMaxFeatures(), _feature2D->getGridCols(), _feature2D->getGridRows()).c_str()); } - wordIds = uVectorToList(allWordIds); + + // Quantization to vocabulary + wordIds = _vwd->addNewWords(descriptorsForQuantization, id); + addedToDictionary = true; + + // Set ID -1 to features not used for quantization + if(wordIds.size() < keypoints.size()) + { + std::vector allWordIds; + allWordIds.resize(keypoints.size(),-1); + int i=0; + for(std::list::iterator iter=wordIds.begin(); iter!=wordIds.end(); ++iter) + { + allWordIds[quantizedToRawIndices[i]] = *iter; + ++i; + } + int negIndex = -1; + for(i=0; i<(int)allWordIds.size(); ++i) + { + if(allWordIds[i] < 0) + { + allWordIds[i] = negIndex--; + } + } + wordIds = uVectorToList(allWordIds); + } + } + else + { + // Set all words as not used in dictionary + wordIds.resize(keypoints.size(),-1); } t = timer.ticks(); @@ -5930,6 +6010,7 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor if(wordIds.size() > 0) { UASSERT(wordIds.size() == keypoints.size()); + UASSERT(descriptors.rows == 0 || descriptors.rows == (int)wordIds.size()); UASSERT(keypoints3D.size() == 0 || keypoints3D.size() == wordIds.size()); unsigned int i=0; float decimationRatio = float(preDecimation) / float(_imagePostDecimation); @@ -5937,7 +6018,7 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor for(std::list::iterator iter=wordIds.begin(); iter!=wordIds.end() && i < keypoints.size(); ++iter, ++i) { cv::KeyPoint kpt = keypoints[i]; - if(preDecimation != _imagePostDecimation) + if(preDecimation != _imagePostDecimation && !isIntermediateNode) { // remap keypoints to final image size kpt.pt.x *= decimationRatio; @@ -5956,7 +6037,7 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor ++words3DValid; } } - if(_rawDescriptorsKept) + if(!descriptors.empty() && _rawDescriptorsKept) { wordsDescriptors.push_back(descriptors.row(i)); } @@ -5964,12 +6045,7 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor } Landmarks landmarks = data.landmarks(); - if(!landmarks.empty() && isIntermediateNode) - { - UDEBUG("Landmarks provided (size=%ld) are ignored because this signature is set as intermediate.", landmarks.size()); - landmarks.clear(); - } - else if(_detectMarkers && !isIntermediateNode && !data.imageRaw().empty()) + if(_detectMarkers && !isIntermediateNode && !data.imageRaw().empty()) { UDEBUG("Detecting markers..."); if(landmarks.empty()) @@ -6101,7 +6177,8 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor UDEBUG("time post-decimation = %fs", t); } - if(_stereoFromMotion && + if(!isIntermediateNode && + _stereoFromMotion && !pose.isNull() && cameraModels.size() == 1 && words.size() && @@ -6514,9 +6591,15 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor compressedUserData)); } - s->setWords(words, wordsKpts, - _reextractLoopClosureFeatures?std::vector():words3D, - _reextractLoopClosureFeatures?cv::Mat():wordsDescriptors); + if(!isIntermediateNode || _saveIntermediateNodeData) + { + s->setWords(words, wordsKpts, + _reextractLoopClosureFeatures?std::vector():words3D, + _reextractLoopClosureFeatures?cv::Mat():wordsDescriptors); + + s->sensorData().setLaserScan(laserScan, false); + s->sensorData().setUserData(data.userDataRaw(), false); + } // set raw data if(!cameraModels.empty()) @@ -6527,8 +6610,6 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor { s->sensorData().setStereoImage(image, depthOrRightImage, stereoCameraModels, false); } - s->sensorData().setLaserScan(laserScan, false); - s->sensorData().setUserData(data.userDataRaw(), false); UDEBUG("data.groundTruth() =%s", data.groundTruth().prettyPrint().c_str()); UDEBUG("data.gps() =%s", data.gps().stamp()?"true":"false"); @@ -6538,28 +6619,33 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor s->sensorData().setGPS(data.gps()); s->sensorData().setEnvSensors(data.envSensors()); - if(!isIntermediateNode) + std::vector globalDescriptors = data.globalDescriptors(); + if(!isIntermediateNode && _globalDescriptorExtractor) { - std::vector globalDescriptors = data.globalDescriptors(); - if(_globalDescriptorExtractor) + GlobalDescriptor gdescriptor = _globalDescriptorExtractor->extract(inputData); + if(!gdescriptor.data().empty()) { - GlobalDescriptor gdescriptor = _globalDescriptorExtractor->extract(inputData); - if(!gdescriptor.data().empty()) - { - globalDescriptors.push_back(gdescriptor); - } + globalDescriptors.push_back(gdescriptor); } - s->sensorData().setGlobalDescriptors(globalDescriptors); } - else if(!data.globalDescriptors().empty()) + if(!globalDescriptors.empty()) { - UDEBUG("Global descriptors provided (size=%ld) are ignored because this signature is set as intermediate.", data.globalDescriptors().size()); + if(!isIntermediateNode || _saveIntermediateNodeData) + { + s->sensorData().setGlobalDescriptors(globalDescriptors); + } + else + { + UDEBUG("Global descriptors provided (size=%ld) are ignored because this signature is set as intermediate and %s=false.", + globalDescriptors.size(), + Parameters::kMemIntermediateNodeDataKept().c_str()); + } } t = timer.ticks(); if(stats) stats->addStatistic(Statistics::kTimingMemCompressing_data(), t*1000.0f); UDEBUG("time compressing data (id=%d) %fs", id, t); - if(words.size()) + if(words.size() && addedToDictionary) { s->setEnabled(true); // All references are already activated in the dictionary at this point (see _vwd->addNewWords()) } @@ -6699,16 +6785,19 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor s->addLandmark(landmark); // Update landmark index - std::map >::iterator nter = _landmarksIndex.find(landmarkId); - if(nter!=_landmarksIndex.end()) + if(!isIntermediateNode) { - nter->second.insert(s->id()); - } - else - { - std::set tmp; - tmp.insert(s->id()); - _landmarksIndex.insert(std::make_pair(landmarkId, tmp)); + std::map >::iterator nter = _landmarksIndex.find(landmarkId); + if(nter!=_landmarksIndex.end()) + { + nter->second.insert(s->id()); + } + else + { + std::set tmp; + tmp.insert(s->id()); + _landmarksIndex.insert(std::make_pair(landmarkId, tmp)); + } } } else diff --git a/corelib/src/Rtabmap.cpp b/corelib/src/Rtabmap.cpp index c9a3dc40..13652171 100644 --- a/corelib/src/Rtabmap.cpp +++ b/corelib/src/Rtabmap.cpp @@ -1518,31 +1518,48 @@ bool Rtabmap::process( } else { + bool linkedToIntermediateNode = false; + Transform t; + if(_memory->isIncremental()) + { + // Check small motion if current node is not an intermediate node already + if(signature->getWeight() >= 0) + { + // It should contain only the query and its first (non-intermediate) neighbor (smaller id) + std::map neighbors = _memory->getNeighborsId(signature->id(), 2, 0, true, true, true, true); + if(neighbors.size() == 2) + { + int nid = neighbors.begin()->first; + const std::multimap & links = signature->getLinks(); + if(links.find(nid) != links.end()) + { + // direct neighbor + t = links.find(nid)->second.transform(); + } + else + { + // Use optimized poses to check how far it is from the latest non-intermediate node + std::map::iterator niter = _optimizedPoses.find(nid); + if(niter != _optimizedPoses.end()) + { + t = niter->second.inverse() * _mapCorrection * signature->getPose(); + } + // not direct link, it means there are intermediate nodes + linkedToIntermediateNode = true; + } + } + } + } + else if(!_odomCachePoses.empty()) + { + t = _odomCachePoses.rbegin()->second.inverse() * signature->getPose(); + } + if(_rgbdLinearUpdate > 0.0f || _rgbdAngularUpdate > 0.0f) { //============================================================ // Minimum displacement required to add to Memory //============================================================ - Transform t; - - if(_memory->isIncremental()) - { - const std::multimap & links = signature->getLinks(); - if(links.size() && links.begin()->second.type() == Link::kNeighbor) - { - const Signature * s = _memory->getSignature(links.begin()->second.to()); - UASSERT(s!=0); - // Check small motion if previous node is not an intermediate node (consecutive intermediate nodes are merged later) - if(s->getWeight() >= 0) - { - t = links.begin()->second.transform(); - } - } - } - else if(!_odomCachePoses.empty()) - { - t = _odomCachePoses.rbegin()->second.inverse() * signature->getPose(); - } if(!t.isNull()) { float x,y,z, roll,pitch,yaw; @@ -1564,7 +1581,7 @@ bool Rtabmap::process( } } } - if(odomVelocity.size() == 6) + if(odomVelocity.size() == 6 && signature->getWeight() != -1) { // This will disable global loop closure detection, only retrieval will be done. // The location will also be deleted at the end. @@ -1572,6 +1589,10 @@ bool Rtabmap::process( (_rgbdLinearSpeedUpdate>0.0f && uMax3(fabs(odomVelocity[0]), fabs(odomVelocity[1]), fabs(odomVelocity[2])) > _rgbdLinearSpeedUpdate) || (_rgbdAngularSpeedUpdate>0.0f && uMax3(fabs(odomVelocity[3]), fabs(odomVelocity[4]), fabs(odomVelocity[5])) > _rgbdAngularSpeedUpdate); } + if(linkedToIntermediateNode && (smallDisplacement || tooFastMovement)) + { + _memory->convertToIntermediate(signature->id()); + } } // Update optimizedPoses with the newly added node @@ -2207,7 +2228,7 @@ bool Rtabmap::process( } } // if(_memory->getWorkingMemSize()) }// !isBadSignature - else if(!signature->isBadSignature() && (smallDisplacement || tooFastMovement)) + else if(!signature->isBadSignature() && signature->getWeight()>=0 && (smallDisplacement || tooFastMovement)) { _highestHypothesis = lastHighestHypothesis; UDEBUG("smallDisplacement=%d tooFastMovement=%d", smallDisplacement?1:0, tooFastMovement?1:0); @@ -3166,7 +3187,7 @@ bool Rtabmap::process( // Landmark //============================================================ std::map > landmarksDetected; // - if(!signature->getLandmarks().empty() && !_graphOptimizer->landmarksIgnored()) + if(!signature->getLandmarks().empty() && !_graphOptimizer->landmarksIgnored() && signature->getWeight()!=-1) { bool hasGlobalLoopClosuresInOdomCache = !graph::filterLinks(_odomCacheConstraints, Link::kGlobalClosure, true).empty() || _loopClosureHypothesis.first != 0; UDEBUG("hasGlobalLoopClosuresInOdomCache=%d", hasGlobalLoopClosuresInOdomCache?1:0); @@ -4446,7 +4467,8 @@ bool Rtabmap::process( signaturesRemoved.push_back(signature->id()); _memory->deleteLocation(signature->id()); } - else if((smallDisplacement || tooFastMovement) && + else if((!_memory->isIncremental() || signature->getWeight()>=0) && + (smallDisplacement || tooFastMovement) && _loopClosureHypothesis.first == 0 && lastProximitySpaceClosureId == 0 && (rejectedLoopClosure || landmarksDetected.empty()) && diff --git a/corelib/src/RtabmapThread.cpp b/corelib/src/RtabmapThread.cpp index 1196b720..44522fab 100644 --- a/corelib/src/RtabmapThread.cpp +++ b/corelib/src/RtabmapThread.cpp @@ -556,7 +556,6 @@ void RtabmapThread::addData(const OdometryEvent & odomEvent) // set negative id so rtabmap will detect it as an intermediate node SensorData tmp = odomEvent.data(); tmp.setId(-1); - tmp.setFeatures(std::vector(), std::vector(), cv::Mat());// remove features _dataBuffer.push_back(OdometryEvent(tmp, odomEvent.pose(), odomInfo)); } else diff --git a/corelib/src/SensorData.cpp b/corelib/src/SensorData.cpp index 807af7ca..8b83795d 100644 --- a/corelib/src/SensorData.cpp +++ b/corelib/src/SensorData.cpp @@ -976,7 +976,7 @@ unsigned long SensorData::getMemoryUsed() const // Return memory usage in Bytes (_descriptors.empty()?0:_descriptors.total()*_descriptors.elemSize()); } -void SensorData::clearCompressedData(bool images, bool scan, bool userData) +void SensorData::clearCompressedData(bool images, bool scan, bool userData, bool occupancyGrid) { if(images) { @@ -992,14 +992,24 @@ void SensorData::clearCompressedData(bool images, bool scan, bool userData) { _userDataCompressed=cv::Mat(); } + if(occupancyGrid) + { + _groundCellsCompressed=cv::Mat(); + _emptyCellsCompressed=cv::Mat(); + _obstacleCellsCompressed=cv::Mat(); + } } -void SensorData::clearRawData(bool images, bool scan, bool userData) +void SensorData::clearRawData(bool images, bool scan, bool userData, bool occupancyGrid) { if(images) { _imageRaw=cv::Mat(); _depthOrRightRaw=cv::Mat(); _depthConfidenceRaw=cv::Mat(); +#ifdef HAVE_OPENCV_CUDEV + _imageRawGpu = cv::cuda::GpuMat(); + _depthOrRightRawGpu = cv::cuda::GpuMat(); +#endif } if(scan) { @@ -1009,6 +1019,12 @@ void SensorData::clearRawData(bool images, bool scan, bool userData) { _userDataRaw=cv::Mat(); } + if(occupancyGrid) + { + _groundCellsRaw=cv::Mat(); + _emptyCellsRaw=cv::Mat(); + _obstacleCellsRaw=cv::Mat(); + } } diff --git a/guilib/src/MainWindow.cpp b/guilib/src/MainWindow.cpp index 989d9aee..92a053f2 100644 --- a/guilib/src/MainWindow.cpp +++ b/guilib/src/MainWindow.cpp @@ -2107,6 +2107,7 @@ void MainWindow::processStats(const rtabmap::Statistics & stat) } // For intermediate empty nodes, keep latest image shown + bool rehearsedSimilarity = (float)uValue(stat.data(), Statistics::kMemoryRehearsal_id(), 0.0f) != 0.0f; if(signature.getWeight() >= 0) { _ui->imageView_source->clear(); @@ -2131,7 +2132,6 @@ void MainWindow::processStats(const rtabmap::Statistics & stat) _ui->label_matchId->clear(); - bool rehearsedSimilarity = (float)uValue(stat.data(), Statistics::kMemoryRehearsal_id(), 0.0f) != 0.0f; int proximityTimeDetections = (int)uValue(stat.data(), Statistics::kProximityTime_detections(), 0.0f); bool scanMatchingSuccess = (bool)uValue(stat.data(), Statistics::kNeighborLinkRefiningAccepted(), 0.0f); _ui->label_stats_imageNumber->setText(QString("%1 [%2]").arg(stat.refImageId()).arg(refMapId)); @@ -2455,6 +2455,18 @@ void MainWindow::processStats(const rtabmap::Statistics & stat) UDEBUG("time= %d ms (update loop closure viewer)", time.restart()); } } + else if(rehearsedSimilarity) + { + _ui->imageView_source->setBackgroundColor(Qt::darkBlue); + } + else if(smallMovement) + { + _ui->imageView_source->setBackgroundColor(Qt::gray); + } + else if(fastMovement) + { + _ui->imageView_source->setBackgroundColor(Qt::magenta); + } // PDF AND LIKELIHOOD if(!stat.posterior().empty() && _ui->dockWidget_posterior->isVisible()) @@ -2712,7 +2724,6 @@ void MainWindow::processStats(const rtabmap::Statistics & stat) Signature & s = *_cachedSignatures.find(stat.refImageId()); _cachedMemoryUsage -= s.sensorData().getMemoryUsed(); s.sensorData().clearRawData(); - s.sensorData().clearOccupancyGridRaw(); _cachedMemoryUsage += s.sensorData().getMemoryUsed(); } diff --git a/guilib/src/PreferencesDialog.cpp b/guilib/src/PreferencesDialog.cpp index b71a39a2..93c55d7c 100644 --- a/guilib/src/PreferencesDialog.cpp +++ b/guilib/src/PreferencesDialog.cpp @@ -761,6 +761,7 @@ PreferencesDialog::PreferencesDialog(QWidget * parent) : connect(_ui->source_checkBox_ignoreFeatures, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->source_checkBox_ignorePriors, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->source_checkBox_ignoreIMU, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); + connect(_ui->source_checkBox_intermediateNodesAreNormalNodes, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->source_spinBox_databaseStartId, SIGNAL(valueChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->source_spinBox_databaseStopId, SIGNAL(valueChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->source_checkBox_useDbStamps, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); @@ -2229,6 +2230,7 @@ void PreferencesDialog::resetSettings(QGroupBox * groupBox) _ui->source_checkBox_ignoreFeatures->setChecked(true); _ui->source_checkBox_ignorePriors->setChecked(false); _ui->source_checkBox_ignoreIMU->setChecked(false); + _ui->source_checkBox_intermediateNodesAreNormalNodes->setChecked(false); _ui->source_spinBox_databaseStartId->setValue(0); _ui->source_spinBox_databaseStopId->setValue(0); _ui->source_lineEdit_databaseCameraIndex->setText(""); @@ -2991,6 +2993,7 @@ void PreferencesDialog::readCameraSettings(const QString & filePath) _ui->source_checkBox_ignoreFeatures->setChecked(settings.value("ignoreFeatures", _ui->source_checkBox_ignoreFeatures->isChecked()).toBool()); _ui->source_checkBox_ignorePriors->setChecked(settings.value("ignorePriors", _ui->source_checkBox_ignorePriors->isChecked()).toBool()); _ui->source_checkBox_ignoreIMU->setChecked(settings.value("ignoreImu", _ui->source_checkBox_ignoreIMU->isChecked()).toBool()); + _ui->source_checkBox_intermediateNodesAreNormalNodes->setChecked(settings.value("intermediateNodesAreNormalNodes", _ui->source_checkBox_intermediateNodesAreNormalNodes->isChecked()).toBool()); _ui->source_spinBox_databaseStartId->setValue(settings.value("startId", _ui->source_spinBox_databaseStartId->value()).toInt()); _ui->source_spinBox_databaseStopId->setValue(settings.value("stopId", _ui->source_spinBox_databaseStopId->value()).toInt()); @@ -3611,6 +3614,7 @@ void PreferencesDialog::writeCameraSettings(const QString & filePath) const settings.setValue("ignoreFeatures", _ui->source_checkBox_ignoreFeatures->isChecked()); settings.setValue("ignorePriors", _ui->source_checkBox_ignorePriors->isChecked()); settings.setValue("ignoreImu", _ui->source_checkBox_ignoreIMU->isChecked()); + settings.setValue("intermediateNodesAreNormalNodes", _ui->source_checkBox_intermediateNodesAreNormalNodes->isChecked()); settings.setValue("startId", _ui->source_spinBox_databaseStartId->value()); settings.setValue("stopId", _ui->source_spinBox_databaseStopId->value()); settings.setValue("cameraIndices", _ui->source_lineEdit_databaseCameraIndex->text()); @@ -7303,13 +7307,14 @@ Camera * PreferencesDialog::createCamera( _ui->source_spinBox_databaseStartId->value(), cameraIndices, _ui->source_spinBox_databaseStopId->value(), - !_ui->general_checkBox_createIntermediateNodes->isChecked(), + !_ui->source_checkBox_intermediateNodesAreNormalNodes->isChecked() && !_ui->general_checkBox_createIntermediateNodes->isChecked(), _ui->source_checkBox_ignoreLandmarks->isChecked(), _ui->source_checkBox_ignoreFeatures->isChecked(), 0, -1, _ui->source_checkBox_ignorePriors->isChecked(), _ui->source_checkBox_ignoreIMU->isChecked(), + _ui->source_checkBox_intermediateNodesAreNormalNodes->isChecked(), localTransformOverrides); } else diff --git a/guilib/src/ui/preferencesDialog.ui b/guilib/src/ui/preferencesDialog.ui index 36972d70..4d11c20e 100644 --- a/guilib/src/ui/preferencesDialog.ui +++ b/guilib/src/ui/preferencesDialog.ui @@ -63,7 +63,7 @@ 0 - 0 + -700 684 5218 @@ -95,7 +95,7 @@ QFrame::Raised - 14 + 8 @@ -3469,7 +3469,7 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - 0 + 3 @@ -7180,37 +7180,10 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki false - - - - 0 - - - 99999 - - - - - - - - + + - - - - - - - - - - - - - - - Ignore features. + Ignore IMU (i.e., gravity links). true @@ -7220,6 +7193,13 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki + + + + + + + @@ -7227,123 +7207,16 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - + + - Override camera local transform(s) with local transform(s) set above. For multi-cameras, use a ";" between each transform. - - - true + Start position (node ID). Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - - - - - - - - - - - - - - - - - - Open database viewer - - - - - - - :/images/mag_glass.png:/images/mag_glass.png - - - - - - - - - - - - - - - - - - - - - Ignore odometry saved in the database, so if RGB-D SLAM is activated, odometry will be recomputed. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - Camera index. If the database contains multi-camera data, you can choose which camera to use. Leave empty to use all cameras. Can also be multiple indices split by spaces in a string like "0 2" to stream cameras 0 and 2 only. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - - - - - - - - Add an y-axis offset before optical rotation on the overriden local transform(s). For multi-cameras,explicitly enumerate offsets if they are different (e.g., "0.05 0.075" for two cameras setup), or set single number to apply to all camera transforms. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - 0 - - - 99999 - - - @@ -7357,25 +7230,8 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - - - - - - - - - - Start position (node ID). - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - + + @@ -7394,6 +7250,139 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki + + + + + + + + + + + + + + Ignore features. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + + + + + Camera index. If the database contains multi-camera data, you can choose which camera to use. Leave empty to use all cameras. Can also be multiple indices split by spaces in a string like "0 2" to stream cameras 0 and 2 only. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + + + + + Open database viewer + + + + + + + :/images/mag_glass.png:/images/mag_glass.png + + + + + + + Ignore goals saved in the database. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Publish intermediate nodes as normal nodes. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + If the database contains stereo data, generate disparity image and convert it to depth. The resulting output is a RGB-D image instead of stereo images. Dense disparity parameters can be found under StereoBM tab. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + 0 + + + 99999 + + + + + + + Stop position (node ID) is the last node to process. If 0, all nodes after start position are published. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + @@ -7407,6 +7396,19 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki + + + + + + + 0 + + + 99999 + + + @@ -7421,9 +7423,9 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - + - If the database contains stereo data, generate disparity image and convert it to depth. The resulting output is a RGB-D image instead of stereo images. Dense disparity parameters can be found under StereoBM tab. + Add an y-axis offset before optical rotation on the overriden local transform(s). For multi-cameras,explicitly enumerate offsets if they are different (e.g., "0.05 0.075" for two cameras setup), or set single number to apply to all camera transforms. true @@ -7433,10 +7435,10 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - + + - Stop position (node ID) is the last node to process. If 0, all nodes after start position are published. + Override camera local transform(s) with local transform(s) set above. For multi-cameras, use a ";" between each transform. true @@ -7446,10 +7448,34 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - + + - Ignore goals saved in the database. + + + + + + + + + + + + + + + + + + + + + + + + + Ignore odometry saved in the database, so if RGB-D SLAM is activated, odometry will be recomputed. true @@ -7459,21 +7485,15 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - + + - Ignore IMU (i.e., gravity links). - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + - - + + @@ -11102,7 +11122,7 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - On merging, update to new id. + On merging, update to new id. Keep this unchecked if intermediate nodes are created. true diff --git a/tools/Reprocess/main.cpp b/tools/Reprocess/main.cpp index 0449079c..ee9a8bcd 100644 --- a/tools/Reprocess/main.cpp +++ b/tools/Reprocess/main.cpp @@ -96,6 +96,7 @@ void showUsage() " -nopriors Don't republish priors contained in input database.\n" " -noimu Don't republish IMU contained in input database.\n" " -pub_loops Republish loop closures contained in input database.\n" + " -pub_inter_as_normal Republish intermediate nodes as normal nodes.\n" " -loc_null On localization mode, reset localization pose to null and map correction to identity between sessions.\n" " -gt When reprocessing a single database, load its original optimized graph, then \n" " set it as ground truth for output database. If there was a ground truth in the input database, it will be ignored.\n" @@ -279,6 +280,7 @@ int main(int argc, char * argv[]) bool ignorePriors = false; bool ignoreImu = false; bool republishLoopClosures = false; + bool pubInterNodesAsNormalNodes = false; bool locNull = false; bool originalGraphAsGT = false; bool scanFromDepth = false; @@ -510,6 +512,11 @@ int main(int argc, char * argv[]) republishLoopClosures = true; printf("Republish loop closures from input database (-pub_loops option).\n"); } + else if(strcmp(argv[i], "-pub_inter_as_normal") == 0 || strcmp(argv[i], "--pub_inter_as_normal") == 0) + { + pubInterNodesAsNormalNodes = true; + printf("Republish intermdiate nodes as normal nodes (-pub_inter_as_normal option).\n"); + } else if(strcmp(argv[i], "-loc_null") == 0 || strcmp(argv[i], "--loc_null") == 0) { locNull = true; @@ -815,7 +822,7 @@ int main(int argc, char * argv[]) int totalIds = 0; std::set ids; - dbDriver->getAllNodeIds(ids, false, false, !intermediateNodes); + dbDriver->getAllNodeIds(ids, false, false, !pubInterNodesAsNormalNodes && !intermediateNodes); if(ids.empty()) { printf("Input database doesn't have any nodes saved in it.\n"); @@ -844,7 +851,7 @@ int main(int argc, char * argv[]) return 1; } ids.clear(); - dbDriver->getAllNodeIds(ids, false, false, !intermediateNodes); + dbDriver->getAllNodeIds(ids, false, false, !pubInterNodesAsNormalNodes && !intermediateNodes); totalIds += ids.size(); dbDriver->closeConnection(false); } @@ -920,13 +927,14 @@ int main(int argc, char * argv[]) startId, cameraIndices, stopId, - !intermediateNodes, + !pubInterNodesAsNormalNodes && !intermediateNodes, ignoreLandmarks, !useOdomFeatures, startMapId, stopMapId, ignorePriors, ignoreImu, + pubInterNodesAsNormalNodes, cameraLocalTransformOverrides); dbReader->init(); @@ -945,6 +953,11 @@ int main(int argc, char * argv[]) Odometry * odometry = 0; float rtabmapUpdateRate = Parameters::defaultRtabmapDetectionRate(); + Parameters::parse(parameters, Parameters::kRtabmapDetectionRate(), rtabmapUpdateRate); + if(rtabmapUpdateRate!=0) + { + rtabmapUpdateRate = 1.0f/rtabmapUpdateRate; + } double lastUpdateStamp = 0; if(recomputeOdometry) { @@ -957,14 +970,17 @@ int main(int argc, char * argv[]) { printf("Odometry will be recomputed (\"odom\" option is set)%s.\n", useInputOdometryAsGuess?" with input odometry guess (\"odom_guess_input\" option is set)":""); - Parameters::parse(parameters, Parameters::kRtabmapDetectionRate(), rtabmapUpdateRate); - if(rtabmapUpdateRate!=0) - { - rtabmapUpdateRate = 1.0f/rtabmapUpdateRate; - } odometry = Odometry::create(parameters); } } + else if(!intermediateNodes && framesToSkip == 0 && + (configParameters.find(Parameters::kRtabmapDetectionRate())!=configParameters.end() || + customParameters.find(Parameters::kRtabmapDetectionRate())!=customParameters.end())) + { + printf("[Warning] Parameter %s is ignored because parameter %s=false.\n", + Parameters::kRtabmapDetectionRate().c_str(), + Parameters::kRtabmapCreateIntermediateNodes().c_str()); + } printf("Reprocessing data of \"%s\"...\n", inputDatabasePath.c_str()); std::map globalMapStats; @@ -1071,11 +1087,16 @@ int main(int argc, char * argv[]) info.odomPose = pose; info.odomCovariance = odomCovariance; odomCovariance = cv::Mat(); - if(data.id() != -1) - lastUpdateStamp = data.stamp(); uInsert(globalMapStats, odomInfo.statistics(pose)); } + else if(framesToSkip==0 && intermediateNodes && lastUpdateStamp > 0.0 && (data.stamp() < lastUpdateStamp + rtabmapUpdateRate)) + { + data.setId(-1); // intermediate node + } + + if(data.id() != -1) + lastUpdateStamp = data.stamp(); UTimer iterationTime; std::string status; @@ -1266,15 +1287,19 @@ int main(int argc, char * argv[]) { ++loopIntra; } - printf("Processed %d/%d nodes [id=%d map=%d graph=%d hyp=%d]... %dms %s on %d [%d]\n", ++processed, totalIds, refId, refMapId, int(stats.poses().size()), int(uValue(stats.data(), Statistics::kLoopHighest_hypothesis_value())*100.0f), int(iterationTime.ticks() * 1000), stats.loopClosureId() > 0?"Loop":"Prox", loopId, loopMapId); + printf("[%f] Processed %d/%d nodes [id=%d map=%d graph=%d hyp=%d]... %dms %s on %d [%d]\n", data.stamp(), ++processed, totalIds, refId, refMapId, int(stats.poses().size()), int(uValue(stats.data(), Statistics::kLoopHighest_hypothesis_value())*100.0f), int(iterationTime.ticks() * 1000), stats.loopClosureId() > 0?"Loop":"Prox", loopId, loopMapId); } else if(landmarkId != 0) { - printf("Processed %d/%d nodes [id=%d map=%d graph=%d hyp=%d]... %dms Loop on landmark %d\n", ++processed, totalIds, refId, refMapId, int(stats.poses().size()), int(uValue(stats.data(), Statistics::kLoopHighest_hypothesis_value())*100.0f), int(iterationTime.ticks() * 1000), landmarkId); + printf("[%f] Processed %d/%d nodes [id=%d map=%d graph=%d hyp=%d]... %dms Loop on landmark %d\n", data.stamp(), ++processed, totalIds, refId, refMapId, int(stats.poses().size()), int(uValue(stats.data(), Statistics::kLoopHighest_hypothesis_value())*100.0f), int(iterationTime.ticks() * 1000), landmarkId); + } + else if(data.id() == -1) + { + printf("[%f] Processed %d/%d nodes [id=%d map=%d graph=%d hyp=%d]... %dms Intermediate node\n", data.stamp(), ++processed, totalIds, refId, refMapId, int(stats.poses().size()), int(uValue(stats.data(), Statistics::kLoopHighest_hypothesis_value())*100.0f), int(iterationTime.ticks() * 1000)); } else { - printf("Processed %d/%d nodes [id=%d map=%d graph=%d hyp=%d]... %dms\n", ++processed, totalIds, refId, refMapId, int(stats.poses().size()), int(uValue(stats.data(), Statistics::kLoopHighest_hypothesis_value())*100.0f), int(iterationTime.ticks() * 1000)); + printf("[%f] Processed %d/%d nodes [id=%d map=%d graph=%d hyp=%d]... %dms\n", data.stamp(), ++processed, totalIds, refId, refMapId, int(stats.poses().size()), int(uValue(stats.data(), Statistics::kLoopHighest_hypothesis_value())*100.0f), int(iterationTime.ticks() * 1000)); } // Here we accumulate statistics about distance from last localization From 27ca7ec05849b2fc5c22540673afa6b156a2cfff Mon Sep 17 00:00:00 2001 From: matlabbe Date: Fri, 15 May 2026 07:18:15 -0700 Subject: [PATCH 114/126] Fixing libpng warning: iCCP: known incorrect sRGB profile (#1703) --- guilib/src/images/IntRoLab.png | Bin 33861 -> 39522 bytes guilib/src/images/IntRoLabSmall.png | Bin 14635 -> 14986 bytes guilib/src/images/PauseNormal.png | Bin 2598 -> 2719 bytes guilib/src/images/PauseNormalRed.png | Bin 2633 -> 2769 bytes guilib/src/images/Play1Normal.png | Bin 4087 -> 4211 bytes guilib/src/images/Plot16.png | Bin 169 -> 342 bytes guilib/src/images/Plot48.png | Bin 349 -> 437 bytes guilib/src/images/RTAB-Map.png | Bin 60476 -> 65193 bytes guilib/src/images/RTAB-Map100.png | Bin 17209 -> 14177 bytes guilib/src/images/RTAB-Map2.png | Bin 38516 -> 34284 bytes guilib/src/images/Stop1Normal.png | Bin 4410 -> 4664 bytes guilib/src/images/Stop1NormalYellow.png | Bin 4281 -> 4543 bytes guilib/src/images/astra.png | Bin 11052 -> 2401 bytes guilib/src/images/astra2.png | Bin 3334 -> 3430 bytes guilib/src/images/bumblebee2.png | Bin 6534 -> 6736 bytes guilib/src/images/d415.png | Bin 3514 -> 3691 bytes guilib/src/images/d435.png | Bin 4846 -> 5025 bytes guilib/src/images/document-new.png | Bin 1008 -> 1652 bytes guilib/src/images/document-open.png | Bin 1550 -> 1698 bytes guilib/src/images/document-properties.png | Bin 1115 -> 861 bytes guilib/src/images/document-save.png | Bin 1971 -> 2124 bytes guilib/src/images/k4a.png | Bin 9192 -> 9413 bytes guilib/src/images/kinect_xbox_360.png | Bin 4154 -> 4340 bytes guilib/src/images/kinect_xbox_one.png | Bin 5117 -> 2660 bytes guilib/src/images/l515.png | Bin 3731 -> 3591 bytes guilib/src/images/mag_glass.png | Bin 1633 -> 1795 bytes guilib/src/images/mynteyes.png | Bin 2432 -> 2606 bytes guilib/src/images/oakd.png | Bin 3735 -> 2483 bytes guilib/src/images/oakd_lite.png | Bin 13720 -> 6293 bytes guilib/src/images/oakdpro.png | Bin 8691 -> 5259 bytes guilib/src/images/r200.png | Bin 7570 -> 4147 bytes guilib/src/images/seer_sense_DS80.png | Bin 7163 -> 2733 bytes guilib/src/images/sense.png | Bin 9241 -> 6863 bytes guilib/src/images/sr300.png | Bin 2171 -> 2042 bytes guilib/src/images/system-log-out.png | Bin 1725 -> 1876 bytes guilib/src/images/t265.png | Bin 2978 -> 3164 bytes guilib/src/images/tara.png | Bin 5170 -> 5170 bytes guilib/src/images/view-refresh.png | Bin 2024 -> 2171 bytes guilib/src/images/webcam.png | Bin 11049 -> 11417 bytes guilib/src/images/xtion_pro_live.png | Bin 4427 -> 4599 bytes guilib/src/images/zed.png | Bin 2676 -> 2843 bytes guilib/src/images/zr300.png | Bin 2459 -> 2624 bytes 42 files changed, 0 insertions(+), 0 deletions(-) diff --git a/guilib/src/images/IntRoLab.png b/guilib/src/images/IntRoLab.png index 12ed4e411bb2ae76b04ae885ec3e5cd575a29ee1..2d9ac03a8af607a9cbfd711caed9d8577f15cd1a 100644 GIT binary patch literal 39522 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

vA=H|C&pq{`7@%@1L-ap!Q8N`5p>{n>W*3!K3 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 diff --git a/guilib/src/images/IntRoLabSmall.png b/guilib/src/images/IntRoLabSmall.png index e3a38faceff06299221a6f000256f591fcc4618a..c5de684b21f37d1b82fff123409adfcba90df5f3 100644 GIT binary patch literal 14986 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 C83hFZ literal 14635 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 diff --git a/guilib/src/images/PauseNormal.png b/guilib/src/images/PauseNormal.png index b043b640b30178792c059a723c05ff0a2a9cb619..90d16d03f91a7b88a11ab2c03abffbd20bf957e4 100644 GIT binary patch delta 2631 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 diff --git a/guilib/src/images/PauseNormalRed.png b/guilib/src/images/PauseNormalRed.png index 39a245b862bf5792ffc4035e1d74bd040490d824..b332ab9604db5ed4dd7cc7217b1dd87eb3850242 100644 GIT binary patch delta 2643 zcmV-Z3as_X6wwusB#}8g5C8^ZOGiWi{{a60|De66laV18e+kn`L_t(&-sPIlt0Y$y z$3N%XA64C*nM@$b1V@b;NdkfzH6(~AWTm301ay<7i}*L_!i^g@g5pMS;nGzEH~v6W zFo_s{FoBq8ewc(fW+pN3WoF*I*RSrq$Hl$XuV>Od{o0KL3yP{K`hD;Dp6~aZTldz8 znelc$^nV?|fB!uJzv|(~Lg2L1*_FP3;I`@H)>KNy3@|Xj3N-6E(0?DycYnLL|C<5c zSjG|Hbh`pv0X{XGG7bLY^Vr?#MKCK%xZ@axg3faq9)EN1i3?`WjF{J2ekFmS6i!Vi z_k8N^J0HB|#0@v4l#EhZ;H3^)G8i~ab}wJPeBtjef9^kdn0{6xSbwBa&fNR%cYX21 zEjQmuDOB7H#VW4Vf2$!mr6e!BeA7?fxcCZic@>_lD&kK}r=R@ZgJ1aZJ@=dDC*F7a=Rf>`yKdvnUGm{U13e;OC1a_PDTzSg zgSVf)_5PbreddJ=JHH0fLd|xa19!x5`)BU?(5KNt+1^faL^bOwyd` zsS|we_S0W@WarY4fLD%H10lF>I6NTaIShwje+f%t)(0%-8obp2l7U0;s#Bm}mV3Wl z3y@W#*Z})iu)~9jm2$&O4fjZmk`wF?sCW;|S~=N90+w@OIK*s@rPQpFu)JiOl(3R1 z#lUKr=3=%~vx~sTBb%8GgOTP~I;>bJRVCVe&3>X}7?ROt$fb-yACCZ~K$=6IFC<-8Bd8o?5FstOmgMew-@DJwY(Ij;v^t$<}L50o@-;LVf88hFioDV2m! zug7{T$q9x7Y?#+9q=8aOO~?{sE|vYV$z6jf|=B0nNW&cH!*7!kUSu1AUCh40+wYFd?|(*6jQ7S*f5X=V;Bc0(lXS}tQ3xBf3JdVVqm3Nf2c4F zLa`&<5!;F`#z>$E<(;r!J90AO99nm*k=@|)IuFH7&Uks6H}^vvKiL! zGyeqvYj(+5$VDh-qy}!(j%U(-hfvUJrDL!trDCR1+hZ;@*ksc1s5O9)sV#cz8W`Kz z&^k>qt=F1B7&Aw0p>CW9HYs3`f0Nb5mK$EtR=BoM)l3_E~XSNp#@#RbK|#w`LflVSxDe_nl1+_nlVIs;PQWJN%AF@QQy=TIl8D!%gs(Tyb_ z&NG~Qm5VRF#NPfsDgx@!=yCl7e{zPu@dWcILvv`h!w~J*+Y&(o=zw?oh00P3v$<;e<jIJv;oOVZzy4AGN;bUePy-L0M?ye+2R=eS1DCfW zpd$F$gmP(z^v6fh*Uk}d=m{~jtVakDVh_GYx~^qv^Hk=Y(nSXtv2!?IEfa#^LO@~% zp$Fd~p%L)wJ+xYZ7U<#pE95^tio0@=?#2l*I9zlPBZNtlfCnLecDE#u2J~-F5)UsC zPE83>(GZZ()vSlUN*p^RbVxUWIN7)+5US7nUQvwL)wzV&BcZQW43z}FLwtmOiaEa) zbG3yqLhtY=I1_Wfucy9TwTPz&FGcC(*Dp0z5^$>GCo^OSl+f1!;*zYP z1MyeGI#>69MmFq_x_;uBoIn!QLSaSggd-SLmR758!Cpz=v2zz5e{C;4eEMByf}=@2 zdekpv-K^OnaPIl%?JuvMf0}DvewWMd^@I7jC(iFY_0Buqf1fjJ-E2%wJeo&FTy-$t z=XcLN|1@xErJ}38Qi{mrv$x#*@Yn9U_p3LaJb6=pK@IO$7OIHo#VdQ4e(<|TfBw^R z=e`d-NBhVp)DLE(Q2W69C;bCozxSixeD9evcNp!}RF!L8A|jGX$-g*z_Uw1hp8d`~ z{2tghyLzvB9RUF8;qHlZw-#%AR;zz22RZK_z)QdmaKPr{%qey6F&PDV>sJgBk&<)1 z2>%3sE&zK#xi;SG5!m)P?iYgp2A2{0SOnfq$J_otd;ABYnj=tC;^5-|001R)MObuX zVRU6WV{&C-bY%cCFfukRFf}bPG*mG(Ix#akF)%GKH##sd<68n10000bbVXQnWMOn= zI&E)cX=ZrLSV delta 2521 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 diff --git a/guilib/src/images/Play1Normal.png b/guilib/src/images/Play1Normal.png index bfa64fe28b750bfaa5d0d04e6db360a47c89806e..245650bc34257e585a2432709d545f5e5aa96d89 100644 GIT binary patch delta 4139 zcmV+`5Y+GYAM+rPB#}8g5C8^ZOGiWi{{a60|De66laV18e-Cv@L_t(&-o2T7j2%~f z$3N%H%zf|MUfa9&`nB8BZa@67pr zf4}eVagG(Qf7YdU`Ukh}Z6v|k*a|@a8ifWA&|n?J ziEwV-F?r5WYevl107hHVoctoTl-S zz|ok;4kXMi%`>t66gU@=L`3llW)E|0Ekh+!h`?(B|PMe=LPC$GO9Ie_`hE$3FrteuV`3gEd!v z>7UnZzvYAZe4X2F%rP<)5*xuO1sarA-sK(e7a|~?#bhZ=l0J)|1+pQGOHi~vUbSZL*S}iY^rrU?57xQu_wp3-io_|<9{l26Efc!xNNClk ze?^waECa6`-~>d76eNjo|4*7cdpw6K|Ln2yZ{PP$WMSrFD1(d1rf2upzK|cf@%>wd z8~nkIc?h6o6$XV-3IiAv7KK&*Zc_8ltTajgCSWDKJ21;EF@gx}+8j}Dwm7peIud2K zTppJn{4PlHk_qHT{_dl>k#~J!~r^HeMYKf4M zj-{j>7^hZ%Hx7(dBo>SnwhxC?s!hi0S6rrS*yC!yc$g&tUYtPCecS8vqo4YZzP@I7 z>-9MZR)g0hfECNtTSInk(6o{^m6$u%T2Gz@JH8{*gBZu z^5GoWz%V}7pxm-Z7~!Flh%b=D=FR#KKUYwC0^Oo_b`dt}Q1J4t0f_ zKYQQ6+Nl4kT_H}(3h-8Itkxt75;44CTZX<;h)qA0Sja{JyEhcrGnA#?e{Axbi5h3? zLaalYF2JHtO5p%2LgG?MrNHg72v{pv>#I_4Q8QV#G|ZOmu6+NGk>oe*k;6G%)aiBl2=tPxBigc_!2g*X{l=VtHye<4U2AOuj^D~EJ( z^qQ`s0i|gs9*EL-_iBX%pamLj#aP8sO*-v0ZCDp*iLJzs7;oOz#T!OSeE;wqpLu+S zlZ_m?Y!7oLq|$_<5{7e-R|;`{s8Uw>uv#Dit%d%cglhLyJ5>Ja>!rExh<81pOK*N# zkS+F!ASTR2{Uu`0Au8Tz$_mJxqXghC#IOcs^jb2U&{BtT*GwNVzLP-w{Bvzk4Ge*yD&=Q`8&te>g1Xn!?!yp>Ck<()EqE zW&%MVL|KRKy5Txtz!L}t^E$V4Gz=1wlxf>QI}ut28U|(>Lc@R#mCu?$A?l@rUw#<} z3vF1}o#D<~hq(XGhPnN+Hnu)Vy-}qeTc+!ddV*Jh@ltFif&(&7-VQ?G0{npmjDux)LgyKdjYfBxw@ZXT)Au1-^H zG^sQ#jb#K{iFc~j4jpxs2)fq+I#khyAsuxOpd4``s6as=5DH=h7o^FoSvGNA8vJ=% zflK_hZJ-TYyS0bw-rvI`2g}^~%`v8GT}V(MFak+xf3YAUs2~@ib3IssP?YXR>4HMM zEK&rW>30z{%QUZ7a=6g+d`UvG>W|{1t_1$@bps^rn7_LFEVi%#aREvMoFL9`R+Wyl zQc+PrYamMfdtnT#7E{W<)d|FEZZBrA>fKdKwGE7{iO{AB&aPNrt$0$Qmyk#UkwhSp zC5Nm8f1lp9QfR%Trt?U@SX#)^Ph{0AS}TfKn3`?!kM|zHNS2(^ixE>SJ1oF}SPKw} ztxkz;RYmgs%lE5`+radAL2Dt&I9plt& zk?y|rC_>-JWXVJn&c;Th-Sz~MQ)eYPHzu;Be_uKw6G^vyf)XFsp+8#r^@$noe)JiBc60&Bt!Lf# ze|0!lz_bITDLAo<`(<$;4x9^-*5oX1VHU6<(o9_Q_;F+3hP9y67nFFdhsrM&1`uga zND!2g^Yh>$&AB6)P(wb1BNOv{{TI*j;BRLzxwZ6f+lvaiv2loL3d%UFvLJ#JJP9XQ zqp&u_)t)#4obwR?q`v>BHZ~u#MiE*;e_KH#1j_3^)IyY6pcNvZkwqE3n0!JFxezAi zntcB8XL;z^2`ajm{?WYznI4?WU|I^}90U$g0?L93{(~gxcblk z$JSpsX$nJIGd5oGekz@e_L1-=h!T`i=T(x`M3jO;2($H=`=2<*eZM|QnIhere|EE} zH~=b#O*E!0C~bX0DDh)&8fn3g+|GhAA+|o=l*ZG~098oGY219(RUZFw();c^tc{5} z-dC~&JdHr8z-b?$#rnRwsR6kN;>7Xb{u6w2|1rjrh@PQQHunrdkVjmIN&Ln|b-caY z^Qk%U+E0waBspyPyZf-y&jO7le*%D=z31P}`rGd?d5sbwKnPOsD~Kp?D-Pql!aLnK z6Z+^qePV)d?>ofP({*}>HnU}5JwdhvD#99tNhEE89nk3{paiD`m#!0#ZvKrXX*6+_ z2OkF}@e{}r0crl~m*(8h9*T=^dAqYsFQLUqbVzt2qJ(gn+Z944RD>Fie@)DC-xI%O z-}pSaHR~DOyNjsUg92G?i2xE+yxIhN;{ne9D>7FJHIzBV9F01lGeV*fW?!# za4GnRo&1M8lm6>(X_ul*AuW{wX*AO3Fg>tZXeSiHJX(9;FU&0Py?uxH`N>Ii?;1Am zxr$tOA3-LI5a1kGD@)*4e|SDsyYisJ6ucEdEF^JAT&szl`rP+`qaFRj>T@AGJtorA zuexu1V>TO;D=0EKMVL`U9g7q}2qz}XltM$J)#gVBj`5|R{+g5RfOXq1W8Id`6nlHg z<}!qVCQ#lPIxWwgCfV6CijhTgvBM<}7dw)c&{z;x{_U5pfAWp50nY)g3q>I1 zaM$0rdal2@nt$!yT&YdI%a_dMk;r5ss;pvp_7^KC1>IOz27H*P|$nx{4S)Rdk2r*8m9fQ1z$`uvtzok>pr&4+8n zZ#|UB^%M&wOTOs6f2Y&>ua%})EYj06LJ(TAxqxgTLna>*Mw%c@=iJoUHm#625bMEr zWa4$6_MLPMEVLWKLM5U;eXgCH`p8$1g(rdW)o?F-I&G&;yZZ5Qm%a7vMA;yl7jk(` zChO%CMv5rXWV0dJLY8bUB9je>GKx&3h(bja`0gEaDwcq9f9Zs5eO-4(a0Z-lxI}P? zkTiwrtfM>`FgMnAts@`$x-@_F->G!!FO2{o@$mzu_WWED{r)YY@=vD-h5N+269>(aydmdtBJBc5@7^k=)E3xyq`uX(0&ggl1iuJ zbt~8uyiJ6pAynra(`Oyyr*f2!pQ<+wec-E7d-z`9!1A*BvIqc3a{8bt|M1CnYvb;Q z-8F2CFMm-+ky(_JB1nmZks=HfL8#E_+~hi5Hy*f6e*_x~?S@dV2s2}rGsj`GHb2E;~7c4Iv6;2<{F!|h>#=^6o{$XBTu~l(ER0Ph^%uUm-%Ne^2a=q895@b);kPp{3DnEI0$?rS@bzKu+uoiT zyzQp^;Lc5@wS!TqKTEMkkuR}Gz=`*Ks|K}-e^8r^sh6jd=FHLQcUN^EPcZkrQx zws!D@YwSNta^`t(r-5nw=Hz7oe>DhnE)8UXBGBbY(FNr3M>gR9qq2pcVQTn#UQO@G zf6H9&RVQ#ELP2Vst#fCmZBu*IuKxu&@64Vr*cJf*001R)MObuXVRU6WV{&C-bY%cC zFfukRFf}bPG*mG(Ix#akF)%GKH##sd<68n10000bbVXQnWMOn=I&E)cX=Zrtk4XFSC002ovPDHLkV1fjL;PwCj delta 4015 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 V%$|0h*6siR002ovPDHLkV1g1k>j?k= diff --git a/guilib/src/images/Plot16.png b/guilib/src/images/Plot16.png index 91432b135d46a7753b7df444dee913a7bd4d7d07..57caaba07a9c9018f1a007f88b6316d0bccef498 100644 GIT binary patch literal 342 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!93?!50ihlx9Ea{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8Uop6XFV_fxwqR^8f$; zs~ZHAfgHw?AirP+hi5m^K%69RcNb?NMQuI#T^`Pg~6m>FJQ1PWPsx;Tb# zTu)9YNB{yJ1|c0i2c|PmPcaBZMe;E5Xz!7VkZ@{jNNl{!B-X~(9?ImP8Wm9bf(7W4 z-ld!=GuLncozI|J;u=wsl30>zm0Xkxq!^40jLdWmOmz)SLJUo;42`V}4Rj66tqcqv hM>C0{Xvob^$xN%ntzi<|u9ZLy44$rjF6*2UngEuwRUrTX delta 153 zcmV;K0A~Nz0;vIz7=H)`0000V^Z#K0003}FL_t(|+GAwE1pk4Uk%36Sh($9lIf4ej zY(}R^asl~X@MVw$JAX67a}b*W$|lYQFwN)&;4uX5N=7uzaKB>_$7%>n1KEHP$?q^* z;Lao93Wy6piqIrcVgcC@bZ?@>0xkmxxrBP@V0alo5&!_tKoT+#2_Hj<00000NkvXX Hu0mjf$gMdL diff --git a/guilib/src/images/Plot48.png b/guilib/src/images/Plot48.png index fa475cb6f1efa2aeb02246d8b6e39ab66eecc3a9..e89d24b7312bbc3a38b9e2bf1a7dfc48a6293ee6 100644 GIT binary patch literal 437 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDD3?#L31Vw-pOS+@4BLl<6e(pbstU$g(vPY0F z14ES>14Ba#1H&(%P{RubhEkx$R|yOZRx=nF#0%!^3bX-A@C5jTxB_V)@MVzv|NsB$ z1_5Othp{BcFPOpM*^M+1C&}C0g;|it=M#{_UgGKN%KnO#kDZr`nc?+CpwLWD7sn8d z^II<%@*OhZVGT%L>HTcJ_Y)CWNn?}mClA>gx!I}Ob21q>OknV@Tl+gV^hxZ&#XJ6m zd}H4>N%nz2;SPfbph|Tb zK{2+E?Uuo>z%ybP*bq!5I3{9*Ijjap~bPdd{3=AGe gGl`;T$jwj5OsmALVG`S}l|T&)p00i_>zopr0MM|8Z~y=R delta 334 zcmV-U0kQtI1Kk3U7=H)`000120{Mpk00AUPL_t(|+U?j~4#FS|0O0i)9?H{tD35{J z5(#51l#bfYwGT!GDIXbs!gBEd>ke1}l9vIHbRbP=!wis!@68Y+iBqTC1R()HR@;8x@Uv>|mq;8q+3 zXdtj$z^y0&%tojHdi5tE$b5%}na?!33MtNq!zF6XwYDqm0J)4dCjQw#Z-Vakf$xA6 zgRnck*XQ}m#a{p<@smdgIRTWpxBSwbv`p zh=X7N$$Zk&0>4poz#^@?d-7RcHjl>6hs;I^#0rdLIZt00Izz00bZa g0dNX%69KS}58b(-D4yYQaR2}S07*qoM6N<$f?(c}SO5S3 diff --git a/guilib/src/images/RTAB-Map.png b/guilib/src/images/RTAB-Map.png index a14115b7b688bd9453d4addee319d69a400c1d51..ace8aed3d835893c151d24301f505d8848e974c4 100644 GIT binary patch literal 65193 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 literal 60476 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 IHSvo3AD{C=`2YX_ diff --git a/guilib/src/images/RTAB-Map100.png b/guilib/src/images/RTAB-Map100.png index 56c6628a654eed7429586409cc8ddbc43ecea734..713198ef1360caf30056fefaa0e0103e2a90ba52 100644 GIT binary patch literal 14177 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 literal 17209 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 M07*qoM6N<$f-*r(?EnA( diff --git a/guilib/src/images/RTAB-Map2.png b/guilib/src/images/RTAB-Map2.png index 26689e084c64f949f718a4fda9a7947fd0cb18c9..0ceae5cae2df71b00f38d31254acf51da60f2fb7 100644 GIT binary patch literal 34284 zcmYIw2|QHo`~H~)W68dgEwUv`S)v%Cq6LviAz4a7k(6bIl#(qGQXwrw)=JhHl#)bI zNR|-UcZL}=|9g19zklz?o60%odG2#B*L~gBGqP8VLgox8zV?@JSXim3;bE;%z>ka5#-ud1R+Ku2=hmX{}AM|27-*aAc%e{ zf{6G%tgzk<|G?^caK99lod2?4_UfWoO>3*FBH# zY32`N2K8<~%6}SnURR(yP3QLH&*DM(rS6WfKL0rRXnozy=2hnHH=ZgveUQLK$lMyI z&DQe-;lky&W2KSb8E+o%*0@t3hVfICXi>ksNdC6@Yy6Y+ZfUlce@xI|4h_GUL3(k+^3s+n_Vy(@e1byC`cD*_h8t6ryjqa*~X_tiyME zw6M8zAa9mTlDr&T(&;ODBVtQfr6&E2DqZ{Y@b@|{mFqKt3aqd7>{*Ac{<2G}P!pnY z*eoSJlngFnX8TWywTMw1(lKb%wueJv_Kc(``et|+CCjW9-FS*;G?I7=Q@hg?-n@)k z6if0RzYp=O{a!RrKHIG>dj?ZE41soKaq79PtV|tVNsqY_-oSW{M2G8b zzLN>P6o@S_?&Ju2T zbO(zZE;45Qo-T&Mwnnw(l+%LPJ~pZBK$)1t-_begd}h43U9xCAFE)#EKnN?Fl>2Qu zyNY9m= z@G)+4c6RplGauv9LYpJ3cq3|6?2hyBa#a+a$nNw;si*-QWL4=*+hj)AGNq%)8CMol^ zzpXQv9n}2%`jh1)pSr*Z#FT5o@$*(p>+j!R-kk5Cq-CracsN5uXp>g+NX_39Z&)cs zHob{!68ikFDnN{QcIN{-QPF9h8z#5APhdrTC8Tx5WBIvQ1){=!+lE{3v&}pdO&9d4 zUBFQ=NxVS~!|c(kyqYqIdlKs1#X(jcsfD!A>S>qa_qRSouU+Up?`pq|_YR@m?NTL$ zVSD4`zwzJ%JJEk!cqKtO{@dPZsMh5d(HK%+eBZQZXy~IiZ?tuEbnKm-`{Z#87qVNG z?!}mJC9_aYCGm=yCUeo0G+hL-a;!_5?)OkicJh=L^{%)Hw*W^a%Zdu0q02SGytOVG zvfOV!Bb2o;=ABNRazAtCI@}f=j+{S#KL6FLKg(2s~Gg+G%>LvCGLdUvKaJ`IQ;! zT7G^$^P80r_@?OX+nX&T2XPoRaatHb8FyW!U!Z^MOZgcB(w7-YR%yJ-dV=fUR+DVe z)?2(vJD71*r2g(|sb_b_@=~v5J|$KRc_a%<{xCBCXYrbYgG24$Z2#i5*_9CXyG1oM zPWeyBtO4i?WdHukoXN<0O0q@V*nud`9ho2S6_2ncx3@4I8})B=^hhwlxepUj+ixI- zWb@iPIeDBrr#xG2EhyOa^CL$RPA{rNx`uPNaXELvlU3|%(N39AJA5QQjk!_mX^FQg zJARY;%9Qsqw-&UUxwY4?-_Om>J+B{}m0#tF&g?2ezl-?H!IgCl%WCd{{b^-_zZ2e` zPsD|5;ju&+oC$Zpl`GZ$&c4&3KRY@E#E3tB9J_!2zP-J@i;qv-jTZQ=k^onB8t6a2aE*i(kBn{G5UHEr9r z?Wqt}DxQth=WAC^<_NHudVlr*Q3-;+dbH|MK;#mbUhXg@phUuAtpg=y0-AG>Ws8gT~&!&8v*8 zRCMcs=|3}e)G4`63mowl@yR}>Wg^T)53(*#FVFS{PZw8JeWB5I@7S^9(2*lrd-gO> zOnA%ffbd>^=?PoHRz+zET;n*DRf2L#YW91{iz_O3u=i`g!)u(+rH-?!|C23O*AeS2qC7sn%cQy)G&l*Gny2s2YZ zTWD#WeXHkYwz#{|ghKgwa;LOR7g5e+-BZ|Iep~x3Pwn~oa9918FI# znTr@0kb|6eqe`$xqBUlYJJs|RJ#WzYloDUR8?^+vh0_pPUxPx!D@R?_4^_W`_v~LE z8GP*Q6fJbB9{6xaBqlbt_}#k$N`flw3iiadoI~U-NuFf1%1mYFHmuzN9S*PK#;1f4 zKGgG#s$lx98XX$SqjzZt=lS*FpY~jvs-W43k(P`dhK6w;2bJC8@t9UjJ6b*bs%>jW zlrT!lb{NkimFSJIa;?$15r(_6Oc2q%5Jj{p5%_&K#4& zV(oHxPL!fKpUeIak2%_>(8}zok?Y=|wRc+b-Ica9|y|5IMlJW=( ztFc-`eYDD>8N29&IkJVW{g5K{2^%dO32#U3H3{FS!OwqJbzyQ&VWN1XhdHpDetrpA zS+c^y!hr@7{mAz02&%ja`Mj*l7u-`0gtM^*jLWj_Xmy5Lv?ZVl9(#%E=tV*%mn)#cZ?*%VkEAH~XJ=l0pF{rw?haYu0N7kf1M>#z3EjPV*C)+LDC$Wv&-v^)D3njwm z_q)5isCcP8u22sEfG`Adx=`oIs!2jKc27C!;(VxG>%nE*dH?ZapKHO>^WXMME0~hY z`ofyNfB)`tn|LoLmuREJMz(bolN=CDRoGb0=F!p7Vfvb(A#J8U5*I|7k>%v%to$S7 z!tymcf^qV5%XgO5fG#4hmW7_`hKa!YlZ3 zE1>VU(RLq0Ol{&sEwJ{mbH0s7#f(l(Jt>?hM$2d$upW#e)Aqhl4du+EdDut!!(rW&F{;4hW#!U6m*#nNoD07NHNR{V`Dc()XHEmn99=<-exP9{H#@!ocs<{t^$#_ z(bzaQ4N+$CzRrwLtgPp?YZc|-pge{Y8EbBEoUOdLA!_R}GP|u9Ii8=T$N?K^&Po9H ze1P7u;nN8sy1q-y%ns(?3lD-s4!P7-_c>0A^*+(u)~`bVGfBMD?2eq-O76rd9S(Qg zc*~#C9m{+!jU0&q=T5-OkXKjr1q1{l>;HsE{WK{NEV{+9WM?~ZDPbFnC=0Pz(&Y;) zRM*2B`wP6$UFJL6Pz97_;ob`z`KYn*28wkQ zKPz{8M91f0zIv*7BeNgFRUWal)V(rg`Yv!%JTET~Hv0UH$taj+soZhy|}2v{38j93fIM z7w;{?k4lW&ar_4GF{#aY`SMA2HYWaK{oI@ugzD!qY4VLZHv7FS*RpX%*>NuZMG;0j=0*Nm)(pj;*a|x?h{L@Ik?{;Mh`6#dl$A%APGpQbSiv zSmhagHFti9XBB*H`BU%YbspL$>uvW%h40KVs;#B~e_s68D!ctu#e0AOxrK!-%d!^$ zHykSOi~c2)NkxrWnPka^RRSzhv39MkGZaQ0MJox=zhsO)dos`=Dt6Uv>6y1{pzx6l zyZvt^cjs_ty@Rx-C&>+sp^%VJdYH$X69PsDujoBGG7)H?lfJ34=ZL$zLSW@1$gJ;A zmvuu%(4x9?T)hMsvu3#(DJ?Ap$XEiE!B;s`EOl=ecE366B{lTIPx2;PF_%YfXEKYW zUHa~$O_3ya#yI0weXIl_8A@qtsB)cQz1N$q5oI4ru4=j$+w-Hdo;;D`>yIsqMc4hk z^v6#xCoivlGSJ4+@lMmhEl=rU1 z??t-;AO*{RW@+Fee&yY{bEn@-zwv_y4=AyHsRJ{A>Y>)OO{6Gz*D^hN>dM;fqvY)U z@pGFu|K`}1d6T7f_$V>fO<4wi=`k_GO&CAT(Rrm8VWv+dFS)z8Fp@N>ujTvqqXS_* z&Y3re!Wih!2S*5WcF&f_&k73S3JN4VNLbk$zkdBX{ERp|I~!tw;8a@N@4rn=?eBTs0Lh?(TAWB@ z7;;8A^S+y~OYBn{m()-_O)DmBb~oN8d+V57dH)|OG9OeMj$!jF zKUp&A7Z1+xx9$e#p+ko>ps9g`Jb(3FfCJL`kR``Nn|syz9!p11nXF%*+q%_?)Nk#} z29#U;_HDz$!p>vIjP|(=$3tad=feTcP6Spi za7<|R^qivgv!T9b7`2Rd6gr$Z0Z46R)$d5sGDD^DV(pC2-b~J8967O!0$~(Yg>$_O z${tY?bcK6Ad#1qbgg$*bAh#tmzQvF#8C5T9;v$m!^5wupS7}w)+5_mhG<0O=QMku7Z1iQ8H78{)Nff-y;<8@4dvxO)H#Z?j|E{nDjG=xY3-RbVz=H#kM~ zf|s@}Pc23Z&2Inpsadw9iR>^=Sw%S;2oIT-Ul zTSThCJEVlvm=-cyhI{w!Rb8FG`Sz_&&GH^2pEAe5O%rHHjL?wyPyOE2(9i&-dBqJC zhnml6ui2-I9fOdNaAp@Yyfue@;XV_s1+n4?3I5*L?!;m_$qlU3IJ-;?h$N)dqGBRqZ4zBiab~R^qGAA4zcli%RA;aY#)BTs`8We6t2|)ImMA7ta((6$qG2|b4R6RWO!b_oIE;u?)mfQ zsgEBIL-QH3dJgxZO}UFG3FT+xOWKZyhM^B&7gbKa1w22vs{WihBNJ36LOy&HJCVK( z?~o?}UA+lE4hvm;W?^BxrO{eaixJ}?_)}3yNgcpF4`(OyORzp>cLyE}ol=8q@zAPO zpskpU_Sa_~sF-f_Wnr=y!-1u-H@sx87Gkp9H2j=xeT<8tV`%Fnt56P4{bZ~3^X|1O z?{DtmfBu{X1Tec!@%XLGxpyfiY}vGF)9ni>ZEbBU6ttvS7l!(s_T?=1(-$m0$t<_5 zZ;C-$fSVG{+nu_-HRxM@UCGwvyrmZA=BUvuuQ7h?d*FbHME<{#GVfXc!84u2Rmv{A z_Nb!Rp|HusrX#G^jkhW)IygGkk1v+Mo|udzZ5}602 z#K(_}albpZ!{wzqaf-$7BhPVK%_3#mbZ{i;$4D{`Ij!ExIQwLFiwEU|9V7FQThBH{9=Zi*H^~9f(reKqa_>T(*pA5W>~?E4Xv&g{M3 zS(Ns<9M+Tc0D@1wh7tj7$u{A^wdFn$z64na{=tv&ZNGkf{`3h4C9ZsT3`;Ug>fX@d zr_`%n>UizOFZVEST-a?qLZL)Qnq;>zjBQ(;jh_wJ9pLWMSoPt&p?Hd@qtpAhG*O4u zM|IQ;Kc?-bg2Z&>^7QPrV&9F9LtcdIw7+aw$j9##cfWcy4WfGM)V)J^4`!MO?sH_; zk@_f}(xRe-yghzfREl13AlXH)i@QWJ^~1W2OLU**KR}rwxEouwG5u!j#2b_gk6048 z$i2nKta@H?AjnR0FoXmRP7sOaw9-XYV zw6rWsOR-z9J$~GOqEj<&vq8hZe`gCC@X0xuQ?hHW_4ce9Q#hq@4?KjEtfmff)7%49 zMb;XV&dzAwq*K>h)W)$p<73a*Y}*>Q1O2O=7&2&m+e<10`+F~Y_(8HzJ6omlS-zQO z-9BFKsN1G;A(TH+a_GQJsEvK}$*YjT&Utx#K8Dy^IS?Isdp2brU(n~wj17-*N2(9M z`ERQYN@$auB!<4qT46Bi^O*E8_M&5c+P!7n@sfuhK;zLEz*j?yC^G$_;|0pNY)#XaarffUi{oD2jr-}l{- z*=t<#*?WkMnMG15?YO_5ksmikRTrP<#{k^8&5Moer} zt~*NN;!scVx1>IpY?0<$93Uj!a=h zACpkPWi^*^oEf8AwRa?3!v!DA>YEQh?d}_SJ>kbVa`?Jad*r(matfKi{o5seGAjF< z(w#eZpv~t^X)*&oI)mUO`?l7x=wLH3%1US0@zFzl`5cunJ@YA64V`!Q7d-DX)12li zBW1xzQuYkTEMwQArrNowzT7J=n}1x_V&g&)L@As(NU^7>9G{1$=T%@ie*E|W%bx13 z{s^?_@|>1M1kfg7<7K^<7XDd_OGx+&A-^Wodm0-^ciXoSmoQ3sT7YP>L}89IwAVYG z`nLO`i<$PbYu3#X@!>{nG|}MHO+ZdCp@whG%2sP`-gx3d`;Q+N;db0oyFUT(d`TH& zZ;d8ftkz@)G!tQPv>!V@e{i+*AQPuyE7g#OLIl&`mu^Pd^8R?eC>xfX+?zJF`9`;c8)z3&e`HH6aSRmu!HJ;E9RN z$=&;sM#jxp5(z}FrC&Q}VMcrR?p;`3o~Nz&L-P9)Ru~-zxSH8v-`%o52RgvT)4y|w zkK@|4h?<<45~#%>@3vE0=gpW)qzb`Q#zY1egCXH)mu+vmwAed1oOW>`hKHjdTS7$p zE(O~LFcBnlmC%J0o$%*4t)Jt2!y5Uwf;4O+ZpTdg#2#+j054Y+EksihI)Ti%^UCH! z)Q(;gl-Yio^J+1OyNlT|n!-Zg#L`p>2Ev**rbm6waXXiIC_Yj0f}Qb~DXhOg*B zt{L?EeEIqxzikeb(XM`Hgpl^C`Apy=$msJUEnC;#btgllhd2tTSm7F~TUq^*z8|9YnP}-g5@nR_KD#)}#bC0V~uB=-%Id zhqktM`EFC4P^7^FuZBL#f;W#|Z#r;k#aP<;eu7*4Qdn@c)8jqC4X|IxH6A4TlPAz~ zUT5Pu>X<&S6vg^>3Zvn}*8Ut{AwuROsVOiABcboPW>m@26CduG$??I1eToNCi)({Bgzx@RoQ2ge5`buZWdjh@rH)bIlNd z5d#mDipo-p?S&U@=Aof`043Sl*$vO}UOPtGoLh-HYDRplW(|!8)0s>HvEhZ`8F78v zmAc$ea(CC}@Amfen89Vxu0cf@nd1e|+3zJy^kLmOg+1^{7m`Z`Nf_4NQ_mK{L+M){ zYApGLo=^;s#>a&rzy5g|#DpRzkbl`jYa8|*YT%k_A_rt;9Jq%14&if;7umIh>g>|z z7C*j}>yGRZ!0V!;*D37%k>OF~mM6nFLBWickN^HD4GsxzFsmGErgslSc6B)b7{5wgdi4c|KyRO$mzSND zg-U5kZrasBMj(B^(5c*`26kX$ITYX0P#65tu=8sV`;)9I+n%F3LAf`-Y!5q&W$gVhUBnwX+v z?3qI)>(m2pAe)hwW9Exi%c0pIXcL%V$xqLFX=wWx+hI#q`l^nynVT<4|DAK|3 z*l^ckQK|T-m6fZSEGF7$;6#BAN^Tnc)#|BwbTi6^io~EZ-6D)OW*vv?$O0}eVQGuk z1kf&|Wz>)|vJf4n2!ZJ(;9~wiJ9k&1>6xvT1ItusWL?jlJJ+|20$L1)1lv4RVxWHrXmG!wtHy4mDo2H&8^PT%ML?lW>+@7?9T|xfNOTbtjMLb zD<>z1?AoZ$>gjLdHKX3R3t9KdtOSZx8ZiQ<95^f*d%ZT)NPz71#G@(8Sk?^c(deMjRGE zH1gk`2u8u&rRc<~@+72$54rs!JMuVVXeNRM(&LxeC*mkh3weH7Np6ALg0Fir|26JK z+-QO~3y-3iGO@SsKT4&d&Y_C>b|7|n$nDK_&{Ha6iELTvWHD+p1vH+Hi>6-`Bm;v7$TWED$7 z2e&Ez_eQ+sh!5Ch1a+-1H6cmuls?=49;8v^x@3kk7*O;_xpe)ZCK|67hd-YJK0hqt z1kC*8+u7~U61=sL;>|VcVnaNDY_k0H!q40YbaY36K*<&4DnYTbiz(d zuNRlP5=Aj$bTkL}!*VFA44C=`a$pmsf*^B$ZvM&sjn(-f$8LlrLOcM^4YZF3$;qZM zn@y&T_(*&x`vB{@3tzvE_Cx^j1F`s`t-O#0@7#CYlm~ym5($uLx$!^9k(FSJS~4*V zZ}zBP=b_E6TuEWN*23;cNj5q6ds<2>zeww%2$Mg4nX3rLGM1YSa!6%$rhfLnZ;K4c zHb!~Q%>YN_papI{MJY*gbI^93FY7i#5<0kvN}r20^T0hY|96)sFePSADxN>T`SNAN zvbeBz1vcS2r%UPt3vr(XldLg0F@`wGW|040vR)_4wWx0%SZIj40hj8ck-C_RSE}#p zDWE%F@`28qY4Gct^gc=9h^wosq3Q_WN)Y{k2Foqj3sFD^%H6-@_!h+h_dS=(vR!13 zT}4I3ud)ZKhh$uYop|(Pn6ZTzI&Vjl_XjEvZ84cA_pys?%e&em%I+kU=VG`U0Tw=apiN&>+Y{AvR_&dbJ zCiF(~56#DOvUnr;IXRid#diTs0;&H6GAMuSJ|>+F+AJ%jwwqy>Rnk0>m{G#!g*V6n zUEZ938 zN<#ww76_+KZ9a8Mm-(Q?kHTb)v|t3U^-1D50n;@Ag2)7;!lt)cT=)kZwV zP>4Ya@{|o6@pQ%+OfP1Pa}f*t!tk&MbPmg*YtnPS(EnupQAm0^VsizRbf~{$V~eM_ z(S%5JSj|sBAm6{6007l-G>&CG7y)=1I3RdQmL9pmgJWTNYD9%Mt8D9usj``+-%d88 zpB#>nGzg5ruyIdNSDET6ka%rJ%S5Z7+$>%q8ajvtGc;gD`@0xA!V~Nhfgokj3;37N z&IkvE-N(V&@kNhrtf*-@GG+Rb4G-zmoCn#c*6EV0bR`$%b^(wXT%ydJW&(M4_h%3* z0RTWjp9J!vC*ShLXG6)?Zj#b{7Nw&JYXAT0*QjZcvD#O5s$iwpZ{L1zZ+|`A#z0^y z2r8)}Xrd-_Ro(iKRB_rOR7+$%3)T7jK^LG|&Nmf8v;Z>#szmc;qnC72Ma5?j-56%5 zceizX^1)e+jTwnD9#m{9a@$y|fh2|=B%6^5Jf3qsZ z@#=IiV)F=RSTl$IbCuJ&dfYT~vUQFkG;kwut)x+y0QMU6|^!?2`3mG-E41gN{yR0>Y1$b zS2RO6(9N4BF)2hC$AkDlhpH{YQtP5&&ER1z?P}hi`b7eFLrlKFY;@x2(r_JI3hfcrY2JvoL53+Wpwul5j9kjM%DPw>#YfGnwZ$;RH&P7vn6LU()E<k9 znVv$F9M*U5c2nRVn$B%i)J%aFC7^i350(Hg>w3MlhJS6MeEb3dImT!aJhN&fYUIpG zYzJiLu|A@bnTXl+4oBn3$ueT8buDJLuQ00Hym|9Z9i7i(MmPTcb%#FF-<+YIcxtsz z30W9(md@QoRJgsKk0*=KFNh3QN|uSC_2I*tMAV&zU^?(n@1n_8`ZK?2Cv^}LmIp9C zS}l>%o-ADBBDC7LZZ>S=-;X<6zdp@QoCg3)2#huD|M;n(LI)2?QdiwqLRXj3QWlBu043>Z&V z2F8MDC3>(=_;+=1JhXoPG)3=ISyLBdvI|IvcbGGRTUMcPOzik)Zni-=Rvf03+SfOzUxn( zf)EIT5at&<_Ut(<&)+z_;GRTv-um<)qfmmo2eF@{GS=;K+UN`>4sC>{T_LBL7@amV zV}TUTl;)i8D7Xltxvpu_CTLDUkqI-iOJZvVdxx_Lr{{-^s#llq2_o+I=<7vl!g(xw zoSeZ%*9)GxdbYGnFK9+aI*cMK9WsNuaNz=QYNdTqPoBZ53Y}X($oO?=NIuI-DZLGo z^MkOxnNYDbt*DH=Uh{2a3yWPJZAt{%rZD1B1fjFNcNE`rc}p6Gqyo)Kj5l1p1zHXu z9JX9$TmS(&lM^g1)&x0k>7LLBzxOf5su|<6^J__`*QA+2S41d!vTtPxF%n}`D27@A zpk!82rm%+;pHj7BSbotgQ+GrDvbVzGpuR=ucwC@Z_X|L*t)edLKajV?MY!V@YIj7vQWADxfqg>K>mJ2-P28nGvR5Jku zl2kDw^JeQc{#o=Up#@81B%FuH5Y^+d|MD&qz&})(bH2VKpwSJPsbzz5dU9#S(-;tN zKtK{x=c=up{AxCy$KI4>;!vm8;`Ct86r+S=okWcMUVG!^s5EO_=Ge+~wj_=F^mi}k z!2tbTffJ&S9z8l)5perp%Jy%+e{ZWb;siCQ#;%q7P|aq6kp20y#NiWW=E0Za4E;x> zA7g!%X*!H^Ob@a0en-UQs0FiGQx_zx!Wjm5)z(`al;`;ivHy-Sz>@ERX62YU6NcS@ zkD{x6??ZR(R25VUqa=v3uAlT2B8!&u+TPVyguL0pYpc$wTAUkg<%B|@?OvzDw4D7b zGaX`6zG$CpGy|0>o|B1-sOG!#!2}3}kMYtk;8@1eQlP%RzFr8WsGaJcS+;DLo!z!O zcqt}D-MIt=OwFU3{hPX8(X-{t=%Zg5$9fMlQvQyuyLf_xj%|N|G0P%{ZDv6%!C8!v zwu2t3?d3krs&AD`6Sp1f6=tL*^NU$|ly;r<_I~>6RV-+5m`@8`eVxBLPac`q)Y7Vh z3IBE~J!N3Ojxwft(Q=x)Tca&S(+GasSqCZ%Q3-+ohH%5LF1EHG<0e_j=gcnC|#T^#w? zQ@I?px}d#f%a)k9IDReo3AJ@d>9r0iMa zIeeu<%9thAtlh9jJL&WG%C$FFGmn2SpiG`DdUAB6qf7F=w*DvSfgZBAF$*NVF)Xh;=OY3np zw2wt4VZ@}1e&(0i(&bFGtpSf1P_6*qURs)P0N_9d*MLNqnZJ4^+~CU8+`n&1upc1e zc)2~>>i1*+uBOC}kfvxIu>^fea97o`GY?(a5j?Iu@%&ypYwb(yCi>9ALU`T3FAS29 zG`%B_^gsNmj{;#u;xWc{KSr3eZ|@h4-Mhaq-^_EPHn*u&wrNNwR^bE%`sKc-jE#OC zo!-0;%6b&{<^jN`%o)GUizDBD{jva77E3?HECy079BXT9TwGj3uB!sb*2d69Ur=ZM zAoWt6H+GAQz=L)S_)TVZ1Y{y_M`QYBaWP+I&-QRa3bax&F)83kn*-do? z(Iq4p-(S(2el8nPG#0STV5hA|X&@==2QfE{YRqFv_}`TsO`VM= zsMtMwdOV!Hy1s*)cx6Rll}a6*p7!+c*xKFBVAa?+JQx-$z=-Ll2aNS~uJ|sjsRysi z<0DHNyxt51=$|`#wtwtL9y4JuVHoJT{wFcsXJ&Te>B=$!+N_xw4KP|&$VwvY$NNY; zO1+UZ>zmvR{lN&M2Dg!swCY`pt$-PX7jXIVW!N=j+zh|JpM_x2?{iyBnM1Hr&ukbA z?af&j08>G%P}O35nozGtKYW$vB$Lvaiq+k&X@5vS0I7~14om% zxVg1JaZjP>t(Rthb4X}=7*aZg^yM4l&id=AG6`B5P!|FYWGeekor>+A^l%|J#!Bew zUEBHo^XE-dzgI2_i||zW*L0-k-76{TW}>tG@sGdv^|e5mEA5$MD}P3hJ;ex1yv%S< zLJquIyMr*BCQf+U;1RZVI@Z?TV(R~pNAt;l{61o8K+SjpnDEP&x1TJ~2TmY}UiBdF zIAd>b)vgBLwVw*5&NquNaGpg|YcA23w055tAaT>3meNKond`5s zsW-mVn{aRFBVKgTq5O!}jt6@{HyKrLY-VOQ@V40_yQGAFLq)0Ms3z5t;c{-&fU@=~ zvfjs4bCUm6&53Q9gORg3_t5hG|EV7@$%S6DNU?pt&E(j_g&LWbx}M34EJ zdiG7$R|yNWzLYnF!|a?y%WzBnzCA%MON*&3{ryJSZ|&Y{PKvk|bJ+5-1$)POYq6+io&7dLl~;Fc zj~%hT5FA%}iL3Ijnq3ozwPR93^O2_mi2bSeu6AT&vm2W#_e3{))Q-kj9}}wcIgaWB zyZRJ1_>`R~-L+wKYz+zLzw1%%|pXc8TZ0Bp>0OSw*Cw6khO6N@-FbjS6=_$M) zOt@5q>@PMjgwrin zYkD(W?bf^$Joq!$HOc0Xf22&Hm;GNEPRY{`^uIfv_UV@q>^PSkDG+Xy zEHsw-hqt!=^84+&TFbq`Uo!N~GT#RB^ogC;&0U~gEXYkT6gvB-k6w9xVUN!i(Vo6X zlbYMu@2Uw(gI~>Ds}5QDoASHzN<7TGS>r$6o2<`At>t7sI+qaFz*9Zvj2aGiXm2O) zO3Im;n|eo0_BxpQ^R1eU8l01>YpAupyP5O^-xfdPo^v zW-ZdR+GTJ6-|{QdQqEy33ZM$;np);TVRF7QyloiQNF)(}U~6d<&3F|Q0AXP+>V5w0 zDDSO;UccXb)PMi5OMO4DJjFM>O8rCAi;S>_{(c*#)6}djIl@W1l1+4L7wns(EjpB_ zt7TIp(~?xYOrG2#gIP1jeyB39kunVV1S-wa&2yNS!$x;FhSG|T_4Mu4 zq68-tFkUCcB02ddwW$1X*^&YPSs3Q-&X^3#L^H5m)5{&T&W>x>peFMD*qgbT5zs-x zGW6VS(6>IQ%oBK-X9rXb88q(f?7@)P40d%2w_nGnP>l|IL|S^07aa9OTB=!Sl3JkK54U(1j?%P&(m=CQuV#OTCSTFkBR9Ta7L^^ zP9RS+tJgyuop}txB+Fact1N;vMbTv?Skl5K7Ze@oZ?n9yNW#pJNTlml;@$|7)lp_D zxo{yC?0(CcceAKLraA9Rq zF`!Zd;R92{rB0$HrlqAphadus!gJE)r=#j;KJP<1pV+a}O}^O}|NMG3!bQBgdKomD zPe>%AKQo?3XL#m)ei01A{#B56uDP3c;Sj6AE2>YqWCWFsRf_Lj{%p&8=+Au1G#Eq> z4iXH2Y8J|K>AQDlJ!YKuvaCLeAm!6`JoUzsEuIOfzuDQZNj-}Fv-yd=JcUysykOZV zbalPbfHs)fS-_m__J@+VJmY@l%Cb3U88Mnj!gEp;n6t?1vS!`Yuv_ZQHG;-LQ8*#{ z9lc}QaKo84bWn=y4x^>QEi^SX?>0#htn%hObB;N@qDYJv7p9x`MhABx#IrugHs;%D3+t3S{2ez2!o!s0aL4*z5ZtzQ2@;Ie34P&L~YJIth;NY*V``Dk=FpX%6%o0ZM&a<;XaV z!^2xbPnVXFG%4dZNoDfq>G~Av-Tn++_btfpyS$32O#eJBaN0YOE)pFF=jULG0k2>u zU@WF+2Cxipfa&Ky_r-J2=iQvA*P^-CVG+6miYB;Z{hycv0B4@_oO#d-h7LH zx^^WEW_mvpPURDCJdulL$qO-LH;X7(jE|}E02MnkUZ4_yj>&&=E*|AeylC71^;xg< zzRRoQxnWX?y>w0`+5X!`HCV=}=iu_+EQ-;>(a{<@T~u{JmMc@gLDw9|Y&qd3Ah-k~ z=i|uy--aqC5-@|1Mm0LnDS36rSwNYsblxaDz(Mvf#@M&w=Q9`XZ z95E{eb?QCELOAKfTo*LP;Gn5ZGSabZL1sQwYV2ZIb#u!>Ey|1Mv<}{T84=bxXtMa_ z)KK<_BKG2E!e5w&!N$kO0lwY@l`T`k0(cfwwQ$sUWZ)m0ieEg&X6WsQ7W--4z*Y5o zT(pMVu&~6X8h#olGceEQdpkwLwCXDpz@?>KeQZSd3O4}1h3r{UQDGsU=x_IVuTNgs zf#)<>%$d>*rxo=qZ1}gHmVcA#wS*Mq^c)AFC&~wslhea1sl+_IT;|9nfq7 zHxaZnAqLDN_Ig^w#Fft2W2A3weU>F%mTU~O#9Bh^LUY)EYq3}<5{kxMWC$!pgNCOV z*qXtQ0jJHO7|Ez3fWY-eg%Hc}btQk*W_(RbhmG-i*I>Tv-Ce z`{qD>vej-F#&E=P)B3^;zCv0K0&xzULjm#@h_^ZOWj5N~2gEk;9+&Mal%H88_&y6h+|e|+ss4*6lr zOK08H(If^(V={@GcI{3^JN57rs1t%rqUCr%f5u}c7Z4_adVk;k{Y;e*oWusMCj@Pj z^J7I%MIi%jFg@w)>HnF~<@ax&(V(yqEQR@cVl7Gwnd6=-nyhqSo8Ctg;LLJ)d3g_x zW@!6}5dQlC?RBDeMPtU*X6Je~dCeEpIK>qc%e~&xB8~L3{ z>4oEEYLa~)fPe*#8S^{$)r3|`gd5>6eIQvZg&VzTRKNVAP-5-Ac{K5uPRFXvsWo{@ zx+!cyJFgvy95D9Iy>SQ*_KAQf`O_yy(7*)E4{!QUzz?)89EAgxZ-tdEPsd8XM zg3|V5_3~$DHiCM(0qwec6AM4(4iREnGw;huk!au}>jGhB@(~X00i+3;Z{7)HA(JJ9 zfe|pX4ku53Da&>iFiE3)$&9%8_R(4cIpf#Rh+=uQB^!Y!J)P$E!}&cVKAZ(RUPmF| zmIAbdAd*spSZ7W|;9P!P9U8_Xk@b~FD}LjDER9&#>a~T^8Zt8~p0$WlEep(euy$_$Gz?AP_ zs|HZeb7GQewYc^d$e8c6Yaa?X(_$Wh^Ns66J)m|z9P7ADFZweZ)1n|K>n@=E41_Rs zPW^tSlzKwYT_dWeVo{e}TAG7P;L;}MM;medAiYeWu~4l z|L3lK`-bi9;TU>(`7W>#Q#=I6ec=qY#Aez9C6!i*<5by82j?X)q>Ba_nllo|*MI>Q zB6#)d7EsP8wi8aBhrJTH$SIjvBGbbpY-vGkHG?)!r3F6q;lF7;8;>3Hb3ZO#)V$l!kw*oT5n|Zq(=n^9 zX=~-Qwfy^XR>Y(aHne)#@cq(a=-}}){T(D}t=;#rbggHQB0mnhq_IA*-wDvmxbqCu z!xx(toJ0Pcxo|;^ud~|8*Y#K-rl#dZn5PJBsV!^yC)K(jq#;wnZ6s6rVEKjJF{i-4 zeGko#|ERk7!Ix{bBDX~^t3~h0aWp~9MhETE%!)ZVBw(n!etS&VWJlP&*jVOgwD3sf z2v&1X!5otBcc@{XK^=TRi=f;io<JL57oadbT+~;1d>pI(D2km-z z#ktEi8UuLHm(KszUc3A7=8u_w<=6N|!)TqiXwuOpq`>9(u+DcHPsYFZj{;VR)F}S! z?`G#&rHSPZGgV$^V7_tl1}L&-m;W^EPWvlf`$7;9A(ave8(-_HUP~(~n!k1L-ZaW- zKqG~sSAW#+);HVl_z;hJo$QIHq^YV$Rog>KSkfAs?+|8#A%enOkc*VQhN3xR?7!C! z468drM@4699?fj3s!~4n?=Rn@{Z0p=5dZJrzi*QF@o$r6d6Zc}=G&frbQmrYn9TPY zT+cJzlp7qJ$us-Mex_gPoJOZq|5U*jacu}dsA3N`pv*~8J#@ao76S-;O8`0C*>;Od z6oW$A;z0?!ueGJ+&FJcr38{n05Hk1Z2%gh^(b}ic7@T%2s@#eF`N0V5`@cAH@D$*K zs*+w9=p_D~?DY(i>lr!$=Z{Iol7qY6zlhae6B$<%s}M{w4g}5^o(WMU|7ygdLFIV_ zQC_*y73vubpLhRsHJaw8|4n{Ad8I;oXV)!9(bSyAse60JVjrA2#Ma54iZko0bBGAJ zZ{_TF;i%X++Us+7+?`#=rtKJbg`~(lk=3#5?2LWBS8px8JBa7ds~?RhJbOd^HtH3m zUp`+pGz?hL(*2KG=hw#EfHnu9m$di(3Z2^VVA(=RN$i5$@p6D70pDiTyUE$OT-8D- zVE(ASUVKZLut*6!H8-`b@kepp)?~dMy6>!VhvU`$ou7}8{x&|={K+6QqpMtddVX&M)XM>W3bIo+6=ybi zYDV%Nl5E-=A>B& zS?xLKGNRG^nOj5!MHNV6k%r0T%OVRpBzwJ}=I2d+<-emf=t29A=^+Q9L-cLpqj4Y3 z^zNA6BS9elmWT-MyIPhr-fvYeUX)cyWM*HCiQ$*Iyr&S*gvY-2{HTcFb#vZoSC|3- zgKEu?N3V^5w)q3_Nq}yK(=-C=l0F=n`GK_H>Vc=3hx3=Nwlv-LdwfivN+E$B%Nqzk zC#U)!fuP2s#EDlo=P6IAaT3wq3JV0Ai*+_mm%RfrwMV?3lzc8vlN|V%^H;7%V55V) z98+HAvPYqsu_7R=fyP_jS?6G9cUhPW<;!Tn@Q=OL=fiH?7#f+IE`;bKO?%o)NpHYY z_^ZW)cZ4dLxfG8&qp--G(;f2$CU4TxBrK;3kYB#qhGZ?18NbOP~V)ouuaG4xgU-HDbT# zJ0fLHx;gvX(WBDFoK()qzGe%J{QK>N@~lneBbzn{PbEf0MKx_T67V4$#%>m>8V9 zguMszFr$XJ|H_s#_FgBSk~mctLl1r2Uvey%f6}-@Q4}jI+qUO6HQ7RU#Ww5f5o!22 zUF}@aSH_w9=63&L4#EKm?Ln#H_W|Y8t2$-}1A6=W5Bd3RxOF*xWie(2OsyDQF1$&F zEvf8~7Y^b|gv{)}4m-zs`zDrhqV%(@4UP<$bHpuEq=r8fj;#1KZ!I^!F~RTWKD?Ug zzK4HyuladtuIG}^P-+U4?Caaw-a$G7aU#UaO$9W4AL90<5+Lgm_kr*5E2_Sq*)@FZ z-jT-ULAg$WY7@y&`m@6vbDPEKzc31w}b>~=Qz;5=nzwLwSc zL?;`INb>jZ%SZ@jr^w$WM_x=1FWENSMo+t=?Pykz6kr%H+bLjb+8Y`!d)|H~XsfWV zJ4K@VsPZKLjb}8i&?~~yqCaG%ilYx6i1YY}_sDi!s&b?8Y`YS|kJ#AQpawnku|R<0 z)bJ0}5>J|z4?c-3fswcCX5LSHv_yMIxc*h3eIYJ9?{{J}W)~~{V3K)e`+z{JS|$Dc zHaEdFc`T2dq$7j2PA8LGEfsOK9Kp%G?2ebvUg`JYL_fbgu~yaR^GL!Bqif#r@p7Ei z=E11ElwhSH))a0PKoS85e*50NXkccbE(TN+KaTP2qeU~#6yvfZ%GZV*Ud?(+>N^r# zh17MZ$+Y7k#xCzks-GnuJlOgEMf|KW;S^TIJ#fEDb=vZLmcgmDfjTWauTZd`CWp3H zmOsVD4Nw0$J2EzO?cpxKS3e}+W>VN2I!>zicCy#!u6PmWK3IOhvrn>N*RV^~-{TW) zYyGrQ7Z;jB$ff|chq^U@qGISg%JOMD$*CV{6^u4jI1S<|a?_EH4mfjZb_7~Umk z4ZT5`ZsBFWeYQf6q44QU$8b=(ME{_=T`(E2XF4PB!J zWy092f-m|!nfS2-xljFjg@(5}il18c_MvgZ0(+Qod^j}Y$(TS;NM=OI8wG2!bN9P^ z{~a&2PW!GDORvnk>e#81UWi4Y1n6xqus}rpUn~zjGZ;|WyP|E@S{k^;UgG!vJ&=^+S%TLp z-Fs1y1mp;aYEkD>N2j{AP_%Sni_pK1Pmir18(2E~>Re#i85X9EH0Ee^wAFKW>B)Y1 zXe8M=IoSZ>6VP^uSqS=FS%s%_O||&6(=Bm@vW1r0uDlQ1LwI;)fUF*KXv~dd90sL6 zDn+g;F*NxnCQ0i`uWi@;JMj`b|Med8o;XpK^WddVTi8{F6Rld)%j9gnbFUVV zku^}d@bVJ%JJVt`A^qoI?;qk>>1IxZZAuIK@`*0iv4IP(I|_CU_uejDl=s;QFd^Vw zXfzK)8yooUO)oG~^A}3p85?a*3`{` z;niIhI&Q_pyIaPHvA4s^R5g}IG4o~V6>i5>$l=CDfLWwS{<*fwm>;8-$ER|8hd%ZS znI7eGXZzd^L#t(&BClhYG7M^u+I5E|u(vy66l3I$gW3e-L{OevknfOzyl@{B#Ww|f zdKNq_=ArL=Lu=SCQLVRS2UK zdQx;F-&Blw1dEOsO;NH={Oofz7?rB>T!)rlcbam-TUS*zg?iN6F5{WHYvPq~M(_;z zrV)SGj+mbv`v9&2XhBG7kf`A9-QNL!1#Ei)07E5iuk|zqX&fZ62Y?=6*a3+X$tt~l z`?l#9jUH;~PCu9@{ltpqj*qpuqbJoVr-X}bBN~}7^Q z_3P{X5yOPG9gP!Mi6|pI5qh-dxj??JfIBvz*<4%yji?|Q2hg=sGm{#Sv}mMG0zePy zA7%*aer?xi&kH6%n;bxd<<6AEsH9BXWN=M=Qk&cdr}cw6s2}K-hk% zu)heffp2TLVMoPi`c~g`wNI)UG^wW}>x(3z`R81?kSZ-@qGzcx&hcoJGLyOAZc{s|a{8!k-pGLL3Z|91qmj zAmcEf=uc8y-D5nfuabXKo4w86F?UTW_9c=tYdP8&XYsASLa_>y_IAWeVX6P$r7ia_ z(*?pFG3SMoMAT1yrU8laz=5zw<#RV^R2o&dGeXFq6YB88zi0;(5NK<1V7&k^wsz(0 zP!SUVIkYdJ&=ocUtjE}&9b4}Ut6wE3x0FWhXcLLy6D|PWLEg2@Jvi^Nk+PDD`$9Sj zULW$@uGgZxg&uZ)=mYK*du3-`ddX{>wTP(!WiV8-52G;w(2p=@pw?1VE#0BrUtk$y zYw$0*&hV+MCKi4jh1yWlXjZDW)5mVPyAgL!4!e#gcE_h3?JJdKWoU0mu*4d4OX+?2 zD+izEJ<&B+;Q3@{%#hTw8?0uQpZAs65C_6c5SqY*L9{GFikciKlmKdJxn+Tiy{N0Z0;zRSek@SL|BX7B5M17XNKf(UiB)1&e?z}7kzp^{XOcwNYPWxRRXe7OVwEA4 zC6*9mMn2Cc?2jmv$y@Y{FnzDu@T2Kd_*C5dGG%JAF>HFM)`C79=Q(H!Yf`gMn{eoq zz8%h6JpO=%rfGMjk z1Jv}UfQO))w|Wl&6pYgCwu?dMq{D+5zx4vsD3QOA;*D9C5%bF9^f; zyW2_s3SA{?$_5_?46{tYZ$K*K?Hwg3ToZrE@Wp%gwAXOh@)u@7dHUsdj{luz%q>Nc zJocxEyRli!Ca>^eA^OJlWdODfu7(lb;` z5Dk_PL~1M7udfF6;D8%q*e#wP2%gR23jY<+CAo*+?XggV0*B!-2IOP91qc; zv()X|@Oc76B+nuZqxNoy=85bhrDmr~l?jYxrUb&eO)GjqoYEGtZn7?kVdp8jgqb_Z zvbfUyY9MM^1*jl^pLV|+f^^gQ?{Kv=Fs_&B1H+~`wEPluTjXo7Led76{bPf&xchW{ zEX#y>`46sKLE4kvxF|wL+~rfs%9se zLZr2LyOa-qDGb^Mr`B>3}yNS21WIt{RHppe63f^m5TptRbFa_LrC98y62 z%ZGWgMix_QeBIOwNQ?Jy7!yJw;x67UZCHiPNiJT`?i%LwG^_#o{^V?j_peJcm!-9* zgO!v3eg^1=;o(bC;=U#tS7+CTufzSx-eO7~rK1;l{dntpd z5q!`p4psaLetZWmRFsqKFY;X-kPT<1dhbB=*flq6P}p(pN;+&T0RADS0eF`a0L%ui zu>74pVFGNUEBNq%yIv7mcjX(hC+mc8akQ0mvhI)HE*^@qiNY|_1EHO~AxsaFVE4aW z8#7XH!= zoZ+rllt1`C9lE*V0n~b!=KV%#IUBtu^7E%p(16d(&0PZ3MQ`ue)KyBnE-_zLNp!BOG@{gK~KjG4b zA=Xe_ha%SKxQq-{;8@3t*Fqt}ZV+`tn2SW^_Ux(2RN_#5NTe}M-z{`RHAQZWfXaWfJ0d_It`SW)J6L(x7JO@3; zHLGy|07|jGfx}+RC?dWvBKqCpS9ilc7UnD5D9Q>TycuU}(t=<@=S476^jQqHP!(LT zrk-96lxa{%1ne|`ny62gDJ=)eFdh1@No#k4DCHh)n=r9N3%Y>Em^Vb&`|f@T#l!AX zn%k~J)VmzQEHDCYNuJ>_uKa=~|5PZW@!T_zfQME)08YU?LFlohb;!yB>9GRJ0IXo$ zWqyg3E8kbD7UP;J#YVn9@p13luyJzCCTp5tv$cm&0Y>fu`2UvjT6}rTUmo?>vV80X znHYHAVL+S$JAkVhIFapwB!ue>FUgv)zkHE|ZrXKSqMqG#CYeE8R{Mm{=kk6`&z1|A zVLq<(_{%vtQY>zsubI-~QDa(J%?nWcNDy}efM}*P)d~em9rG_dNTZ+v54!Szezb64 z==aS}3ouqDeR=frn>9^0B+@(vo3WRAo=?c$EcMrc8Xd2>sk&V*M;;?C!cKfP8PKZZ%rq z8TlEq>kj=;{s6H!pdTO)dC)o!LUX#$`2Nw)Y1|OG<74EVoGpbVcQhcs_ji5aZ-? z6$u6-2=G|nc9gtq*@73d+Isw%n3jIMMFEAe=9zxIB5p~g_vds%IJ^znxKTYJOpQ4& zB>zDGR3e~204SV8fIx%XcSWp?4`D8_w0rq50b%7fzMOZdqJlhu|NhDWsQZAAkNU?0I^o!F zLP*OESTTjYyt?5x#N+pn@(KgpK`vuM`~Yvg49ya|AR%9LkbhE#U9L4E1C7auhzK+a zXc77T{@{c>@XHqNivSjH>gwtWu9u$0#LMm+DN(8qv;22w9N)Bh79+;T zC{zUzEL7iziRj@1!nYRjxLLsYj(AN_F97b)$FE;wn{PB!m#@^lKpN*9b71s+!1VGe zuKpPsOXX|3of95PO2`TCC=E;y#vWXDQy<0oKi?)Gu=oXWHIU(6um1$AyRA|A)SYY^ zJSYOLG(1n#pmT!h%&yD{gJd{5Ck#Yq5^_K^XMrS!_;#BDmYjQax%Zwzn49*dx$Su; z4Qcvd2BX|{6#W$ziam4Wm+9dLopW;`t&+)jh?<~Q_Y*PI;HSaexs_aXCy~EzBJ%N|70uztniaEO&Q%>>B=v#l4yqgpL#E{+CcqI{6kO`7p@U*-MEjSp+ zX(ddDyW7j;oeV)o9F%!Sa4{4_G_7J_bP)>75iKdA)P64zg)&(8yw9Sy`yI~A!m5`mm+#wNUCnqiN zIvar>jFdKe1_lu61>CT;Z4U3n+qjag6L{U$0U?G8u{b`Poz5jvJnO{25SY1tT^w%c z2%H*K0%2;DOoI^RAv7DnsRxsO24o-@XKj<5aMLHYy!M^ZI2m@25bF+4O;6{-jRMa! zBP69}Dr+ym)fGUwK>HG8MIpd^k3u<+bwveXuq5!V*KL$oI1t{v5L(_`$_#>OdbpLJ z@D`WMy$Y9{hAYNy7&d#L7%LOLl8lMre3rV@|BcEvcTJ>i=$s35s zbbeY3-kyDE#RoM^?$r^J6yE>6wli@z5pzs9>5Dgt5t{4+-q zsp`LFa9Gr*?d+sNi%?Ttk@v46QNjtQwRbmBiX86Z!LmSG5P-FErFYFAfVlsP62F& zEI8qYLL5l{>HUQlL&rifB=v~ALw79FeNd$AM_x?_IcVpDJDK23?m$=!1YDrJ!trJVa$nka(oAwV zaryG!n%GUCM!bxg#f0iJkXnp2@iTRb8B_mnsfuqN!DsJ;0Kle14bmNC_@Kg6@fgq~s4&`JN3y z2Sf%$m59lY4=-VcmMFwJp~+g^dY0YA6xz`pYT7nos?Gx*TO4YCP=~RtC4n*fBNVX1KTY;nY3G+6+i?fYYBquC15IntA@z{>4a1pCF-}9 zPGnV*$;Wnp`T28Y?~{!v-Ag7MyYm^uE$HxHSc(PLe{eIGmJ*ubfztmTU>g!PyE#SXQ#hdN`{}dIh_bRp z$@AwRydezx{#gq_TE5VprVlFG`fJ%2Ye@@NPbAw27hih&90y^~t5>gpdG!~-BT7V< zXejFd&Ghlprzd{ZUib72KMB&?x2&*Za4SL(`hH(jNaTCXUdoZ87%73KIAE-<+S9}& zyuvqa+5|n&w{8bn0*74-b!gdH7<~=;kKI?_NXhd-#g~%BWT?LZ`8D`OMBJsZAM;nq zSwO1-R57e12+zI^dTuie|FWFbWnZ!zd+#|8pb(8h@m#eSMKM36PU#Fnn36;Q(c0Gb zW83MDfr+e49|(5g(j90?ffZRml$L#Gk6jk?#)b*0Y96ToV$*F+{5^6Q)TvNamG5=s zQrPA~|Kse834mKNSE34_n?YJyl3SL6v_WE=jStx;vDOM3bdUv~OlwyJ>}0f}I%#GN zqv7ENRNMtYJV7#f7=YhgrNxC3$x`hKan*gGtmArOiodCpdCL+zmZ?T`1cwxhYSG5| zA|SXnrz(FH%2nPn0tjM=M2+D*L{gaWEDMOZfHj1PU)!=-gPw5A(Se> z%TeJm@}LX<1-aKCmAUa`ALY-VKQN=JQYnm60S7_j z^NSQK=2jUtlafc~dc++u>6FglQ;__-fExkj1E6*Yg0I^(rbBC?Zm3*;e z62JNqadmBiM+Ehd*#FGAJ?N>=mF*S>)NJ7FlonVp~-8#zsHfBBr-ld6xg+QZxpZ`NDlicCKY^dfciZOC}0C! z(&f~tU7q15*t!~dg@p;AYXxqn>Ulc`KCG*f9i)w>gnOtYYVUVk-l_Ax1OpR!32K(d z!6`=C_=t((*D@VS`^k*&-&AP&DP`D7dG1|ci3Z+G=h#p!BK&_LFVU|J4HdVyoGZxZ z&vkbP(>P1hS25Qi47TYOdpkoU>bgbZRHUTT3hi!WqiV=)pPBSACj4ZG?yLLIj=3kp z=s1l>G3f0T_0MtEYl-J=d_n*k7iq(R@FlP9&-K)j1RV$mV0PLuNdpck4DPZBIIz_Y z*dtpS*}`=QkkWup3HZwcMt9P4m6(es-`9X)pN=v>h1ZC|x*rt1DGfE_XI}{3zsd2r zlzL_jK|%^Q0eZ#AZ+1uaW}!(C0)eYMeuKcjF504ac%AuY;o?VSSnlpRmb?dTbna;g zH#4MDeBikDw8U--L@DAJ#K?L7T@!IEc|Z5R76ak|-TDG`xV-&b_@ANd7e*&Wjaj!Fb{L~uM81L->|Kdm)ojpSNyx}6%NR@ zRZtFB2>=SXO+r5>*0P97?^Kvn4Dr%zQtpNEwZ^JWTicj?A1{EKt2ZO~Uf zZM)^ex7qinIeg`7DF$!N)N6i_O7#fG1dW!QTl3KY-h*nM;d#>PPbgFvE4pL=YJCtqg5PApEe9#lVelgE zao3{u-(>!ee*p^iMbHc#b%f1_L#sR>{@;(?`cPTP2F|_fA<=!8;EvHPtz}dYYVoSi zg3!6^?Q59A$2NJ#_%<*2jUVKA)F%=ims-zO!@Y9b3{k@G>h~%7h%*nHFj+#e9dl$q z80gCo8NtyEHzJ|7n7aiafrVdE@d|$}#iq5E0=Y3#1=2j+{MT3o@r9+Y0A!?S78AU? zwe2!*40l>u9@Vl4OeOBzUP4zJ^)=mF*hcUkuCC$u8aoC?bioYaD1dDVdR*{1D(x6< z3Kn|EgKN!DyuAR@NpgbWFceMw?WI_>cHxYLzjguq;^xK^XF)E+(<{8c^>;f)E_fKU z>7Fz1aeqAR%a=WnL-!60{Dk{cz*`I*^N5Q~xAf?SOvRh+c~+ z$8FYFn8aA^3J9BlyX+p+_hO7k@$CgMND(l048F-}c4|OZH0(}@QsKTIg9hQ`C>sv^ zC+_&-D}`n-lSFFKXRehZtRGT0fX^%{DuRtUk)^{m*2wp@_<2%-C3idizX3Am#mK_j z@~$hY<7QOk6S+f%$OUaQ6D1TJU%!eQzqR2AF z!5FI-?F^8si}Do1UDbt3!~v7olf zxRWNPUEw-JMn?qUfcp$!5z-jOToZWp-G>8v3tE^Tzz4z(R{d`cujgZC^f=A?4fA0< z=NhB&Nd1A&8|DJ@Rkh7y1>NbT`{@VS2 z{M8*>x8IPvcTwmD_pU%eWg&FyEErxK<)(#EhJHO3AS2*2+S6d|75=VDYGFn~RIugP zfk2(qMUMsw9+u~sko2Po5iG@8mxGsxTxtff&+0{MgD}hgrTiqyn!o$F1bMt&+$V3+ zs6Nu7T*Ea4tRCxTg*g>1tph{4*g0Riysno02P%dCZ?htkfWq-S}X#h zF9K{7wBKEWJS+ki{EOm>F18_$*u-CpIPD6EH~D$7q$2ETOxV0h?-VYO( zY7nffU9r0nCKLRMi5OKNr86j|W#JcJw%@kG8j?-M|3-iQ{28$F2M!-LhsX`x@qiW< z+8a4GJ`F(&<8RpuJEmi(JE0274C3N&>Is2V2&!L!P8|f5njfRXO#2sI%1|JA2B-e~ znl1b~_ap{@tPcQ(Q7Ol#6LME(`r<~%C{XxQ{C~F9vo0J1)5j*{9v2b*PIwENm$# z3-Nn1p-kkWr;(Q`v$(q%2HA^e05{>Mr&AWf5!3IBqdEgKGL%=n5Y(J|RdIT#>>A9N z-G3{-gj+Zq>`sVTJh=cGj>o>ml zTU`6si_vWR1BGP}3qTtxIywxvH#JT2Esp>FpD2g|?<@oYP>6=i;FqRG{f7VkT20|t zfrX>EwF@q6gF8G>N(rI|#rtxtx2=D_Xo$lH_AEpL5I$*Ws3ViH<}5fo`psl}ZfI%# zg#Xy#!*52L^d}boQo8sT6m-I{(jlA#2}f5w4WGKjtxi!}m{0JQI$$$?D$1hEF&M%R z3lk$pe}CVBmVEG@6HSYwIG$mJEze)DAhfVMcV2sYMVG!AjZn9rG~|d?L|>$d^zsbzt!>qmnKb@rV5V$q#@P z(B7N-;+zn|^ql-504XzQF}Qn(2wHeEH5@;g@&5H|$MiD|LkLV^<)akWl-?^aTp%L< zL+lnwNzG|{>k3eBFf{ZA1%Pn+E<1}7dq40FA~VGeRrc(()&~k?0uBm7AT;PT z-rr$+*c&6QTzs@K5Pw)CZ<{^Aq-Dj7PBiB7aX4|a&Z!aydUG)q6w)s;eE`Q;mQ*Di zbHS_M5~Ryl98#h(#$WRhU*lGo5l?pom#kKY{d>!;OuEmjz{FlV0J4EvRv$}FnB z^N+*jN2YFn5>ztfa4PBcHu)<)xRRnF(IdCW3GY){>mu5pd7)oB;9 zj0n5Ff75$mM}TZ7gc0{Xi}{!w_Il3n+p16_Ezh(Zb?vQ=86^sP=_?b>nDSRV%)R4; zeb=0wI@B0SZxe}|Z`bR@{$fT?YHYDLC$y>n&Fzig+P9;R@>kavQwuV}x2)_dsV)y;=Q&=A|2^TEe+kzOIa)e7J60kft6oEU;-N zHGh?ToF<*`axIVWe7i*>)ktwOptjd39k^nnG$ph<`~~&rHj3-An;#x_B#WF(7juYX zuXbc{;GW%eLtoyIvLfa-KVi*D@3>7!+cei~p()CE9ZyvB$HtSZ98VCId20VqJEK2W zWg?8tyVjGKFu2Xe#_Ufbl12#_?Nbk*3ixdnyhM+_8@rM-Msm26m{eEYXgnYK>4J*+ z)rztCqfUeT8FTYjO?ap&I<#`dL!60>#N)0r2E5CQ*YH0Nw~Hmxl@!(b=HK#Ih)~P0 zU}kw#{t`Bi_*Tz>UyIxp|~pX6MMpLr!7 z^*o?wUV18`I$|ocV4Lry`-Bi9#T8Q>*K$RVYUCR7KA}C57Z3`(t=Q1IYBunB)vwsK znP=LsCScTab;qO_7s@#&uZ)G>JN{>76;Bf8{;y)=ze4MqGmYU!i|SpZ7V7fOLw7?{ zj@~WFSH+sIVH`QALL|`Vor66;6dy~!Nx;a?zd31^xudwhbh>(-3@cA!=c)sa?9ZWz zRg@$`s8NelEVS-1Jx+Q{+TX|5xV!W+RI>V@7dPU`3=rGbEsOD!X6`@UN0T;HqIUH; z&*e?<9P&~&u_s7;v@e*o6((Jp)4A%=nbluaMZ9um>s9$G(`#Z_sm!5gxXyVYa;cYo znD%953*%SgckRV3 h`@bIW@^(4m7Wn^v;0jO4J$L|PVYiz!y*VTGm&Y5$b^PK0o@B4YqoU*suMdT9af*^=!ZMEG2g3#cf zXov$3{`wyM_80gI74ERh0xEqa`5F8N>t}9j4nYquaIbpc!2da^R?guN#M{LFjcNkUCyIjy&wABc9FxlVX`@ZI&b{Eh#{%+)1*-)MWXZ1=8PBK)_u?6|dO-~HVe7609C zhP$0EWRARjE}yBDxx6{^d55=Frkmn(`JIaiC4x~C6E!1avr#LxfpI1yh%p}$-qr7S zeJ#-WlDsweb+72(f=!}?A`UqL{9HAy;0b|5yINp!S8%7A{f0pj_?g-Jl?(!-iyrnlDIQf%V)Disp4CW>Vy zvrD&(?iVr^5e~U;Muih&du89ktAr)-g6t)<0kH=qp6-XB$rvMiYEpfHpg;P4(j~K0 z^Gp9)k&;-r(iRI2Ig5uAqa?;5#%9J8oI0xH#C;X+hj)6&eki=9i8si$QYR?|)Th@IIS^1ZA>w5xyoZ zGcSa0p@L*sm{aHc+9jZlTo;vD}i_4i(19wr+DA z(v6DZbgIe9)XcbLq{5y>5qXN95Q-I%#B-yX@Fggsxg6q}Xun_9Qg%GxIm98Sb@46Ax~UsVk_EutZtUriInQIJ{T##2_j6LPzR-zr4L?@zGk*Cd(_8c*FX-r74H`3;4-Uziph9p+64uKr&F_9Nzd_75kd;1qUBE`u0EaS*qr@Xuf#JJ?>@D z(67q=iqDr%d~NCNpkW9bJ?Dn&d?#OB_Z_GVPVjkoRc)er@`kp`p;Dba*VNZk_L{C$ zWb23C$kq*Xq->Rr`@T=~>P1?%D47C(?k~Udd9XU1BKy7~VBrwvHQmE-OH%O{59qi&ee=#gW+9wUX5jU-pRjy zt%2`(*HFTFt03-Uap18;cJ=-a_U*Bch=fKKY7hgNag3B}(|7T$~NAiJUq5 zzA`vaI+{Ui$VuLE@IJPXXLifShf+92zGH{rW%RT@MS}04wwwwV+m80{C4f(^((xa@ zyn8?ktBU^mk73NuAd2{TscpWz(nkJ2r~3t`je-WNUKhL7y;nV4(cY1*-DY#c$dJrA zc#v@(-ODGQ{OiY5?$LMmvmg1q#E+(~%+`_;uEh*lV+`!pwmRVq-0BSPV(!zZ+LUm- zH}=-$4EAML9IS~@A#)Wfj(&Q5v#}H%C8QTREIBRTk#9O;ldIQxR!qC~``n0WpD`u< zqFn0X$47C?9D}-6+FtXTON!B`v-l2SN;>QdZ^Q5iUPh~GRu~hiRAe{FD%fg47U{{< zm7g<+Y@TVtaGpurdr8A%@*Mg>BlSbY?hV7L&X1anzBUW`K1=3oEJd9&KeItC(R)Wu zqG!wL+eylle;HATBUzf7BUek(Lq9fCpgiE5E;WmeHoGLSk$s6BJhDbx&qZ;JO zLj{r)1=LHf;6P6?l(NSq6raT{`KZ}J&E8U1?n}Y}TtDYVLfxWAapj&ZT-q@|r)3)o z+#BMH;#Pi|yP=3E6#)m;{)|Fe8AcoxDe+OsD^fCuQ#?@|u14z_v5jEEHbMo>1M8?m zy#rgP+n9N0k0s)`JF3FQ65TkaV~{P$T0SqbR^}Q;fR7q4v1)oBW54a1_d&X$C@h{9 zQlLgDg^46`Kt>%7oLbc$ycx&E%D#Ej0@ryN(-61#Cm@QO5#g3X_xoW%EtIBJH&D(njT)VnB6_+pFbna90}SP(FrBFIz*Qt%$O zJ$qV6`BH~HYK3E!PZF~S6@{(Zu!z?|cL=sW=jg_^b6D+LUx433IgF#!^l;s_PwDN7 zJ5^1+Q&5xY5##@H=!psAuyoH?`u#_E*1esXM@X4tXN@e4-6z24FnwitBgFdWQyv+{iYhg?Cb? zFk>)LH?Rk*cB*=VmIpb3xw>RL`$H?0EKem;^%*DTRVnd^7lr%@Ee)wkR9GiQ>YFK= zz1Rm_lOs$-RGJ~XX@Zk#f~w`M40xj`8juG|qEfN-Xg9(ju@AZmXTzC<6*v;MPCN+3 z5Xkz7BbSoAtf2zPw0!)YQCMCXlc1{_?$XC4f`*?~D<8an;At(7mi&AdL$#04=Mb%% zMRHO@M(?=$qh^EAYia6pUO7_EymPXIFphDLFC$jBkFeu{wM*$F1%p?o?vf zE7Fn%RhWKdUIkW2%2OkgjJTYX5xYblPO^Eout0`9+wSQb9Ens5cJN3#jf&jxalX)C z7Ed;uS2Vz7$u0>*^BUob(7a+_xI_gQ?H+ohM+ebgWR-C20Ff?A5yX+Cwt7!=<%fb` zFEuv=*=_J{fVCOAbOPN0CMNgk!*Zyr+?D7E=mGXLCX(-f8_WhKd=Ha7P_E)2lTLWTPAqYd-W2P6Y`$ z3C^nL#UuoHIcSut}=p+O4P zAI;>YZib)J@>JWm&_obz_*{~KZ;&P=)&9ZPV?qq966}xVKu?aob9sgOSL_bE=PMD z*o*H>I{Nl5{{`iw27)_U2UW~RaYM}M7_uQ%T@>RahkSkZ7||pu9FEPB^xw?=D1fhxdl9e7{zS9ipIFFQ z6~9(v7(HK*Z5&e>0HS{P&c4S#cQ%GXD_AE%Mf2Q2E z2Ona4N!c?W=aLr9?G)ir$WAZ_6%+y6m`SfkgnP^6==Q9HcL@mwAg%2PLBJLZ0d^ z)5j2ab;e&@IWuktsbFRyM|^TFyFamQ7#+F zleaCZG~nI+E6PrE+4Hr%NB72KOHq+r6^bw>d3{Ivv>oud#J)|4BmOdWHC?^VH zVV&RZ!!Hv%PXqW}q;t~+BhMGtG-MvUfb*eO-1J(g@Cr(D(^E__WWyzieUIkl^{l1j zl4LO%zUyA3Gxlx#rTGOIMC0raEoa8EVupUgh438wQuZyj?*)v3t!aCZ?Z{_ZkVw!1 zN{GwKa($;cjnXJp(no9~0NL3VDO{sfF*83I7Z@+W;Q3o1e`)k_7#O6_BxaKkMRQ?u z%>pJApt!zSODE~*56!1!mFXT$sfr-~8O*vh7%z!Yu9=C$-M7ppFp{V|G0Fm}PyR0E zE_a^{oQXLop0)G$dQ+@vWlz7I7Sj)5bGD9`E#2~z~oC7kqSsL~W_BXpr+&(n1>3gKf&D%u-z za9Ix?6I5SBBJ`<;Z)c9rm)M$v=PGn`SxJ$&`@B=TLrSMJciM@ zw#a&Z!YG?ZCcg8)-FfXyjgkDX%WotB@7ing`X83pm!7r4fT3U88ZVTZ?!UExwKx>d z?47M!>?+!RY9xF2mH6P(A}owaLgHNZNW`Zo)-o%wY?d{2&PdaW{mnSXAZr?a6WY80%-MY=YJ)^;+kM04u>+1K;_oOwHfl1Z=J9ZB_aAdg*K}=H=%3=>@5&i*8AdQVYS|EA@R2p>u;DZenX+GAXF)dI0UvIq2@8?n#vp8D> zK>h?^hI)Wi6Dt98cMIw%bq%Zx>i7IPI~2%9Ja*T&`V60#lnMutjsg*xCok^ovxoLz zrrtg912-UTY<6mkMLfm?+XI2XvIir^MR!Fz3nTYvYGw!R(Ra=5x%S`@j2LwBJpG_* zVDlLvWltazh*exD0au42bht7oU~AND(665}fjiGj1{B$3dxHGxcO&j+KV^~9b;Gz3 z%2}A~B1B9sjcq^OWRJYmXb4Ir$w+5e_Uev4H%*#PM!N0c5-ul>KH0k}{?Ic&oRu=T zF(hwjyIXSTsXR@0K=})0i zY&+ZdiDI(J1U*^{@!qBL>H+kB!@0-x`rPlqhX{ z61$A=P-a-BD)=vzY;2(@F@E&AlfGFj4YkM$%^QRJVphdXd4M(``>QaDVa|fh9JU;9 zn-L+PMiI22jhP-wg=RE#qn&wYj;`&(L}yZL;=2cj2+vbGr0CWuTKMOa0LQTf2J=V$ zJmT0eJ2+4T(c{?4YoNj~B#X@VKhtZpS)UP*A26fEs#=E{#m)SjxUS6u#}XSBbFK63 zz5Sis8c!*9H;e*?6Ig4q-m;b%l1`t$et)xqsuaa0cC%jKom{8O_MP0`0qmw{m2h0){~Yne?JZr`eS)2NUhhLFPtl(~rK}vhd|W z8Gadae~T4QrM_7juT&!6`Kgs}vbC;IE?@g8ng;D4Wh*5OxmqAj_o2PVS@A!IqGbWz zG}XZrYf;J)fp$SScjl_PtN3^6=*6L^g9rKis>Ys8>;=LRSEOIo%h6vqHhv_@9=FeE znMLPe38-z@V@};pl$Yqo+!>yiQ1fPZv-D4tSI`5lDi!kaPxj@M#zfhDGbK-kxi0lw zR^gK|(4>yFo-?;p%F*nN8f%P#r9^@r)`|BHWgl7tZgdD$!pWh=J%~2gOp%P{ z=$kP6-~P4-c|;fZzy8OZd+dgBOpchgZ&vU?B~Bsb=nlyvRZY*T`2dLFIBZp0Pa@Uh zEc~!e7{neP!ZF$zkKs<*v^sPLcT_}n2MCyK%YUVX1?Wlr!_UB+E~Ej5kK+uwFP=^u z#2G}*=4G2E)Cau1yRV`aYCy}t_85w-S#)^8yel-0zLgF_O$Lf@V?AKImgjTyf{*Sz zCob$=R(|vPGxS3m=e0aRa`ZgIKMG}na>rEnfC42+qkGHT zdqd2Gs3LH=B%W7Nbh@(YRZMb~IK~;nSI>={hQ;7G)N~akf?)IL&w9W1(V>BUyF3F; zD{rq&a^v`qH_H9Gaty;Ku)?T)KXFTX93%{gUQwtZcX$Uy=M(k|dfO(mOmrq7Q6vF> zNYs7o=X=vrml^IzSEI%l5{l%{K5JvVk)*m>o;VLg4bv<`go%Njf|~TxY>_TSxOnC=z6h%G$sLuda^{Hu`s5%2FT@B0;xE= zpz*kClrqVy2dIuj@%MmJ1C`7Cghae`G{W~HTiY+O9(sKA-DzJD*;;S2TIsJ$k0=B~ z(as|6gzUJAiMwfuX0?ztP|66(K~VHf=eEwsJjt%-^@DlcEoufAjh3YN;$^?3Yz!G9&RWc$+>?79Yf{{|4SqwoW1H(6IX5iDBj(@+ z%jnqUz3+Q)s0_HqvEBHOIn7g7w9#(ZJRt+l@7QzZID4#;7?~4XK-nEcIBse(u`3NV zq$e2ahUq|jR?p4GG+2omjU-Rr7(ty7rn(%hn2&i(-8cSo%V?A|uU|!`pd8gRH-&rQ zgjx9f4rD@oP?wRD@hufj3I;->NXLf0ih#^&>S*FOv=M-I0A=zBsJTuWSVY%R@4R?y z5*EyLU8oAxfwDB0ph#ep|Ewj22%mc#Mw@K%CH0}QqMKglNuWZFE*eC?Ix$;4^L&nW z+dk;DMdgm?Kf4%F9m`X(Qsl$s=@1O1%IKT?0ZFLvxB#>3c&cSB+|gOe;_q%MH&((N)F zIer6sHs+Yg)&`QV#SsHFHGO)~uK*f?ink#No@X zOv1YAg+(c6bURU&V!cqN+=<|t(*9eGTWCPO!){753~^GFJyykySko?;{JMM3z7T!B zx+@I3DoTEP`HgtXkdpZ8?n}r-0hhfN1ym^*=BLFW39qrMI*pm$`9t*K0@bc zeS+(TR8Cb2kPo(&EmL#rBB2#9iq;^d-MWk7=zJ6>Jx{zyf=tr4Vp{t@J#vnGVjk-KbD4o?nu4yQOr4lW1V8Ww=BDtYgi6a$ahd-;1w0I47~FK>Js2(B!{<9 zU!S;Oo#@oKqsGW|Rn|h|esOaB&M4cygd@!&F{NkX@m;0^Hw4s=y>|GfT@}20N$NFB!GJml487g~DH z<>*E&bKaWIJtes(02E22!n?8Isn6l!Tj^S28Fh+IN533?5LETAB1S9w$_dp-6R1qX zt&9sJ57Y|jTUjXjz)>AYb=k8=f2?A#XgpVZWT^^SKy5$NS)zL}I;+onbdgWRV6U-| zdR~7c&lL5Mwt8V|OAawm%U?0t_O(*C%dyRg-1@etC|-Y#rJZfp@6ILO8V3sUWmRXb zc1|b2^iRyyEqbGm$?kj(uh#bqDP8(;T6jyPwk%OTs?gu!1o2z_MU)lLT8ykP7^F%( zvq6+a)F^v6aCGCt`KO!3>&mdFlE*o;ExHpm^4^~dlmMhKshj-qS&Wus z4Mpyn(yvubR5Rf8oq!!CRz3mpOHj2XZ zf-}e@Y+b)&xGv_Mo&mx0GEcivMy_nFu1dwx=RdKOOZT4r&Lsg9$cbQwL6X&mY#fS( zj`FG&79lRx^ka(lu# zLR5017XFGxs6zq!5VTEQ-)<&4M`p-*sYdZ9q2 zqq<|UD2RdMt_&m2Lx;?2L;A^ZFfr%>$|>Z9q(JJ$itudcHv+PDA=8YTz*K~en0yjy=YPh9TUF!1JYD_vvItag&HI`ocsDTyWbYV3148Lu zj|}B9)jIkkbJR*_f}qrM6_G7Kga`D&%E6{3@X|nCko%enqekQddH_d~HgYP2gA7i5 zyOnJa=1D%pzIo3u`%<`K-_e{QMQ7t@WVol);5Rn6 zKAjs~a_sz$XEtX3O7KCHfxO8WklB4f8LtQkQH>{#U2mx63y_AT_&cnN7T_ARB0FTT z_u$g*I67LxX@R8cdiZ7|!pS=U<2PGbZLon zggj6g*Lj0#K)Fnbgq3)7V6w1ajel>Xz?Rl{F|H+k>d=Pji(6JmI ztE$xMU#RbxPPi4*_Sf5-g@Y4k(G1g6pX>Ry^nBJmQ41isQI zKgng9Cd5mI=hsRuMP=+)fO<7^34B|nqm}%LIwJo$+@*hhF)SH1hdg&@Yx#7f^Ka@X zv`o|4#L#A!tw9B1!*oW~tA5ud-6~#}K+(~y8@5Vkg`*5)Jmo(@20HiU>8IesZJtW& zvV^>{@`n#yzo^3^r7H@)^J>=?mqAUt^T$+gSV!FkZvl2iK6CS4Jy2q5^jkdf5~Ub6 zj4IVV+ZS;*9CaSPP7wow8(aeuX;tTyNwTef?!+ZUx2|I4s38r_6wXk#k{VjStY}i( zn`o0LCG0E4?Ke_DvkRLb$}89o;bdPJ{P$^75Li~s-ZIgKF8Bxta|Lo4Q?Wz1$m1RN1YoLKIgysdQMJK_5N9rkoVu7o z6F_;X{@LU`&orbA)oU!M4HP%G1q4IFS|%C@lhU-r^|0@2A|9|?9x{|ZZ}p3!X`@^M zvr+=Z1W|hv*C-f9`X^d};a#4${AAw0yW3Kad$jND)QwZhK(8l{wO!C}J1@0gOSZ2z zs%%|Occ~(q)OI=Zu4=d66Utbhi)M*LVkPkhIQe${3BlGPgqBr-s@dGEeFbK;0-X9t z>^nguYtydtS+8&I%4JJ_+6t9$p!3aqy#{=RwE68_FM=R3Xgj31=ji`KTb< zyNPsCvg=E*5z9XtQI@kiy&^{)+NOCk`s)YZ_6)fRgoY8zC-C_wATmfW<8Yg*%gdlW z3}cFJ7S~DIPBU%P2R1tMhUAqMnp??15>qu%o~?0<`Q#of56KSo62ycImR#hltJ_UrSdu`%(?; zpM(stldzTv|2!z}nTN~kS%bBJAg*s#>IWtv1`+nsUNg-3OoZ)E)7GVhj9ZrlUN(O? z%-bFh1;4M*>@r@=s}38(g9wvq!}$LBjW6iuDFSNy(zJ=Y;rs>Q5Y84v_#{f+>W=|- zpA!4ffGUkq^3@3$bUFnyz?JZw9jq<_x+@rT$Z&oy7?dJVbE6Jhb;A~lG%oj|J-*TM z-}rmDC28uArRGggPw)k$n~CZE@}+4v2`w6EI?BG2AOBwG=vxk8kS&B*9$1R|sX*UD zgb1~v!;SZAz@mS$;cn%JdU=b9Q_`o!uBaT+6Wna4vw820X1~Sv8JsS4C*KvjRBL5j z{A*!FV2^HeK+Z|%;|CZ^ zSr4ZNtKYC$IeACv!exvp-ULrE?SrHV7&C2qej$_=qye;?JyT8B;1+c|~AjV*7AoBRC^D`nGD4W4!#`9ZOmA{=AeLei& zyeX5@VgM28b{Q?4Jppdx*Y!DSqvrHL_lzEndjmMz>KCb2L}(J#4M}2i5lMthiZMBW z&*2dLFT`xF7k*Sf7$MWbqt^f`j@GSFn8E))+8)$f5{2xrw@`gN6Nz#K3U|@|kPbf? z)STE_>LmxeY2G%@W2Gy=zx}%rc5rj4qN4JLrYsfd39NW+|8Cj+pdV&^OX)fUvrRO? zzd3TenK>UzVIRo8Cn2udp~FU?V{ML69uwD&<&`v`r1FS)f|A8~kkPIXFFK-<#bFU7 zmMlr`wZ#9I6CLdG_`6y)*$bdTuQK)@*wW%&!FvE zJniDp#Bs2kT`-T?-=umw&q<&X&)$t3W?tkUEA&}!NqQtC@HbhGH$Ng@JzLFoKY3(% zVKO&rBu-@APJBDWC?Py%{sIM^D0>0Q{|;Kf0T1d-mgUi3J;T3#*by69M#?r5ItSNd zT&A3q3h~HQ)GJIAd>{HR?2k83kc%~EFS2m3i$Jcen*)?4w7X1Jib3In3qXrB8wTh~(jggt&tTVH=lkPI zAB`SiM=VdGnZF7p5`930@fULMHq^zlAeY5`9sZy~?A#Dv$BvcjvryQ$8x*=GZp6$2 z$OAkx6y3XZ16QvO@&5!G!IL*ifG4hK05^eAV=-j$5hhe|n-E^hHPk z0C7;S8;T`X^*ppMMj3GaOd>ME$dC>OT=oCr%opBx)cPEq+X6?NF&8d~z3j}@?*uT= zW~!&;>$(*L=MRDuY{97ybCg9p z>yaBZVOo6T$*YQ>egSsemN3D)W7!8|kD-%_2|Cc~`CK%Z=nnzdZTN8KDhUCXG#SK; zoO}E4$R;)%Xw$up?Jgjt)strRI5V6u zRP;3JL8DN&pbqx`oRhGN=1?VPM^zM`2+*DH7vWwR5yHPNm>PYK5b0n7f)Gg=>X z>o}@r^Rtkp1aNC~F%X@bvCw5IQ-S)Khne@A_hB4vvKT$tYHlVu6Dt~ov6+|T+zRIu zV5?;2Yp;3!g@8T>Bnxu~%acTB{fOye?$-x+h2hx3MUqf{N+_C@bcI zBGD)t3^vUnw(PY%#LFHRnwKPFsf_)?$UX#|*nfb`6V2$Nu@?oJyL8ws$AGsWev3I7 z%1_sR4)}up;3DAAY&g}A%zXl>)T5AU%$<*+5Kv(P1v^n~=6``zItleNeKJD~z(ie} z<5M6Z^DQ#xn@E6SDSC4a;@ENnUFE}8cSF7a637J4YN7i?s^s4^n!Kdfo~l_IvssY} zbpuhVbF?W<+l0B-E$+v`1u#u6z&c=8c|J}0w7@m4v_R)*!Ge1QBL3L7Sf#(5#la$o zX90kMD9^+hR{rPlgk-!w5ax=a=7x&^&;gX%kGINlL6~v4;n~B$>|peFRYqP~2)ryE zsh0UaZ@G2Ghtk387gs$sd`dio74`Yw7SMudm_@r_DMsvS1pTxkm$!qy?m94GC~s^L zbWF{Q1XV`H^Q&UU%th=8bk2;E{{tES9>lm10D)1Yzni(c?`B6}SzXag0wQmvXsP#` zd8kUnb8WPRDvTjF%s}IQicmD)TeJf0|IYD_vrG^k|XS1`A3DdP-KeV?+PTT)9W$?B$*H$_i zjY+nkvo5S7ZurqC2;q?_O^;P8u+&Y_XdbkhnGcxDU+T=?n{?~+bBa5zG)7SoR4to2 zvNW6W48!ptahfTHP4%digNm5!<`4BNvrU?dC&G~ph&#IUv5I7=PbHcic!D@7`wIk+ z|58*=)zSqFf61Y0+VccZ*yj=$?)$YNP3s1`bzWG-F$*wYaOzKeFx#Mb4wt|~2BTA= zD$uhw&;a>ArmCoz`1K^4hZlm*K;OZtu*Sg{1=d>1li`yujhIVgMPL@-!lC{1BaAr4 z;aZD_Q(4#1RTwIlY^R$a!Ufr)&Sw6J6t~iQ=e?1YXAuqCiBFy!c@h$Q`u4Jj8=0co!h=Izxygl15HI7~#HzWHZ0$!E?axJa<( zXBz%IhnN7#7+7{KKOlC#k>ER26Hx?UXFU z0BRMgn$65Ac#@zWlw_2^jNM=6W(;6pZ4d*C0Aw4IB2b@NhjoNXJt}c@0ch-R#3}sn zfn`J;+N5b7$_mNCKKE{C_kbQ7e=(Z1vg(r*x@ua1VZ5R6eBwBYQGiyg}g+$xkMN9PonV%0`W`Em&<0qJ#N68^!u$O{3+8VTL%_p0(UuqOcV z`wv0Ma{AduNSb1%bJ_$=4OW*s*PVRKM%TbsNBlNcvJ!6YJlA2oI0zs~_$Nj$TXl0D zr4qf+j46r?Xtw3WCr-d#`PrC%mJCD7(m}d3$j3&U);MY-#ErGl;yGg;TkJuUXEIFXtZyJrH`0X##ph6_fYnU ze|1gUvkO{Z&-B(!-ALz*EjS3UeS65{69GlECrYTg(dhmV-|ggcl<%lYcLIh)DrQNx;mS8_-0HhRKZ=BWle;ebKpO8!g&2{XWqQvJkpfO!qH46~bR ztoNPqag|(n5vUg;$W&#&=aSkMsq^xh2I<9s&|*|m21nGcR9>m0pv#N~?oBq)6noK^ zl@`L1fpQTSGEW8SC@c5BxlZ+NOnW--FvuEJjnR5)DAE7tP= z)?|i1|oqpZo!W`sIm?KZrL(dm%(j! zD{tJq-1T^kB~O3Ddb_64_=oR4Js|KM>oPxfV|tgMDJ!;9`(d4p2kxP4*em2yw6#f<&wttY2tgU`npZ{aDG|&jh z>Fy`bUc<*tRMv8Z(piw)R6 z!pe(%PhS~4yE65{maq3!d?9tJ24E<5f1g+zrHOB?{yJ0Kb{6|l1r?%W_r!BHZgV}!>mlma(m_Sg-0yGKbPTi zXmd*q7lXt7eE7L3QFoES$`Ft}`I?5y{VoVFjVNO>=eLId&fBf_IwHQDwkE+uebigN z(E9>SMjwYN7fpM&0O$B?L)prJ@goCRBzVXj(;Fdq$Y3xnVhU)qq@64AAXypA)~oyk9=omo*uDGyz_U^aMU$(JDG==0*TCi_)aWtm}} zoo)PTfirk$cZ6?6<9D?<~BmnqcX^L4~cTtkoyd9r)eMJ*eNS91jZ%%>U|CKdx=E zO=kYRXW8Pdx|t)Qb7e=Kl#1yEWjR+LyA67u&Pp?H%+KTpU-aw9@;KUCIwP^Nc?4Y_ z%Box#v7<}cV2#Z-6?|-v|LsJ|7t%>5>rXt!#vwOhjI+VD>m?^yf>c+l_qp*(KTGl# zWn^5~ET$SXxSw2^L_1m;Ee?j851BS8l`dSzm=g5iK;vVLx~D@~Hp~H1-*TulBm9mr z{docoM8nMAG4_@Y-xG)G;I5V{oQ~5EO(6Ox*T7e;_^yoaV+Mj=}{ie-)8W24xn`Q6Fc)>)2^BXHyL+=Nj9tU`H>bBXEhKC!Q$3sQ6D{bTNvn?W-gFMXD^ zm0Gexy~?C|H)|(4H7nXSa5mu*pcdxh63`Ikp)gT+#Kj5Im7*$nBWC4iXIg|(?8@kL z_|@*O!R=by^D}uwhozzlJUxJ`EJ-5?bPaj2a7zjd%j>FR8G)xUmR4 zZ}kPh7XV7#(?8mv8x4otPPCjA)qHkY?Z}3klUz*SRWbuIet!GIN5}%b*1}> zkBNUcCM}De+b@Nrg&PE2JUUXpOy^BGGTT7-*(h~->TtX0k|Pki5556HC8$41mPgIG zEihN+9z1a8p}fflNhAj>6j!TKx{!}4UadmAkYYq)H6K0%M7DwNEc!aM!W{V5RN?m# z$#+A1t%Jubmn?V0O7|{rutr*!4V&)(al;pM!TW+XOm@fg+an2Qt4=KVk~f@SAhha$ zUtTfHmHIX9h5T(!XV+?nnP)Ipm1G4 zS?nhF;a^_Za7EA=D|TQnJy}b{Hi1d}I5CrNnGiH#8Z{U+ZF>Am{xCE1cd$xM=M6Ev zw&mcK^snXoM8Q?|^T~x)iO^4tGG5W@8g5Vh*^kAFU4yPq|FSyZGxg+3!Z~A5_r>&v4morxXFHw%3n}_mY@>O^sGS!e&2t z)&%_rt(uEbnM9t=zx37~T0gIxl2LKUO8d?MUP+nv)UT;%8vdR0V$+hz$X4g@S22Fm zeGeN4l*(U^#4WnEERD4Tp)4^c!OprE^h1YKGFjZ)x6WSchUn85j9H{4DI{`j3YUZK zS(}9BQWwu-#J$cFR9vRllIE}$dh1#-mr2kC-?wuYdM6qH)S z=wl8*td@*Q094%%t#T%|k~uTL3ZskACs~qY$-dHr`zw1je}}B*$x+Mv?A6#eGC(GF zANf{Y*EOLn`R1j*L-4|8eC3Ld!VdNWpLJTeR_Bv6*JC=`lpo3pg)J#6PUya=L}QlW z)|!p>y$wQ{>w>(+s!w3P?#gsm7flPW67J#o<(_+XanDmcuCRMvN3XuPqB7~*w-@Z* zGM71x4)Sbib!0brW9ScV4>SKXJg0Y1^p%!+yMEH#f&9Jy@lnAy1?CBF3Y!eTG2JTI zp)oT5bzu9Y!sV@Tzba0AIJMaZxMfoF%Z4?#x!oa8n3gY@eaUBS6I!9jXF;IQWw-if zAmLAY@WtNICKHt@cbkWvor!93ydMHy|8w)6d%_vzL$h1@AcLw0&Nz@-&RZ?M;6RA7QCJ@IK`OR9zG8W3zL{ z694r7npC|sM3JJON{}-1@6XR+-uhke0bgD)*3bcI9IiO6yzd_c>F-kW1#{kC-c1GO z=t>;jdR#Q1CMy3Zo^-=cI_`LOwx(C6bA`W1{9IY*B?S@CH#CCekJXE8|ALgTz+-)} z%6-0eJJnCFwRukEuQ3ffjv9U9&04zis6hxk3jp3V+E4o#w}B30t`Vos1cDI{z8;9C&)?pEj3F%~ke3%&T%pMct*ph&t|cAD zjTq)Puca;)X)#XLf#B$vw>9ELM~?1Goq&Jk^}XV&f3R%%W_nDwF~b;63Ef-=2)kGJ zgpE_6Qq1CxYxjgye$G%{3ul`@^t^3$1=iZJC+EwKJ-NS99^@*fNRvCcAM2@cnNSW3 z`5*lv{K;d>plQEQCrefORg5h&cCxc>F;+@nKZv?b=9=P*qL=cp9Pt+imFXV zi2MIxgzc~Xv}J$oTdyLI&+$cJ*#z4rt{L0wTU>2Azckr&=8V~#6TgsnFS@F$xhbwD z;QjH`fH%H1pa1o_^tnI0=JN=P6S!n1Ky^MK2_E$Q_VFBm(+zEwti1_!Av=bXNeOxf za!eCUN>Aw(Q5so$2EQ}X7+a{upIkpy{OFqG+{Ww=NJyboGWR?2&p{PIh9AnGU>93N z)fRo%`Y(6R@I@rKd%!MTA;>B^4x_k0n*^K(~e zi-2>3&W`eUFD{?8S7B-s&378-#vdO>_r@!@TsZiMskXH3G^1-Q|BA-c7-MlrsyBbG zt+3eo)!4=eMpJ$q^P1WbU-0lma+;&MXQp<3KNV9Y{at;!$5evpKV?c%+K8b_F*b9K z?QeT%wG`(lw@^~tIY3t(gQUE(RTmXg?nKBO;tt_abPLl`JE8 z4ZdE<@!Fr#lJ!w{aNdKO9@UabM=C2AfoRrempVM|r*}tpQUXmay$} zm)@!~_YJ5&cguo=i)o1kjQ>AKMGwxT~kG#-g*n zM~ShGV==~@{?TQfkhjp4YU*XKUr@r*0eqeB1ACx+_`-y3IjHHeRp9Z;!3(h@7gQ;s z?#z4@3x&9`_JF#UAuDLyDvagV$OA^tm<1!Lrtg34)!q>Vfv=m;T)d+Ed%^Ocl0^&$RIi|?VaJ&b z)oP0?x>dy3+_k=wtX^afoD?s*n1DTwp8nyEO1Aq2;%|0%`q8 z1+Y~v`#s*$Wbb4xqRUi4Hjm}wVNycw_qLO)NhB6AH$A(ekGf(WR^Y}8P?`a6c}>#B zGOWmXkJ__XfXXJRCsfBS__eZn%|f)8mYGb0@e4ng7ZTk-CktL^ajzFx`uoZmpHjbw zaQAx_x0JU0cqw(>awCmAkN{pHH<~H7t{T2+e5^i#Gg)omjn!rb|2x8o+-gvRtrIe7 zC5vU8yD{57?Fs`1Y{nNHc^NWs$JuA_&;vf@c9!qZoRRwMd9WV7*rmDmk5$@#mVwt@ zTGC9t&vSfzynT}iTj}8FzJbkji?L3w?6m~T!Ryb8=*%_}-ur_Y zpss-_m^pBfcU3O!>Z^@fl4;u|L5h}Qvb(I`gpMV@I}OqWVt2VNf4`P?OA5MpC&T(25!jP=us&{p{q-ikKwI_&bn$uY(!k*a zlhyy%)tkpdy}tkBvzaln6|!$-EtRrImX=dQi8djFvSf|2WSOz06-g4ZoX}<&lqF(B zRMt}+OUT$O+aQencfE|x`}6&sKl(?*e18AwfH{F32V+l+>J=d)Ar&uaxOWN3 zEao@tddL;b?zlh}ILJ5MJl@+Q@}oDkZtx;1rSaTY#fyj|TR2SKOVmkkluaJ;?W-ZK z)m$oq789YtV9=*NbqN=U3)EhUkYBnu#~7waE;7!P+DM!N3p({O-Qj2}X70g`gPZ0x zx2H~*eAox`_pp5j*=IIM%R39nmcbz(UJ9+)lOw4%E)(yb0>cxYF*YW-!YC$^b0f)G zX^~`!hi%+6je^8Cd7jd-;oHpn!un%Auysx8kypw)p!EV;mhL#Wq#3vq7Gm_mJg}+$ zD{vmo&N>Fz_%QA7OSaB;M(Xjc{AM)kBa^D5{B>#D%aX-yf?Es~W+f%XBEt!RBg4v) z4)eQ!O9gmZdJw*$#2u?WXzrPhCgnZcv;N}Lx4X2LapDfwV9hL zqC;#Sc&fqPJlj^ScK{fVgiezwMV5z*(j&=B;5&S)Y8Q9_h5)V7Lgh11<5WphBG)dG z+1*3CY8Ner>ikz0X{Q-V30!}neV8ujv%`6jWc)TStf}v_%~to!o=qM{UZX|hT!kym zBfxni=G)gL5+wYjo{dHK<*eG%{8mrVuYGWgygL^eKR<70XvQzo$ zDcS|S`1yToSrq9sJ~}D&5=CYC-`-sZz!IU)X(O~6@4?tjipr2UP6q43F@h zny)#QZTvf}G>gTT*?SgNB|o^`w1L-2wG%b}i4Iz}7}7w!B5>(nq*dvTj={HH-!>&q z`Ss)?D*OYlSY(Z3AhS0Txv_D+nKwyLu6hL>e@CVup(q)=JniBbJ}{f3ccorW^?M~( zHqM)3GHd_H#+v58BKEWXd9V*PT4F``*$pmQ+=~MjQ3pSzjjb~KG~*^2Rn4Qp?sIyO zO$XA@d{=HHPo$LQ3r%0gBTO-`PB`Nwc9v_lxeE2 z?aEI5qJutq^2{KD72%(XnZnG8^Hes^dh$+^cj}7%WU%64*abtaKel0|%<#e#yN-F9GX*>+X&=;{yy|1h;iR0%!A_8 zvtaSP?yR26;+Umg*~0F6GB4?-jX7;0Sbgq9xy`538$~J3S7F+ILCM}mEOJUFoo5BG zgVN6iC;ol;5KfI_*%C{YY(1D#lc|17y+~%)SWcz96ZRL@ef}XPd*aOcON6gV#-Vu? z!#MQ=FN=LLr-n2`A=43nc$?#O1rx_B9=8VHliNRm&<`pwrnq8dAYe6LGc z-hGX+ieBO?;qscM^BkYD-)_@7<9&lio|*roN0sJN5jf)LM*d+*c8UixF^fWWnp2A< zUO$6#8}<$?tn-Ga)A`mDFY_ucwdUDKmXComQ2x;dxgmc5X@#8CVLs|u-RDWD>8onK zuG=+ECc0wktN19wE2BT!HnyLIcUKm`m|@%bWt{MZ2I)P|Hk!g57;Ohoi+%3-x0k;A z_1~3BBEzlMVc^5PENIfM>I~-Gv#^7V(L+gwbX}N@)MaXwM_vwY{aqNTM_?*{jP_oX z59_6v1SRy^@chWJxn%_FRCRX|xCib@YjS7mboCV%GNIn@f#5>qj;Dy#%!+Eby8;YC zbSx&tRH%PgbXCv*mM)UE6DE8cmDjV3Eb{|) z9lTE0j57=T{g0sT;^dPw+h#aC8=<7^fONU11Wp;MfW;$S7ip(F0NZDm|Ngixx)Rm! zEBeqKz28;MIKB9p9QI?w$j%{RsB&)D0)%Bxn}@Q@YHz{GPpe?F>>+JAvP z>VRU^V{IGdVvp5}0jXGjW>zIoA*3CefHHPyxNhq>AubC(YRP*(bHcA~*dNf+4Gi;g^e{ z*JJz8SaDubf8B8e!GymUfZ#77OTqOQqxIQ|tv~7|t2Kh&WmK%9psUvHyM6Jz8&=Y1 z!d^?Va6!0#_;h6uLytS5Lr+Ktmq4#8s9+HEiWg^EcfCK!fnEzzO|43R5dkit!IvPP zaNoIwt^p136OY_FcP5Byb3d9H(ZE|SJ~yt=SyEX6)mtJbF7^`ZNb=PB-f3MK8&xGaVYii}2lgggMVkl6*@>DA8e_AjU}V zuJ^`&Mi3Wxi!fK0b+O*&QN)JT_vMS@XBr|l*eJ^%Kd%umSK;h4u5&@%wcQS4J@>5R zsqTR8+mx3RRG(Uvhf13Dl-boYyVc~^hM*sj6=Q`il0CB))y-zQ?$aIwQJM;6FtFj5 z>v)#(DZjpuPr~+-RU({y^**)dSMQH11R$WbG1>yw?30BVuC=bK= z-7~ECb?DmZ;!10Qa9;mSD~yGar(t7@kebAM{6@HaHVE$IjQySPsHc*=N;hcM2yh>q zwzbK%DkVh)Cl^iM460t0WjfLby2Z1?&CxP9cP)iTF0>Ti44h4G{=853_Z((13;u^A zvF9KSSssrKiIlw*#1!h>f@epH-2x7j&Hb#}C2ImsHh#z9gr639nt~go4}K5TJNbOd z+PrXRCT+X|jzXleiL|&OSa1V};CovR8CL|(GXnG~&)38-2rF$>g8CWxOUV+3yI4FQ2?bx+)nEl>i{}+Rg z=ylMWk_ZTC1Z#*l@hf>NS)IPOrlMnYv$E|(8KrZ6w76Mlr5nD5XJJ%=+4Y%+Ts>S= z5k#Nfk!rbkXSncms>!v44;nlTC_dU&O6MYcphghvgUxy>^Va!6Z^w=*?#dHKA(1Hd zScm;*n`aySpv-Cf?~7Am;ks=i_qfyzqx-$*Pp_`hG|-k~Q;jKHe(jDu!?uzOX*@w? zm7QQ@%-N7;uYJe9_gm^do!E!Y!o-hVwjSWGaHnkbn{RP!3^t)@1nK_Px);q8l=LyL zy&}#Bjv7@D-S4HQ1E-M8%5(|uBg(QfbiwsYsE+}ojpbBf>W9W``n)oT}mIN&V%kry*aa^KaBW8;1wV0R;{TofhJ& z*BZP4_~T8dp$pwQ)$d`oeaATqYW;J!3<8IYfv@JVYueCCV&jkSXaR<}5h5u;+D2;f za#N%kb8f3$-Gpb)b;+W?=ruR_3_5zAMC*#EPT97uTJKqL2KF;*WyOerWEA+=x-mB& z-J}UdV@iI2+-OJSK9w08{GDDE%N||EL$usP2SNAf%qX^!hcpD6r{hs{2g_=WD7%q4 z4L!pXq}cp(C!W5S*%?81KiRZ!@A<=-ZhGC6#j4x33(F6mC(QKI4+kI1v|rLt|FjS# zoIDd{@!<8R-}P^PEO~Hb`c3BH_kD^Gb?2M=`9{IagJqEz;2s8aa*>bvYaI#L4 z=Xw=TYsw_x>NZjXH(VUkx?2lP0~qXO*B;&J2S2|wk~KWsSBg(6P7@`L3{)0;N}4OY zmLzPEm+;P_(mOb{4^n%h=^N8F+E=GST?02r*>Ua_`zAJTWsGapBGyxM;N%NO$(2CH zbh_lZux9nLh9<|C)@2Wxn*svcy|q&f14e0et!2htg_a9}bQS+*T}D}G3qe<9ZXQ{7 zmzj=?$eV$^V32R~A}it?`Fpwbi9k`5H~A#s#0P#R#XD8f*0xBqtUUj}tmXiQ=hLj2N{#a^L;gc8^rP*@O}ec2B5%f=wUjVN^>p(?D0$2caTs{omOAt>``E~=!U+8*2b9sD1 z<$kH&2>EfHcUZRoJ&Ln)Whx+z*pOuhJd%1C*MI5@LHz)RGgl?|0^%dy!~H=eCC1Fdm++sG&}$ z_qPP9?C$IJ!+C4J(4Eca{?WH6n5{_o|v?4E)I|z%g0LEGmX3YG`z*C77d6qE+|_U{z}|wA#X3jbNm#C!?JEV4|&5h zR#Pj6m?%bUpz6b2c+#b5BElM7DhdcihXs}1XxYH7m0@mF|6<_cQ#7eEsA6o|1dj3< z+Cv#0MI7`J>VJQNa(ei0jZX6G+OJIu?q(9PlUe$$lF4;^Y$h~-V1-@^f%aqqa4eQT zGI@=j+ox ziOceoj-_WdHX%LeW0y8WV68P@JXlq~J<^jhOzJy# zGnB27TJWH8fn%R>DrVU1O(Mni&Pnczo*ERlhz=7^xDq1VT?Ty5#JsS<9PLI4W5JIE zX?XoF=z2^5Iv!K7Eo~dABh?2hqlf|PEPx`TO?C9R=w5S#Zl*f^%V*6^{}O!8)<z5{e|1zLgil0 z)sg(HWB=|-3Yaj=d7OXcg=+~UC8(lQh;pPf(wx6!X`SMrde9!C2`jE_N*gcOOAX;U z(WA_|=RxWQEw4qT49#vpFsbI^{vO7p#TwK7CuRpMGly_x&my=_5XJ9aB0R4VU3u#| z@oC&$b)yFQZuLWy3AXW_UdSp&l~_Jr@;)hkC=Dv*CGwXNn;t(+j568zXexr7_%+hw zO}y&2E>zQ%1Z`jUmcB=i#S4Fm(p(}2L(7M>>U-2V64}mf``}Jd5^%-62x;j?S&9RT zv0*Bt3tNiZc$R)t+*FC+S3}MAdcc+x^0rG9zx(N?b9m6d{SnLB%(;!+9@0x1yhT12 z!vuZ&?aR)H0BeSv|K&5imUO);50o$0=XdwIO-RZ>T}I5WuUXiUW}6@ZumYiFlSP=V=Y$fngO(kqfhhdd#RhstmoE zpd@heh<@yTNOCaitDA$S9XNojhshWg)sv(KqB<>p_}GaEe)OxZjC^udi~N|pYq6y8 z@%9cj=0vZhXn#o_irc@p^1g{p`@(~)2yG!%{F29TR8=#oIN1CfFLex|_Xv|d#3yI0 z5`BE%rR!G~u0OZgoUSOiDl2L}{}$SCN_&bdi-QF*FvzO~r)bY3_^O8wY-zthnpFd*H^&3W3W8fg;rK zZQqr5-zEzdU(gDBOx7qY>S(+>dG45Jzo}r9+eHh(`=uoj^(S)0nD59ccX1{-E}RL* zmz&x1(GycmFurr)Sjb4StoRUt(fFW0MiBT()jf3G;*}P%guD7}!gdD!Bg36_&CVv9 zbc7D*e+QsHtJ3@dZIWzdN*x1iyw~FrR^Pd#%6%QjhpY2k;?R_&c%@?J}1ieis?ab9p$I(b$9Ua zzOuc)Xi3AXrT#wUw)$P%>X<5zDtD{04#A1Dq^!z+ss~R-<$6Cn$xQ3R-fnc42pG*z z>l*7SogQj=AVzY$Sd^>h=wLa@~f!Jd z3O5HsyDs+uN^39Q9n?O9?WTgt1~H9caZ(Bw%K@)_Jr*EB_pSH; zVNDB5JCNMlQ}xp|yK$)JW43FpT^Uy0y?0AP><;(FJr*w|g;1KGt}4(gA3B8ML#u6^ z#GHhjHaqca@?RJ4b>an@lb!%^<#gr2{kY`Nx^4HPrL=NFa8cGl+*8CUxLwkr`zuw#}SO8;K)^7FoqiV_lEvm!ZvSp)(30 zH3C5Dq<8Jt(#$4;Y|k~Z+~;yn@=y}7$e*5qhL7j*&nZ`cUZSz?f-Jt6|QYa5IcJJPNK ze8+O^d*W(L`F!J@SJtITP zI8+H;25E{U6U}0>h0li?gEoH?4&w{iG z-T%yxehkZn@f9e!-GXTm@*JjQQOdAZj7Rb@d(weiyL~YjVZsDtT$QJo`{n+J2g5z* zE^n{w5tA2FFGgf5enO7g;>{Ij)48VaOs4EcX;m~Z8WD%s zp<7zvsqOx7e{{A)@AA7CiV5!p38L>O&9gV=ID8-aEi!-llxWZ^x9#gA5&cSjW#alyZ|HmUd8HjvgUOxnlZkH@mF1H}56R5-0Cs z%Ioh|HO}lMY2H_q?r*K+Fx#R;OPPaf1@Th14Ky@)LkV5X*T%PRni07Bxfk&8AVir4 zE)H$}+#w%BG*H_e+5>NKy=;%RlP2v~yl?Dwgpw{dz9-@|ks%V7p@Ue`=XTUUW^sb4 zIf6fT^;A+D9#hEcQ8)McdIQyUR^DJI7*E8Ny)OvRrn^66u~ZUG4V)*|UY|oni;TC)K1MG~^*FCq1?pYjAgrFEge{ zipg97ju%NF%+?pt_jz#Pc^VgyIwJED=h`nd#HRf4dfDM@)8439V@jOk8f;Gg-1e`W zo*Nx8(u*6ErAOyzGy7yXTVEGeJU-xg0kf13oYoYi;ul+^FH%V+i0QsBM{#HL?W@^& z-&L%p92L(|Fo9b8S zk8s|h?xlG44Mb8NY*7+;<&RbpxNy|Xhv!1SV*JB`u9F`_%u;~L8lYAFM(KZ1JbXq| z{6tGCqQj;6c*TqMl0c3{SB_$szBJx(>dO$D=U&+fdF~uL7q{6B)Q=%W&Zf_ZEz7c* zkJmbaJ{0zq_WJiJVUC?KzH}aVxvw0w#f+o4+xnJk#zVw%-;XDb3c_spv|sQb$`J}O zU;nk?s57V%K$rE@E5BgiG(V%6@+D3gEv8rwXwe7k*X&GCNo@I_`gXd7?P$fd;8IWo z13xdRr)LXhR3~;7eRJ@Vn%Mmar8vf#iob5iQ5s21e8Lqz)5_o=75}LV-VD0IRnFhx za+GC(vvwc1ZAdu|b_@i?Vc~ur`TM0*wLCYnn{W?=*S9o<>_MBITWiUs+}YSHSYIi$ z;n)izeO67L*aW>~NKdYa)Bw@<7{X;}yb-K5i$yXWlzV_GN0}4aj(c|>+i}=Vfn=}9 z-EFt!`8Hg}D;5M%Sk?ujc})Ge97P=4@pJs9e2TLCv1Inz{yyrB7)X(_JqZURb$)R= zM}+gD*W+PjCyXD@l&&^gg4hQTlHj)DcEI>>54^4AKzHh%mD4v81*Zc9)R+7G?cWIo zumy5j^_<$TU{ykO26&$Zl@x*i&y6Vroik^0t#*Uy??#kzAeBF*hA!_phW+lSt`+kb z>lk(qDBC;C-^CL@;f^>KP=ZX=wSnZR4Ff!W&Ge!1*=jpP`s=ccD+CPqW{_AguE-pN-bqGcIIj!%TXZ zRinjZy8seg2oMl@h|h47pl4Um+gp_pa+F$UI2$0{ubXx9xmlGT;%~wpZ>hvIVZVDA zbL-&FN>fEVKMy#yU_Zb}x3WfhPA5|5=^ZxtMjRvL6!Zeqclbcd)X?Y5m(UWi3uFM= zvYD1h4edLiU8hG2n-dXo-s{`;^DzkYJ?LGbY9Ksdq|KVlNxMgag1#xfZFBLwC083Y zXAz*uK#_{0`=id{*L=K+s_2F%B!viRf$OKOhC9$EaI1+tGFhQbJS&&*nD`9s#{-k7 z2;xr&alj3^G%Vf9zs>g{@7bo)K)Oo`rH^+nHfm1<66?C#a+o?wPjvu#Z&BiD8)GG0 zg)`XTjbV0oJ_Z_hQky>9ce&$hW7p-r{((1CS9KpZ_ulH^+HkzTfVZqvV7+ zV`?#pl+Ba_;TJF&ajaf5vxJ0!_Yo;Q4@Uxw+3(ou{cbtnF!O81@8v1yv|Xc{KcpYE zuf4MyQnt5W9Y)RXW}?~`TM{X7g`?>Jk-YSNRWt|Th$3=wz`R8QwL6>M^9N|ikw8(5 zYtP~66x!~&ne7F`P(m?yk_Kc;i9k1f_}f;>63U9LTu|#+f*f6$u9nXoo`@3VHoZ!T z+qCAy^}TKk$1l_8txGEE!)b)m2+45D+(ac=06vrISuKGELcSD)ec)$jao+K0lt|@7 zXg%E>kr%Ms@}UDQA|)exinRb8H*RYZssq72Hiy%gYQSiIzP!EbO=h}5MGKC4n5NRK zV-I-TU*-}_A`@T2UZ8?j-yI{4O+s_?_VF8hPi@5=G272-DUJ&($*iwnDBk1$5MP|_ zTPNL%Zth6(s1R^yIN0lU6lr8@-r5>j&Pmv9E6>V6&zxC-$$|lO3lWtnT zNh6Sa0|wrh@xS8?IYtrEL&#%Y#CW0WvCz}FIqnQp&{_tXQzh%Vhxc-7|CO*019ApA z+-JE`P{~``*;FwP^e>yDU@s9_XQ>&^&WvPsm|exk8ejlY>282SGx#DZwpoIz!+3D! z{BD3VPYX*M2V>k(0pxRhK+_Kb0Gk>x)dq)KP$yCEY0Jo7N?;9Z3vct)qqK?UnQl3p zE)`L?xWb_Wq7%mYJ4=9i&r}#;2PUoT4>t@!jwf{boPs04^On?M;(EfD4Z&7@2jkMmeBZ z>!Sg@kJy-4*`tZj!%XN8%d;vCwI3g`EXg+vL3)lpDee2l_GKxENZmtAqwjhv^s z*h>rXxJILfV%n$l2eE6SMQ|?1KK1)Pf(<^NJE0}rK3U~@wZ5i^M{kK297u^L5~@MB zQ3_qGpf~pLQ~mgeV0OResl9%p$(X~mom2_x3Ep9&D_A9-Y|fLyQtz!$Ff z|6x%bc7HuKl}*LK>7u??+RuDCd7~7BR-c-rs6TG;^ zGia3wDX^T^aMw3@*$F1NjX!1s^5;CfDLlN1{3XIl_-Kp|hb#U?=qCMn zl-bwyxyWa#7qe46%18DdVTW|Q3Sc8u5&4vf%2nN{(mZl+?GP5>m(o~9SFj0jQ0HEm zaOiOOgbmZ~llXsdh=knIHwn#n#`2>4gxy8;IW}!qV|bah=b+IRaLPvtAuU4MtP7u$ zz~c!CH5HIHBVo2~-(fZU4OBz|g_Zefi;2j4Up>a039Cc<-r0*%BgWc+vI?9&8Jaip zt%7YhFOz^blyU)qv*hQ_iHFY0lT6dA9*T3m4(6 zw?}%_`^UY-YeDByN=K4n7*by8c3(=7D-!`>-yd`dc=e=58p#A_q z6a_|<1jhVG(L5^w@?wi|co68sx$2@OCXY;^+a>jwp=_F%*;p`{%4(UP?&2f&@gbZAT|bLTWY)njX#DEqFxdVV(l@8b{kuZOrg zr@ifQ)1}#hCRsT}n568q;S%=y^fs=zE$e{~=DkL#Gv%(GRojvM^j5WEYR4cD93nIb zZ>0+&y9L^EEFItYPPjuH4Lz^kWAp&2a@Ce0Y`Z`DXw^a5?k|mKg2@BiCqm3PNKqjM zh|1}S6E{n5#Buy$u&g{vz24i zL)l22^q?N0lNcD>f3LdTBU)m<7X(fNKz4!jFf*pD9F#N^`rCfo9Tu{S3a*75<~$ag(HiLIH4 ze%s@3Qj+^HoMx(@bMAF0x9{kcr=}FW6XBR(o_A%q>%|;JPr}@sNkRJ4&z^_GnzPK$ zkiW7n!t(vMfNkNn5HQ<%*&8J9d(^j!G-+XL;Oy(72z7zge$IKeUuzGdXSxV^**nr@-v{j zarm5Jh%gz82*qvqo1KlitLCA;PiBac=*5Z;fuyH+bzHLg^PM4|dsxI0iflD*Ruvt~ z@}Ab(#1MOT%h#7KZr*iX>`vt=1zIYMR}bw&im~1x#KFFU%Zke(Bt2~(sI0g-r?q0? zlpqMB6c5!jjLXyNXjhkI!aG$W|LMWyua1%|(sqWxj^o*bGP`v% zo$^TPIX3K;z;=Ws+Fsr5+XX;K!uDD`T$1h#T)AG&BQ4 z#fG^m(6~9x6r`~0Hpy2PNQ%a?D_<8p#ANj<~+ESS%Le1g?O|7P@*0MOC*Y>{JKBe}>+G3j| zlyAlc39J!jWEu&is#guTr=z$zuK}4X*1#prRR8*V=Q&qR%0$<%i^ZC4uLXRWG&^#@ zY^h^qKm=w9x^~Z*p6+;gW>=4T)xGPO6g(%}Z=6ZUezY))E(RLPD_x3$Eg1nHEGX?#E@{paiD`h1%rmNjTsgJf}{9DdNm z!4FH}V~1#Km_%?01Rg-A7h*=Q*&aZTil&=_@*zEplM?sWHXe0K#&wMADa6hFE2SVv zHDSOxYXWI+liqg`z#fq+enDfqlqx@!O@TjVojSs3>!dar6}Yh^H$OZLZ}3mm^i{yr zfflPPbev*)Oerp|)b0EXng26Dhk|qp!fX)F`&CL|Z@Ziy{KA@=6NG5*f$hkL)}87R za#ZR{x2b|Y$NCPxZl^$b-DAkZ=5$(r9O0&KfISte_My;3x$HnN&dXdN%>wPe9$Gsq z1kYKu02SZscwT;v8&QBaN&tSZZbUKnYp!*tuN^&_c8d3n&xJF{p9!x!dR2j*zU!gD zl{OR4>#9JR8O!!YuDhK$1PIS5qWS?m>n3PU??C&KSJ2FG%xE?xRzIDhvwj?^p0=QJ z%c$|wntVn*>yG(yuraHw??a|M3E?w+<=*|R*n}}b6Q^r$*S8OwMnqKLkpFzP93Lye zK|k;k+Q1wD%02{qCnpqWe8)I-yasLn-`C&IPv;~~)X>O;Gx|S7V!N|Zx}p-etLVlj zTx?F_9}ld1FYnBQ3TeQpn?+t-aX@680b!##RM8VIno@|c?ssItuf zQCaK?>ME-3{(fY(*P~yZE!Q!46!+cB)?{Kjq~RPG^eO`fZIv##XVB(3#7eu~2Kbr- zfgX4aM*@f)gyXlfDKhs88R;Ch(39H8(b9S(SPLT}*Qa?Pf^tTeJ{|CRbFb zHHphlOxgdH_%kjT*4Fi5*&E$7MJ;v~Ht$>PE}Ym0uK|5JSI|>Lj*o#(xdc9JX;aC5 zG}2S9eHIjjbt%}a7noz5Cl#a`*F7TP!EaMkaV}h6JS50VlccG~n=s3y9|O=*jR=V% ztiAb8e6DQ*q8#nG&6IyY$PuM!zn>5LWqn8apMRL>l~##J+E%oyEIu zxIuVLhl3xHD9-}umBFLT5i!T=RZ6GSm7v~l@4{*K!NKK_i1dZe8=UM&5-biEGl|Uy z&|O0pZzhZ+-qGe{gI?$w9SAjtfD*dVvA{(?_iC$IIla-Ds5sOPGQT=?IIZX+_aj{qck$&R#>ltc$ydmIyFpXa|IetViw4? zt0|v=j@Vf@zNKcVr|MP7LeJEz{1381Md80QA~7XFjc%h5Dn2*y+*xvM=0yk9HVkyx z9eGb*e%$X$@}TSrNtVNZQ?I&jVACQ-)u6zTAF+nK2MR@O${}}ex&qW!4brl0HTuU( zT;3ftk~_I4|8t=UgB;=_6_`Wx;A_Y@QA8Z)Xcwiph5jolb$8t>8yH4pei_#_akGX}Ge$U1i(s)}of5l|W^(H>Crl_tj<#olM z=@bZCl92EP+FdN%LcP&R+ww~{B`M{1#3JS}q3`E`%GFAD3oQ=Gq_}VgU{$fBTo5*Y zM>(&17f$of2hgrAme5(G;?k=0n%KmqW257tL)M8-aSKJ#L%C}5uQX^n%9}t9FL6Cd zfqh?GS}Nt7j(Sau;vP@-BwPgiJEZ3KAbXRW`IyWVE|2p%^Wk4nl9UtICDaW52$tm~ zFFq6MyeM8Wa?XVP5}%m(!=2W95YFPCE$Lz4sgGR4x}rz97zz+}QH=E6Le1Crk?=P6 z!KuUr4f>n&{ZZd=hY9ld6wS+9p1t^RD2Wb#l}+$Qr7D7qoJHDqlp>3WHyU`hWRQgwJxP6!)A1e^=z3OB=yvgiZTEfW*!SAn0ug#h{0^Gxb(N^ zUs#y*^`pr!1P)5rIX_5>gYf;pnrLqfshObtXssDMDTy?T_anmUimdbAExcTRyu@qp z)|z@Pq@ERlZXaY%3d71~@S5x}94?kE%Pikzp00CgzRFd%HpwY@(5;pL8z~9saTmaw zdQjF+G8v$II#jO06r{h2VlCnUqzJ!z>tA=2|Aa8Gdj9KVU!R9-L?)amJh-;kV!p_H zBJLYxA}zk;+aHGb?-y6h5X;raCihu(Z=}D8XT7x(m#X9c**I0V8;MyQ?b5DcGfG)I zSLW%TiO`DxezOFK*w%Gi=O)tt}zX;%S<|p+)P3+Y*5O| z6-Rc$`nr8OVaUaXm1L0Pr= zS)Bkgq0e8NZq#UmG>{1t1cJWdUjFadsZB0#h3BqX_g_AY*sHYwG}x>uc|vL+^3MmY zJKETXe%-GLh8zBTJ3L|nVi%YLgL~^bV5!%gkjzKRKa*&}SQCV8N)`@3sl)o+b))?B zV0}$Un+73Y!Y^}bPwD5YQH2Juch{YSS#Hh9E|yc0h&M0DE`Wm(!Vd7e5v=oEX7Jql z54EdTA#^q(JN#b}PkT`K*QuWQ|ND`(BMd(rGyLZ_)_1qr$-b)}5-0K?4wpQGSS9%V zZocuqJvAcAwn35$aV38q{R&lLG3Gu`>&455khz-YYj{G^Wt-l{`m#S;|JKct*NYf> zQu@e3^mfE4W@mtYow=z+ZwIpQW`7pmxMn0@mKi_?6`@;*k^Fb7aRkoBUtj*8XB*W$TjVJsPyW{n&)#=y?i=>@p>+#-e{y}k z04EZ@XZ>=*dJRh)GA1Ko`}%9-PKb5 zqVD=*%}zG`k6ra@y(wyftu1I1%bL9l9GfLn>}uFq9#)EVY8w&-^K5d>l?1%Iw_iR0 zwmb~Q*`W@{R5kFTzt^Ua$jX++#_{KWbrC<}$2y|iLXU&;EHoe50?wqL_1mn-0};iU z>E!RifhNw{3W5u(UduL%@BJDF?TE;Q5!UcO^`Or+8gJ62de^30!soPAk?W&1z5>%U z6r4*}g?@4IIjympF(SF_^Q4E1@5th6EkG0>(cS=%VBPzNb)k}+Al@#l_mcO3{(g0)Yx8bJ8Rwh58{yiQ&^CFZ%VU^VVI&ianLwz z3b872QIVR5S=>%-4jmCgPG6z*)5yp|R+gXln;a9WcvXzG;&kB_ZKZ-Qf7jLM!_Y(i zOeeC+flgO{v{dO#H>dz0r&B=IG6AT) zk=eIphTHOAHy46{-<|;0llkYpXUz7ajY%(UDl7a!#`j-2O+j#f_hN|;jI%FH6ot zGKbGcHjj=CB60-DO?BYf`u#`KyL zc0yRgJsJ9`yfW6}78U|b%FzF>{{>M%|IR8m@hbx2z+3$KHLT|P6bvfD&Oy$Gs_FOY zRfq^ZXw6Nnxo6uyBTSCn#JUP>8U)IlMu0yV0}b4t$y8qVp#I0!SI(*;ume2V=PSRy zeuO**J9y>2oHKa#KVKpLAdVHXntWN0+?){!UT0+I{r8aLdpKjZwk->3tW8SO`D;IA z5t;yVK;%M}2|2dtDt~_Ybl}So&K0(j@sB0Qryx=%g@N;4UvjOr5j7lmegvLJ{rR3( zLj8~N2}(n6cJ(9siEvaApfL`JWW>O!gYAe9N&~_nh~69DTpeL}&pnK3>m_T0=s;Hu zS{P+$6qPU;gew`ZFClf}qD9oUwKolk?ts#AQ_4P38JsRu?tgL7)8Q3B58r?Y1Av=R z*|btTEmqQGA_jTLi~$9R16m6-pzBS+#qyQYJ#Vf}T1XQgT{h>!$Ji!F913@fO4(uc z&%7Ud04aV)nI(@o&-j0~0PXTeWVdB=kODg4y0o`V>Yj?KDT3hph?Qdfk<_MJh(_BP zh+b}F+{=H9zJN00Lk6A;3(0FJZ6AQ8R!kPtu9_X3N>kjBXU=sPgn0V4i*hirp50Hk zOwht!rM2;wsYStltTGx=Lv*bf+I71MbRGCsxjjvm@JwzdnW8pyPVfsM z_;3#)SFJ5Kzpm;$6|F=33pEQ3q4nYVouX1Xtrl-Kuk<_P5;_vZucDKrR55O-?q)yQ z)QvF;kJMgS_jHC#K~?AnaUHT?$XljY=hb*PnWWIiJv#5(|^vb$gDx zhmpX?TOl_k;eukywM+a)_oI^Pl)aOAIBR44^SsjBiE)Z1 z=uqKg!lo`w-9{yb_KYlywO_sJ8VQL z)h(Nyv=i0XZAG)DL{S`+o1-|hn36S-?7Ob$o@9FV&ekbVQJ$9JyPX`4ewLj)c$0bH z%X{yYEoTWOYBTJloNc~W+w+Z(C6xbu#_D$|b9(TZx)TA9^@-`v?b<-nNITKCkPyt! zwl|k5A`Bhd&V@=1=oeUvh=9{Z0IE7n&7Km>-fDS_y?`IB`Zl<0Lje6+u z#5jR>GHf_uIB__+6XXxShZ9h+hBw8(Q>W;!*X$n(qfW+}9aKf9r=xT^OLX_}*9F9;f3~1#U&)qj6M2-v zD6ze;Kz00bcmQz1SRyNVUN>e)G9>zc)JU}Zp-AG2h!hhUfda5yX`i1b~uy35a zikYn9p}{@qE|Dz7ryE;D9(`v7Z0+i>l@;q*a-J&L+Elzs70k`fUgZ%zTk$TgUO5>B P{}~=Sb})B8DfIsXAQ?xC diff --git a/guilib/src/images/Stop1Normal.png b/guilib/src/images/Stop1Normal.png index a28d9f430c016e4417d631630b1a48ac90612775..6449c1f1e357c6c9f3a601aeddc9eff995db531b 100644 GIT binary patch delta 4628 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 Awg3PC delta 4253 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 literal 11052 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 delta 4799 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 Z{|o+MJXB7}+)@Al002ovPDHLkV1mh7>%IU0 diff --git a/guilib/src/images/document-new.png b/guilib/src/images/document-new.png index e6d64bb90b32dfec83232de6478afc1a6b335b8f..7dfd0d9e73698890cf4f195f84837c145080cb84 100644 GIT binary patch literal 1652 zcmb7DX;4#F6uyEX>~tIiL|i(kBPb;-il8XLpaesJfP$hS$Rdj%iJ*Y3LWn}JfS?G1 zfD58kQE>sqisFVFDs7dh>;wV{*mNGMKEN131E|3xoK<+XC@>l?9&{W<8 z09iNy_%r}~ssXUgINKh!4giCjp`?fav;aW>A~r@87(`}*#+8+oH8nL?FZn%vxB`)b z>gwv>TUQ}+O9aBsRLX5;EaZSd)`;AEW3tl;o;#F%FKJd!^_JnGBPqMD$2vd<9qV|2DP=db#-+O4GmRQRi{p!I&tDe zNl6Kt%|3SQ7?a67eE9I8Lx)%edn+rV&({TCYa-mdx}o`suTH1a>-8FqMyu6MqtJ|M z)em&4(>gd;r%cc*cj^?;I%Q&ifB(S1z_)MT#>dBphK7cRhet+6Mn^}v+Mje-dLB2e+Cj|=#*3)OxMFPqA~aK@^T)J zH#RmVn0z3eDprcAI%SLwF4U_pBI+V3pHfyD#1ax$5mnwknh5Q#)eN=mwY`?f-%h>wp~DwVOZu`mo% zsni`icA&zDh=@S7fgs3|ro$io8j<8`h{-;|KhQyANK8i zX_Y~QsblWuL2(sZZPneNfMvpJ4$M^7zJHVFvWi4<{bJww`Z7k(I(N7usw zc{fa~sL-dTEWMoM>Gb+u+=ncF(;m{>z=nEn$JFubU%q(nAAeu_m=^m;Q)6VBWb9aE ziKo$_+()rJBTKjJuX2g$F;3|t7@Y~Mr!P%;in}wfOS<)1@AdAssAj=uucMybfRksJ zhFSh*7hlFY5m@qF~S+Mrtc87;%X0Y|Wbi;cXn(J=& zG4B^y#r|P;vt!twM9wb#N6zhqCuMJ&E*6}qR z87*4MmxMCjn@^(9tJ(!OI@hpsE|LGF-V|#}WVLP1&WwEfi^uzF3aU*gc-J-kxnZm`V1Udm^8cL;Yd)dV6d~SKiOS_15*p&kGiH zrufkc@Y{w)pL{ryAJfT-jKFTiD=OTt&|gy@ZKoF9@-K;u_-|OXqLE=M=6^8);;nIlDUHDn}1e#cc~Czx6?=AeUwaL}kXuW+wQ= zF%nP*+;Q%!U2v;h+&m-PJbm0eeB5v@I4>U@ZZz6p?!O3W`{I)mbN-u9VbuN`N&v(F KGU0|ljrlk3RzW%d delta 997 zcmVp$%7$=1V@@@tf zj00f925nnss<|&hI}u=gef_gsF88g7l#DSLWANVNy(bJqoO7{#cRXHr%k(?POcqa> z7;g{+S9tGn&bH>~YF~f+(aZ0U#&aoA0|@#7thL{)t*w>yxk%nY!jFra=|`UYN$jJFw$o6!Q~hf9-+XQxm0cZJOI$ON<^+(?>q^>$`OG* zB(MkM>^|08Ym<9q61gBW@79(}6)DXJ_ zvV@q$=v!W6b7#Qc55ew%sUXe!sQ|-zqU&OjCPtbd;~+DLc?kBy&c=e6mmuXFdR0bb z0&h#P0C8c6bAK;DQfQ(=yP3S#V&!T8z&V$yJTVWXfNBAyfci_s)T2sp-JM^f{x8te z5ZoE6mwyCqunOV;lIAz9ok5vKnZ;Ab`d5+Na@biv#$EjZxL4o~z}Glwy)kJ3o12@l z0CCN8ALRjF-^KYwoG*gg2KS`XwmCd%g0{BNb`{ZQGS5Y#2LP6rm#^zTI}`;x@8Pr& z{aA}Av*4q7kSkN&eazDPt*6j_O0l^GF(;vO4u4gRth!}MDixeQ0wz#Djgb=|0Eaf*# zFF>iFT8X9#$^@5w}RmD`>TX}4!0Kp2LxO0$6C0QzNU z4}Zid&N>H11xP%I05&!@FlI1uh6OanWah@e?qL{S$7*F|CG}!TQk>+WI7tmb|J9u4 ziTE=iTT(+XjMA*NEST6TNGiioz`DnJl8oemyOdkq1z9qR=SBkPt&A)XRZRu!2Phk3 zPF1yBE|(FB-UUGr{Oh~`$mjECz&vmav|joFN$sQ=PfJE9FGSUI2CAPv9KlU&muWq0`^h>)8QUL~d`<^FRF`d@;z@ TDRhF#00000NkvXXu0mjfOs(8T diff --git a/guilib/src/images/document-open.png b/guilib/src/images/document-open.png index f35f2583540678b7a544d9175245096082f302af..805c0df0da489d373cc0a5d8fd8436115fe440f3 100644 GIT binary patch delta 1634 zcmV-o2A%ni45AH?B}!*OO+f$vv5yPfP?@5`Tzg`fam}K zbua(`>RI+y?e7jT@qQ9J+u00v@9M??Vs0RI60puMM)kuev41%F9IK~z}7#g@&F z97h?(fAullGrQyQ?izt&gTTN_iG@IhBtV>GoddCQaH0^%9{OUzl>`3(lK%qWf&xS+ zplEK$;*yZaI0@h!z#v5+i^C=ahpf@EGdsJpGt(dS#bIa0^SN0tQeLUlUES61^VIV` z)m0)%5fAMfdc7{bx&914d;SHg)#}`FAsldQ zHkh5odylmiV+?7U^2M`X z5-0CD`T1YJ`ts|GmBmN8-L9Fd*as1~dGn?Kf|Qa$6G9+=r9>%()*7W0rBaC~iYS-M zNGW;p$)|YyYZo5urkz)dqELq_u)4aci=w#ZoO@`Nw735Gu24!1Wd?ZTw{PIR8yly! z=7IYkq$mpFIOgk5KO;g2=dWJ7`m5DXuRgW0vEgSD$g=EH<#PG{ix)5IG);#<-g}&L z{N(xPkW!9+ft_DFKUVv}z4xA4t;UrrR{;3?z{Is z_0HcnzW3aZe)@wXNx~EXV@#Ikxma0Q#yN-geu{v9l#-WUd5Oo?9%E(Y1dl%QC?KX( zkWvyw5lShXa~NZYqKH;&w|42$rPvsg%~}O{p69C3Xwqmjh6DyB2qB=9;@NMk4->!k z`fFnznB|=)ig@U&=g?XcMp;f0IC0`c)@rrX^70bKm@xvzn4$Xzp_Jmn;}^!jCc!7o z3Ct&du(7dG)M_=kv$Hc)U|>RP?GSV{`ERaW#W~NMi48rmwswB_?W_=ky+S7rZoKEY{!SPI4oYx0;hX~@?ZITki=Sz~^sP}35JE7`W>YttO>Q-+eEaDy^Vw6& zBU1LShoQ_+W8#V4Y;^eXFK;|NR)Ri5Q>`w4R*W&2e@Kp1BHsOXm%W|^Lf`L``pp$~ z>Q(^NSW^@RA!rO<51r+lGp*Jx&1REs;i*<4tPcnQ0?|(~(ZakI?lgbUybq*>Bg-uW zu{EVY2q7<*W0sbd$gN-@)&!rU<@7Ye+|GPyI+S$VE#ZB7?v?o zlKpOO1$Rt1sz9c-M#V8zN_2CFa}()*#n`3qu`s+kAMgPR>!((v=(FA3&%F&{nt=B{ z?Q}Xcb~Tlyh)!zAtw;1{X`RQofUzFy4jdlD40g<@TK_?69DAKq38`k5AQM6$<0@L0 zXs3q#)Q}bqWBh^CVlwXxgNQjKfNpN-W=1oo1ifCb%TBvYT!CBLJ@$HLGVfG>YB9&8 zli~=V-76@pEB*Q8+dKDeOe@d>f!7P<&eQH03ggEz%v+FiDlzt4O2PJCh7_`wj2z9z zJdma-?bLDdo|uKQrX2lmVB-oMypckXW;s%dz0voh1iAnwl&F>ki`vgk#s`4S>iZCA zwQ@udJ5vPe^?J0nw&vQsg56$!;Qg)L{_%Sz%8@}w`ys&A)}AX&?+^8QT_;Im2g*s{ zBd1TFe)iM@>p#qne@@B&;_r8q2;c=^^X~iaz4_vI8gE~}4(ugK;s;w>4fv#vZS3i}0@3Y6JfQHi0xrk|2^KpeSaeg7PaX;Y07*qoM6N<$f>RI}7ytkO delta 1479 zcmV;&1vvVm4UP~AVNtL%`LLH zBqTCU0yqaSND;{5unEB->u9qxJG-+p(;wAU#bIZAXL@Fg#YV~_mFlzK=c)I7tE)s5 zMSPafw|}!J%M*XQ2BnlPs8n80VzU|qLGRE4n$4#8;p$WT=J^+>*XuLadB3Nz-EPYl zUwYxOcmMwGJ6V!_CkTT0Py#MqyeO{!^B=6QuTKzq?{UuIoFmV3j4{M<%(qV8BaYvB z{2RY}x#ajD7d4czy8~^!0D5VNF1HAo*w{gyntm# zk{}3BN@1s#>i!tyZfLupa{NJxVE_{=sTt_|3Q8 z9Ls?+y!Qk_zysesgVq{9s^l46L;vWWKi<@$y%@ul@)|{)FCp({8tQyWQqStIm&~{5D@Z zvB<&vu)Y<`6mOJ9^m?nuFJHO#V1F?%qR`aq^EG1(=HHSdwSW&k>9aG)0YA+55~dE8 zCd5Hi4>eh45Q5hJbdg!sTGQ!l({8uvXO4O;Aa@=iKp=)DO2yAA{-C(M;+!YWEJ>P! z7aJ2E@ZP7@YRJOE0%5UcPJ&MSf9DsF{H*ZNG*6# zgfUQ(-F})24w7)x1Buoe6@P}*snAa?)|SkRv1*uOzNoG9I1gFwCQhUns@>jAUGDv) z0OwrX>-A`DYibJtz1WcE4l&$iZXL#YOzz0-UWR)yK~ZXr55&f@(~FgmYHA1)Ap|n4 zqjiODY}kzrab_{b?YUZ%?M_LMl9xu&PjmW-(aZ?JU@+*j)vXfN;D5&EfStj?Q*PG& z#Ec{@J4XQBK}MF_%HNK^w{_QANq}>100O7yNUftgFl5G!S(tSoXGCIbTuQ;_PJ$G2 z5RE*|#&RHzW4f{B_?;nhRZTVc|H#Ic3|%CJAWl-G6g#8yqXPN>##g9U1@n61{a6^A zh4TDp-+51`lOnv>ntu?`Y&L_Hl@;3^WNZ&SA8l+8pWjniZku$p?>%g6?AXi<{@iRf zbreN8?0HTCpF4T-_k!I_OG@E z@Fg9FcT}rK8@wz!z{kKk5JyquMHEFen=DNU1c2h;zWub}hs6MS h6h#h3o@AdD{tL^vBq*}GQZE1i002ovPDHLkV1gx>)t>+W diff --git a/guilib/src/images/document-properties.png b/guilib/src/images/document-properties.png index fa697db405aa4f9623d839d18449132402a10aac..3144963e0889083a61f78365ad52e42aa48d0bc3 100644 GIT binary patch delta 849 zcmcc3ahGj^WIYQ51H;YYP4z&ECEd~2k%3`jKlh(RRv=#?*(1o8fuTx`fuW&=f#DZW zsNn?zL#Y7+!>a@a2CEqi4B`cIb_Lo1C76=D-CY>|xA&jf59G0zc>21szhdQM=jCE% zczqElpX%x27~*mK=+*OGGNB^JKb~J}f7kG2PrdLl9!U|Fqcc^S*f^WkKjhZvjl`Y{u^|A8lCkqvW<-mz3v)b8lJVx`thk^%~v|_eqMV&C;qDL zzxN!upN_WwT)SbK$Ca$5tBSvFE{Le9dA(Zh<>|j#*EaUKu1S_%oi=r2PwE`A)g7)n zk5v2~ssGNckG-b$)a`>qz|I8%QiTikMJ`Tzd5qJ+MPSyZ(1yHwj92C!da!TR=E8|f z%c|CXXIR^{-Ae1%`JAuw7<6WFyj$s)tFU!9PeAFZo0;?MKXu zk|z^?o3XB3y_(T}S7SoCU1RKhDQ};>NxxmWD&iG6>Rz8oxct68Yihjhw4K{z1X_2_ zw4QWM`lEW#LF><(s%ADt%KW^iTKU*P#C7Sd^G4UtKROgV)A@a*@RWUJIU0pGck2gT z*i~>Y;;A(AO66qBEMexOb6#aE9Sd#*tNQEX;8RKfmFgZr{96`^P^{(e4n^GXs6FEnGGGc z&wkmh?v$(Y^_G#t&5dcx6lSi@_&l**rF+uNa* z@s1m}uQFYm+`MC--inm-zKi7#O0l)CyJ4Qmn63B!?@jmlZPVxI_Wl2HXWJF$uRf12 zO!Jj)v+bWY<+RP`Bk}XfX3JQmSDey+kP@;?O>MK^f7V344*^bsYC^z-s9NG0QIe8a zl4_M)lnSI6j0}v-bPY^(4NXD}O{@%!y{rrkbPdd{3=AGeGl`<;$jwj5OsmAu0oO2z SZP!Ym1_n=8KbLh*2~7YgQ*jCa delta 1105 zcmV-X1g`tt2HOab8Gi-<0047(dh`GQ00eVFNmK|32nc)#WQYI&1QJO^K~z|U<(J=T z6iF1uf2XRuC!089!k8h0f}l8)L{JeCe2skp7krPs-OIfayv!5qD+t~QiX!-rXu!Xi zi;1zBB-1n9UDbOLyXfikWHNE@_Q0XK&Om)%ednC28u-7Plz#*F@8AF3_x%kKsVk*W zN@12 zn8$JKd^8$mqtPh4ckkYhGM-lsq-nY+B7FGp0TIC%!@|M>aU3IZC|G%Pa>@}w0m!n9 zR;xvlB#ToCWPe%a7R$XjKMjFN@k#>L+DbqXVQXs(V@&0ORWYqKola*W_*4Q#a1r6k zl`EA>Od?RWT-~Vzs?s1Lynp{5V@xIBBn7(N?ra2#0OdO9bUI_Z55tgGuU=uTWnp1~ z^XJdgZnuvu##0f<=Vb-bG$o26(lli-7_hUm!`9Xo&3|SS5kYIs>gww02|R!PoEtZ8 zxOunRJ(B-!x69+lkBOoPr4&h$aPHhW+U@qQDFAT$_HEbyBEq|O?<#Ac*=+Lh<43Hu z7-M+-`ZeFbf5-DY*4EZ$Cy-?sB66f9JDmTM# zuh(56K79C);c!T;RzoSp^71m*uV4Q)1m5TLbYFc=U;kyFK5 zOBjZ{c=3WD2)KIns@wgE2~TQN;ZGJeMwAnwN7 zvFkV=d%Ye2XU?3V*XuDH4$)epwPs~yg?_(Z34W{s<%0-deSQ5%d*%mI?qL|Rxw(nf z+9{#6o}ECIy?OIytm=#8ir;xFYOT3&;eWzx1d1srTdqKP*HvnSVaT^{-(0txn82?n z;QPM2Jr@@b-=8}>JO3SlX$s``LT+czo<&4XSA@q|@yT}F?{L@|$61*`Ln+nIvaHc) zG+fVno;R^BrkRLPuh#(xf?y9=0e%3ZGJ!g<{PpYCzwX|>`=^Nfb}GclHvIJI(@Nh! z8_>WuV2c{ofELgM7Jtqs_s@*r9B%di*af}-`%v-c^e6D#&vE)=|72!>ghSa!fXnB< XUZr&S_G|>z00000NkvXXu0mjf7SAs^ diff --git a/guilib/src/images/document-save.png b/guilib/src/images/document-save.png index db5c52b769e74c5ff883ed02dd4f96e604f2982d..e6fd350b0e7dca882598846674a892d9a1f059f3 100644 GIT binary patch delta 2110 zcmV-E2*LNW56lpdC4Xl@O+f$vv5yPfP?@5`Tzg`fam}K zbua(`>RI+y?e7jT@qQ9J+u00v@9M??Vs0RI60puMM)000MLNklX>cW0&_%w28s)(EOT z(xaWXo%!jRXa2Jb|7R1|8ZhJDC)>0U8>KKyK!g!EM(O<+@m$ZoZEye20BTq5PTIEj zn^-imU~Wr&Y=7F+3OT8~lps*__7CcJkM`tJgW1Ch|Nh>WzjyA20@U37WNk>AH(O^< z&&-?CP$8t47P2iY36_*dDG&mVo;;`ay?Hd756t4;KW#sHLjYPjc7(Ip$Qz4V8_t8M z?Mn?!wL*6Jw5iGRIZZXvl9Ifq(8dsr*z|v#;ny#}lYi2lKljWlkDvdv0M^8YX#L7Z zZ>oqbJfF%mb@!&;989}A2Zx*&d(K@bJ#qT{j9crgLUza^?F&QgcU=*v?buyvW%HeuC1(3Z-}BKQzq@~IfBov8K9H=8{qmu;3(5wE^8`v`jG-zS z<5w^2&wqS8G_vT^ZiO5dj^W%~Bh1-AtNSJ{4$$-G?Y|uu82SClo|LjJi63b4zQ!5x z`FwMI%))HCt_V!OXXiR?MAQ7T!_OLHeh~_VmRgoI4sytkGBW8lRxD|ye;`YsMsuMg zEP3{&*ZD&E35+qt@k%M>`~C~A>prw^-@g1r0DpUIMQiJ<)WJWr)<4_0apSV~_I9L{ z#R-iuWZgV>ZhZzR!3#868-_=Gs*(w$9c9m+Js4xgy0v%j-upW{JKqJKx+*~4*JtN7 zd^9Wu#JrU&SMrB9y7=bqS1t*gZA*j@c)mhwgEodp$im5a%v%5SxUpaQVk3`s+)Za^ z=YJMp7htXkkb3R$e}yNl`|9$$?n+vgMcb?@zQ1`9&%D&dvW1_ev?Por1-2y-fahzp zF=%Zt0=93x8z}|Gz&l4iblKCpF;m@_~PH=N&Di(j&OJ0E5Ap(SL1m^8qjZ^%QPCptZ4zz{_#;cn?4S)-tpT za2$sfD^_sq*s-qz;{e7b*t&J=ZkaM=%Cef88XU*LwryDXId!9!S1lYEX=Xu2Aac0k+J>$rcBP%Zf5JG%y)v8rwGJhEWf*`;c zQ=G&YgVvgmW%$viR!*Ki%RoBAy?4x@`@{2O(nCDb-iYUU_`Z+t`vgINloF*BT5DX_ zWyz8ySeCVQ9Kf12YpN?ND_7Um)!{e}N-470EC8jYrA0APN}-gZyj1eT2S3N5Bi;P% z&?&l(_wZOp6P7RpL4ej8fPa#b5`++BG8wei6n$i51mE|G#bQLGQNrOcLI^C&qPjB1wg+a@b^Hw5zS6>!iWuQ=m}oRgJRT<= zkK=hBj^iMuL`q306hbM*!i5WMt@Y-j4hSJOw6?b5x-Qk#)uhvDhJS~L5ke3IL9uLE z7V&tzD9iJcNq+Z}I?76Iq?Bl_(OQq@i`K<=q?9BQ2`Vcq>Fw>Mt*wn$Uw!pK;Kv0a z`b<0?XLxv+zP>)R)<`*;9+qVl7t~r8^Py0W*x>vAxX48!5rp{9J!gy|o6X`l4q9s_ zPo9j{`qr^Z81Xz0+kdu;FOI!{3eN)6mHNl_6?|-8AvgN_`-|nG6bC`T$&)89nLukj z@pijx+kZP2pt`#HQXwi30USJdkgl#Sy1TpS?d>I%N|8>daU6$yK3^1>g31|V7#J9! zuC9)BI!#?&9fN~|Oq({1bUICaeSPscaV+ns3xE5>zAJ49fKuw}5r{-0 zKDSs(t?KR_m157E^n@Xjg0Kx^eC;@H)Du8kz41{j*x*nrHLOCDtksQ>@~07*qoM6N<$f?PrK?*IS* delta 1956 zcmV;V2V3~e5VH@EC4X~5NmK|32nc)#WQYI&2Wm+~K~z|U#g~0>RMi#0fA_w(yUA`g zYt}$89}tWUA)%C-8WfNs$mbAfg;?vL4x^=G|FJW*1Fij_9jDfjamGK|nNnx^htp1V zu#PP)l@8HDi#lkPpdBEf0RyCqCY#N^m)-Z?y|;gaeN6&L9e?Tc%$#}m>)rF)bIv_? zAN-$1Txr3~wU2jbBQ{B47K4Z)aE#IiFygtsJzHPFv+# zAIiFq4h=gm^qo0he(dDAnKv}nMC^z~(Np9LKIv4PWPhTp{Js96>$AOodG1qrpuX$L zaw}i#wk$EnH+sznfBf!&@$-$VetK8BI`PYU*Dt6T8YvPejWLFrbb?s) z9>W+Du2)Ja-}j$)UH9HSd-fD31K8s`T3e^34*sFF{@JEYo0fKVb|R$=H#EkOcZ=M% z6CVpu^!4faO&^SiK{0>D ziWU6fl^(wNnQO-TeTt127Zz z8sEw6d*olyYc_m!+3mNdEz6=~b`9UZe-Tf=*u&C=pQXGkiX{cMB@uw41 zZGTyflmcVm^~3LTZs0uM`0`v*sT6H(ZOuZ6rMq_R+ILxi3v&OeRjbJ7^Z35c%8o`d zgD$%d{*&8oX(N*z#VrIF491Lw#VrQB@YfT##enwa8UnAtk)wV5{98-WD!_3ZmM>q< zyYIgHbzlO(gaq5LVZ-%Pr%qj3S67GQIDgo-jcwa(UOSf?o2z;Ctz%5BDn}VZHeY1m zBZv3S4s+&wo@~BIQ*DYe8~*<0F&=wxG3itsDJ7$$qck@+Q&CZ|e%-otHJ1etLOif> z<3{1SZmEyf8l@Bu-`&BqRE$^OIzc*FMs}n~&h=1wETpy770kS@l6?o?;oEn&Gk^2C zN{lfGAuz_^`##H-EsI7Xk{GUW=|s;87rifi6}R<)bR4bcj#O&gN~M|D5b*FBc;T3T{=2C0Jd!3zFl4fkWQ!9 zELyavHWGb)4s&+kBj(SZM!YP-t#j*n?d@Lb zt7F`8>x^){=XnG{fNk4&o<}m7WX_y9GY=m=yy7ANA;i~Ku3SkjmjfUO0*o=?CdL@F z)b0PEJRtF5lCUe(ahfa5qQrO4;=0F;-Phs8)Kg;I*ja>)wN*hzF5=5g>;_*1iWRheuiRXDZ zj)RmEDJ79e1f>)U7cR85*7w7OIv|9&yS=>~*LA6_ttFeyGBPrP5P~2G!nS2uB$G*C z?EK%HPV>8;G*D4)Bc()Zjn;ZBU$hS2ky4UMrKqm1roX?Rj*brY?tk5T5AfqsA^J=* znPg;SgtKSQqP0fKvGlMkD>SIJ4)dYZkJ;e+{shm(VljmH&pl_1A)n9VI1XBCrc9ZF z*7}Cf13=94JZ#$zUmSk{m7XQ2%gv9UE9v;TQf>?k4212l6bC`T@#DuY+CXbP`F6YH z*nhhgptiO)gbjaEiGK*-(4j;0^z_i%+e?3cKbcI1Y&MJII24P;u*j6EoH2&M!9f}t z8pvj|G&D3YG&IEY>C?$(votm~hR?~iLV)q^D#iVBaw)e@p1WK)0F+W6?}1n>HX+_0 z2Qqo{$#a*I@wUPoC{Y<>Xl-ppDHWE`lFOA+2q7la`76Op9)BwxZ9s!Lx;nJhw6?b5c^)k-Effj`nwy((9EYZ+CUUtPsZ^?T)QKK qi3?iBCBB+Ir5ywpv;gEU0P$~Fo?U%X0L_$Cqx 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 diff --git a/guilib/src/images/kinect_xbox_360.png b/guilib/src/images/kinect_xbox_360.png index 9e610e81b993eb39438a75d07190cb9640a461e4..26fd8fbab140e73a85d3e472263cd9ea74c7591f 100644 GIT binary patch delta 4234 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 delta 4031 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 diff --git a/guilib/src/images/kinect_xbox_one.png b/guilib/src/images/kinect_xbox_one.png index 3df73aa67b687ceee12dfd79b873fd5c57071036..11537c4e88505cc424b423529e9e99b97d42ed9d 100644 GIT binary patch delta 2595 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 FV1iqF_QwDK delta 5088 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 N07*qoM6N;tV1jI}(I@}_ delta 3680 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 diff --git a/guilib/src/images/mag_glass.png b/guilib/src/images/mag_glass.png index f9f573c9ab3fcd76762d3e5201053bae131a2963..c308c61cfa573fdfdb32fd7fb5799ed696969d58 100644 GIT binary patch delta 1703 zcmV;Y23Yyw41*4kBVhmpXF*Lt006O%3;baP0000WV@Og>004R>004l5008;`004mK z004C`008P>0026e000+ooVrmw00006VoOIv0RI600RN!9r;`8x1{z63K~!ko?U-F? zT*n!PpP6%!Hc)@(?4CV8^Yvo2u9`wwIU8**^uS!4+1YR2_v6gWHwQM^WRp!=iE2WO zZmsnp+qOq++wKm-Fz5UJ-;FUhf#uJv0j1P%M@PrSy?ggQlTN4G+S}W)EDI6AaU5>n zzFnD_nR&}`obLj&pCtp@vaFYO@7{eenM}qP78c0o^8|m!U|BXwL3<)WZ*MO>Jw43L z%~fY-XTNWZc@;39mXU@HYppL13=Eh=A|cwcW%$XAy!6vIWb(%Q^1In3`Q^Ey;yju!o zwh+kGdli3~dao#7|H&0ebUh}~XjHnox(x8GW(%^`F}6Dvi(PSDw@t^lF#f~e^7QA& zz|uIP@GVVXTjYIVrD`yN(AU?Gy>Fa^3IpGXL?X}nen9%g zSJ<)t2u>h~rHDo>BDMtzD!x#tgv_mY%w;`di4K2CTXwQ=eG090%oyVXe`uBg-RL;W zvRf)Uf`pRG!R+M9-)r%Vcy5KL$)R?yueWL42hV=)(Rs~^$jIApil`Z zS3{y2u%m0^MH`&AX$JCu04l$D4|)y}cp-mA1V0cQHzXR-SXv>geHGtOu7*@R0|;%| zhY1%TyU_;jubSxO_B8B&9-)@$AP~w`g>5O6RvPmH-TLM^dBI0>U#`*Za=AxD1zX4hQ*@CgSmGIuf78F ze*+*MkLRM%=$=BMu(ZxOo6ZhkR%?GPT5D0-5;}JXdorR9eOcI>5xRy1r9~+vN-5d8 zb*oHHP6ih)TzJ0OVjks4F95HLNOwFQKd^0Ex7``nw7d6dE8L;OU0{oCh{d9WVd!Nt z8996QtiEyMh8`aue=eO)|9X0QI@d}A03PsW5Cp$)94GI2-WH`)L@5<|o>zY?7K`tc zN~NC%LGaBpXU@DkHa7O0h^Sk)ZpDrsJvx5*@@30)-H%q2@rh{W1tUW3TnPZb-g*A~ zdA)1buGdFKM!q^VHN~}S*XZo*B$Z12b#ij@Nn?!rNhWPtRY>i+81S)c8GDV5D;*}i={i;I7YeRuEP?K8$qJ=$~|%XY1G?)2%?a_rbKv29yMM@Qx0 z!GlIA^^I1GYlu^)PMu4q)Ah4X;_hK9<(NUQZVf(2aWKL@sSb#=+flP4va zOwIwFtrpk_oxuBRO$-hW%83&vBo>QZq4DCfr5vDksTV-B*0OKkJ~@AKV4R z)6~|KO|7hm0x@ctj#GboEk54jZEbDizyoS0cnnL#^wH8_{dadw!i>xtGf7WYS z{r~kf5CyhUyL&$XR;cY%E&rOm$tIg@vdJc&D*px`@VCkF))S)u001R)MObuXVRU6W zV{&C-bY%cCFfukRFf}bPG*mG(Ix#akF)%GKH##sd<68n10027xC3HntbYx+4Wjbwd xWNBu305UK#HZ3qUEip7yF*G_cGdeLaEigAaFfft^x~TvF002ovPDHLkV1mD)5nuoS delta 1540 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 delta 2366 zcmV-E3BmTR6o36}6;hbl@#^dGq4JjoRLO}5mLP%|H z2qB0V5i#bSwT&OKf4RB&DhPtVUA=nsD~t;PfVH)?db8R52c`69VHl1Y2+1D3NGUU# zVxCu@m1fp;+F0O;{mj03$E(? z7Cky*DWwPKlu`&G4uHoVv(bQx7Y*LC6hKHBZ}>7}Kmp9A=koj7r#Zrk?hCr_T> z?%liC+1beyf3Jkm1=7urPPZ&8S6RAH`lI@r3RIhK^06%I5OReO8Qt7yosW)KWma{% z-ha$VS?2?E9re129z7$AP6&ZzSvl>av2$~CIDh`U{qp6@-vjuPtgWs6@cHxSFIHDq ztyZgr;cy55NYgYEMlC>Bx-O7XhG{a2%!_VUO+NZYf9P#`|4Ap)Tu+hWM>K#CdT8T{ zWH9}q;-M6;w^K?ox_)vhl?p!l?60G7aTm60?FJ8Prk|c*vnTJU8 zIOg>?O+=o#G104!$>X1A0`ts6o>KG7;xxM1K533wd>tVK4ZM`b6)7!-p85cMNH-inZ=<_<2~y-CarnaZ}W6aa{SY1&z?QQy?giY`RAYi zl1V8^zu%wg#f1=vq6l#uXSUsO95{}1s6Xc%gTVkvl7MrbIU-6aY}*E743=eOb!F}j zAMeAq`a>5rw-5rHb8yZxAH^6$sZ@gR`$*Fie|vj-xna4I&-80WMF^pElQKd`qC2vp zMfQ3*LAbaQNqP>oC%yH34Gr_poCe_^C1gk48HFp z2m)}i$$$NcsS|Yq#v3qTob#b+I!xi>Bmswo zdYQqY9RGaqFMR!~g|r_a0Dxu+RY0fp#+04M$qfy z@mDLT@dQ8qsEU6_5yF&0OQ==K5CW*W3=jfj z>=%npKoA5Wee>o`a}shHV~~Pl;Yb~}#qe@t8?W94h*Q9GY#cw@L=Xgpf9RJ|0$NlM z$1&RNHaO@1SNpF6OhsiXbe^^z&N&FBXjWX5EQ&al=@IQq`0?C3L|(CNe;dta6OQZR?fUu%7`6>ai8M`7t!Hn?eGh1*sT4On>y~ z(NPuzfj0?>6hOUR&thfIVW1@kQgH0=@58bzc%FwmKawN~f*?Q`e})<27Z(>XKR=(v z*l`>eqE4ahCuh#!lXK@V3`0D8@BsBj1C2%l!@&STN<>kFt#|K0EemlJ!S{W1Ivqq& zlzVYfC5j@fudl^;{(?nahkoN;1wZ9ri1E zPTjU`v|25fg<&YIe@UibjFf7%iX=&pB*`cX$rzYzXQ7rk$zU+Ra5#jN60YllQi^i9 z48|CCc6PE0PSIs0TP&s6+1|$O+qcndHZd3uvAMYkw^V{fDZ(g%S1zN|>7Y`nfKrNn zzn|SXCOK?%hcT0kmSurbiZo5Jy1I&5t%f*`4{fIUcE*>le>`6?$q8?7Zwt1vva;)z zN~qOpXt&!aa>(l9qEfBqZO;rj>-Bnu9NnqUl5a9|?(6I8kWyl8Z7mbJ-|uJRc6&V# zLa^KGK~BkD=3R>Nay)A8v9p{S70sG2c9QWG;*FJHdD`ff0c7F=habHl)^Uo&ypg4Up{%V zs7Gz|{QNvlpFWLi*RI{>oKpheuZ*!H7cXA?)rAWee&qYU%{iyzpU`yLA1^ZvtL?Vb zzRCD4i~b3lJd0A~1QviGa0iaL_j%dA5ZvX%Q07*qoM6N<$f{nk1od5s; diff --git a/guilib/src/images/oakd.png b/guilib/src/images/oakd.png index b6c25cc8c7b9f2b761a6e5a0549ee5255975fe95..12252aa1da01a8b7e65789f55927c72cef40dfeb 100644 GIT binary patch delta 2445 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 LNkvXXu0mjfwM=ov delta 3707 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 XI66@<;A1TKzJwU)-qj&#*@piQdT%|9 diff --git a/guilib/src/images/oakd_lite.png b/guilib/src/images/oakd_lite.png index 6515d25ec9e63fd99912b6a9aa8bc9da36687132..4f186324baea3779295865456ce897ce0df31f1c 100644 GIT binary patch delta 5888 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 CNaD2s literal 13720 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 diff --git a/guilib/src/images/oakdpro.png b/guilib/src/images/oakdpro.png index 9308b34a3ea69ae3d1bb288b8acb41d3c1bf3bba..daef30cdd298a216493338691b70a3313ff46a74 100644 GIT binary patch delta 4799 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 U?>ZXjFcgr1t|`1++cENg03Ej;_y7O^ literal 8691 zcmeHLc|6qn*QXT9o~=^G5Q`fQw!vUfL}Fq}k(4zQnrtB|B-tWs zwh$_6qKH&>Jzu)F+kJk&=k>hq>v?|vUCi?Rob!2~_j#XlzKio#7N&w*c5LC|;u6G| zp^pHc&cJIo-)7*wt7zwW;G-wV)}DR@9{{5Hcu}bCWDq@&Mh1}?R0Vyn!r!=27vDPHxfnQWEDL zWr#`a&ZmLCzq_*7j=tx!$bahm%IYn_z>V0GM8UK-m2+HN+~=sq##R_(<3ADv(n>jf zSGipelx@iwk70zP@q2~GDsJFKz4fg4xCvIVP=itiklmf#l1 zkQHSeYU$>nVf^hge!{X&p9s3omR_c3nKoS)nC7o5Ijp7nM)1uz)|^vmDMK;A$muKX zM!XNNi#?urB4qS9?L|AH_8>~bQbtj7i-&4COl8Nu8F6o0YmMePm*aNYUfy=kaJ`jE zp?Kw-d{y663Ju3G2XV+OmNxpF@$v|}b_EUpQ+LB4UrJ}Wt$kZFVY zq5SLVDYW!D?F<}reH>)vt#3Ob7k%D-h2duG_<)tiU6 znt~s{6@pIRG>;rJ<4#&VwVPp(<<-35bh+VN(%>;)*&JcRq=$!wxp<=GaFAp_5$%{Z%V|dVjR^;N+HDJ*2L^m=W zL?F9RJ@vq!DyqRCDoGD~R09je(u~QjRI?x-vQ3bMEiuTAs6zr9=x@hlw3rN?8L_r-v zAN(BxIMM^V(&;pmx_UrBfLZ`T&CAC{9j2qBqYj0u!{HFX1L7O#NyjrFp1$&H5I-=` zWM85Wl}4v}d4kq3@dPhFx*iw|*g=2r^Ppj|KjA%ne^&v}L!E)Asl(Ku>K-2IzwYp* zoA?7DzZ3emJA7?{!Bsy(_Vx1fA(Bn}$)0riUm-}upZjTkKJM%3kcjGJcd`fI>I+1L z{UzkuDd6XhzeHTKtdsl|-V}qi`su!=mrnvlP{uxFJl)I3*2~LX54=Viv=;5B84HX%2~WqP@pLkf92Aa3K@lhf)E1_R zg6p7Q+7PG~3i^w`7l}#<{NKFSh8v{&qsPpszCirI^`##ZZ$tL}arEQRow`2iAkg~I zqVU8YDfr_3$)t6j0M-u^(G~CMLI%!mzt6`X>(qbKdjt)H7J>o?_#;Rf5TrH{3enL- z!XR+C78FS)Xp`ZjABF#k?(0RN2jG3kMlOJkfL6dftZM~QUN7$6zj6t1B?CGGf*~MK zI0S~Yg=qt_p1;#bu)u`y$HgVWi$NRNx(-(*nKM1cagCvFr$>gDbU)Y*v6ot^hA=293!@YsW@s28A|uU@ zVjg;(Q=^18;a1-alPR54;UqBasXIO95W9a>z%g->x` zcBbM6YTPw^q#3gIF=1j`j6JK9P$~_Ju{a@qnQ11{V?P#HDovDNzc?8{$YtBxCERA8 zV@FSP6PR`hx6X;=<}}z!a4N$tk*~{zqcoJl+1DJs6D^@3QX+j8-x(;&-iNLs zI9!oyExy)H@kXWcq$|K|b$mBC&3k!?Yh2`bt zw*=H4TW%aTg3ERj!U{)VF%2c&5Td0=mwOW&KFqm_iW-@Sa3kb*Fu68c+*>{221z#4 zt9;PjQq-^^_a66VaIw8`p_1zg+#KK{U(B*=OeY&my#d8rflgH zz||AbJsO|6liO8AhzbXfsT#;`%j#)Yc`}?oSf3g^x4K*(TeTb&y5bTlw%k`e6TCQd z)M;fYzP2_os?o*yEBdRSJW4`JLPmWJ=$Nf?=CgH2%zY>I2cd=e9=A20yq*aR zV$i0SOidfh&lEnBs{ZoU>GX$u+0#p>zF)H1ly?^eeO}t?yrekkFO{mk?;z`P6RB>z zi5rJglVqV=z@3EI{xr~ix7E;v>}R#zox47EOwheC;ck`1%0Ah(TeGva7TrOptA351 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 BbTI$` diff --git a/guilib/src/images/r200.png b/guilib/src/images/r200.png index 8531ac07a1f9ce44e91190c4605455c81f0ea708..9a997ca778f7b0b5e6b02d9bf3d7a355172a57ec 100644 GIT binary patch delta 4132 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 iR53I&s+8tG#3Cs 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 diff --git a/guilib/src/images/seer_sense_DS80.png b/guilib/src/images/seer_sense_DS80.png index e2fa5fd395830db0d8bcd93c5203bd7e295c2b22..0485310a59c49d1f47152b6d68d4efcd15bb0346 100644 GIT binary patch delta 2364 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 aa80Q+7PR6^3dPw22*NpfIQ(iCD*X?uQ7D)I literal 7163 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 diff --git a/guilib/src/images/sense.png b/guilib/src/images/sense.png index 78775b518ab3cf4728d6924403e58b8d82ac9b3b..2ac3b601eac5a54bb259700ac570d166ff60dc0f 100644 GIT binary patch delta 6831 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 bwY|-||Jxv*^jitR0EAG{RQ{s)GWh=iBnA~a literal 9241 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 diff --git a/guilib/src/images/system-log-out.png b/guilib/src/images/system-log-out.png index fddbc2bce1a6b3742bba65b0fa950659873db5ab..fe7e21914b5d5a04fcc57d9bfeb8c55f9caa37af 100644 GIT binary patch delta 1805 zcmV+o2lDv64b%>hB}!*OO+f$vv5yPfP?@5`Tzg`fam}K zbua(`>RI+y?e7jT@qQ9J+u00v@9M??Vs0RI60puMM)kuev41~ExQK~z}7otI5) zT-6oFfA@XPrxQ;c$?+ud1e;PrA}flJ6~JW?QLj*>2&5ts6~ux~t87tL-IPM0QdzN~ z67)meS6x(U5iDR5wOOD@SaD?=r70x-PW$a(<`qtH#bM4(LhQ`7=}bqgp@KJaU7$R z!f_mmg#uTvUS(wD7($44P{0^N5CpvU>+4s6x7*HpM!`vcZyX2Pws9N>rBr&awMJ`A zk}Aux@H~%ld4POAhiy9uA=(fvfKsW%;9v!y1Z=nI`U7~LhvPW7u8UF%V@$_L6Ko8g z=TR&cahz7@Ez8P8Km$-Jl>l;k>Z3P+@B4V3hf)gHbpcZ4#+X#4F$ULl@jMU9vNGyL zdnOAoc&#~q?97Y)0G{Wi0EyznaT5A0fZHy--Qt8KP%R#j(DXHHcLITJ+jyRb@B66} z*LCrIKTVJ0IBC5kaNR_T-Dl!2Pd-`rqY&r%Mj#2LWm(v^jqm&EvqZTN08z4h1emtLC8A3N5y7&`#6 zj`sp3)o$B%rt-UoA3y{FpUupC@`IOOo*27w#py-sbo?j-u@}hqea6Sf+1c4)V`C$e z|NR!Nuap1Pbsm55#oCD%Ua#{`K2^0NvClxp!TcqeqW&x>383Cu`Cy3z!*#%^S3wNC>|Jr?B{pm#tjAr z2AGvd2H9H-?xsTr*?N}+>bKfpo7I5054 z*|TR^TwH9|L0kFK5`RDc4ArqQjtmd8^W~SIHNthlwh^|CkrJadmhblm=%wCuT_z_d zSzB9Uc6OF(wYsbR!2_c8b^Os$!ufgR(h|aPFky%e0@U_4CJb8w9S5sWAb1vtS;N&Dzzx#x_ufC%3=n;0gOt`p6bnhPV+FI%yz{D|XbCY;= zwL3t!Vl0(P2qCz2>lTjVkjv%Lbs$MY_2Na&e*7{2T3DbdCHXUF(!#^C_6;Lt_Y`E) zkX(ZxV0LyEV+{Fx9>;Ogcj*_%&nJ^KvDKllxEKl?f3QN+MgPob3JcD*iD z6uEm`J?sh4tr)W)wrw*%zd)^4qf)7~Q_8Po82%VoHhT^teT&etEb{q0%PY$?8v(Uijgu!&QXVMddp-h?GD?007_>I1IHnmk zsZ=V>nVFf0X7eAw%~!kl$^e)S<>hjj<>e*5yT3}UHp)nKghHW!>v{-(OCV!OtmM84 zwJ|8AXf&F%^L+5h2wF8_+ct*|9b$WXi^Xpis2(5YvGGxIeh$ZVG1?$ygp}EiuXB21 z3`#4SjRrtOD>(-P01OTu=Kj)ml*=Wa_~GMtzK3Ny#7Yrs-E!Jqe6pQD$Lz+SwWiTb z0s0<3Q-GD#CA5hd8mhE^iwIHzw;x^WwhJEQ zJZV-sAq=kTzH?Cc0YsPMx#)5IU(94iU;8hH%Z=xp9DV)(001R)MObuXVRU6WV{&C- zbY%cCFfukRFf}bPG*mG(Ix#akF)%GKH##sd<68n10000bbVXPybYx+4WjbwdWNBu3 v05UK#HZ3qUEip7yF*G_cGdeLaEigAaFfft^x~Y*yCkg-nNkvXXu0mjfOT1qr delta 1655 zcmV--28j984!sSKB@A;yNmK|32nc)#WQdV(7n3CiC4UAwNkl^O81_T|T!2nSdUHkEspR%~PNUPN%iXws_APhsIC`v~h$4Du$EQ?~H zz}2f)86Q7}5TX+l&|2ep9`F77`c>fVj`M+0uz%7E%fd8GEXzVlnO-ZUP)d=c$}kKZ z$Dv%Vkk98ZO$#AJ2civ7DwP-+sRERMW`}M#fa5q=mW6HGNGZ`;ca6lf);Nwsu~@{i z+MzcLBNG7yK&ey$$kAONg8^LE#c>>@l-RZnkSf<&rz*A9*tU)1I2eYJQ7<|(8Gy!V z&wpWN9t;O?947@x6eo^>)L8;xcM2~tAs`7soKX$BdF?IoD9rpqy z)oz+*rt)D>jS0`=v-$Z?zW3^@)00=OSc7P-u9q?py+E$(GBq{D-rgSD+uND^@3(Mk zi~O&y^W@7f*H66klBsrgK`G-;LOkmE={NfT`l(NHZ`(FUj~?a7ks~yl$@-FgAAkJy z4L+Ou0n>AHjGsSmdN*%UICqY@ci*)!3>&RMYjhm*w>RD>9vFe_-|xna8&oP4W@l${ zU3Z^ye{GFVpMQblGcz1JeHyRPKugKa=bt0Yb_r;uAdV?txG*>cz3P*3`0!yyM@Px! za@@Upx666!{(U}uVUF7LG)GUJqJP1vtS$jl70zx#y6ufC%7=n-bQOt7*-c<&zZ=4R>~K*uq1XNP!Wqd!1z z8iWv(N+pC4+`4rO%d*Jja_Ksdq@i~4B43JRt3kzti$>;M}mX$tB%zyWN4!!syPks0yciw!H$+0ntXU@?2`fG~MK8tW1FbqTo zK>0p;5Wt|nV$z@__k$q7&E*g%;#d;L5vAv!vgKtY9|%#J+M4G%e~irg!ug*{3oI)SAUwAKxrLbhVy_A zw30D!JL`3tv}j2)qLjjGdF<})5XVt_ld_ZNF2Up}*5~GavAn+SZ7nS&&B89Q*MSIt zy`ccv4Jhdg9LFIHLxg~0p+KorO1q}CQ_klwO_So~%Z>lkYCrk!_O?eHYv2*E18f1? zTI;B93bFu+av=l{AAfG6(9|dD)aw&et5sarC6;m8cnbsa`5cB2T)K4W!9P!&xc$w= zix=D=_#v>Sdk!K)i_kC(^7%Y#>udNek9xh%$&)83SIW4KivXf1Ox^-CN@-*q;|D&~ zYSo{gpAYf($)Q7sXf}6Q`S&ul<6}HAH9^kJVc9lXX`(13in1MF_w-t8 zq>}ip7C=ZlIR^s(jEo%S{_3}s%O#%r{*ySagJD|4QW7iGcG_8dvYkNJ>{_FgqUEOm zLl2)R!1~52N?gZ`j#fKG2XPc3dsfrZZ!x{SSuAjpt)rg_i&T002ovPDHLkV1hFq BESmrT diff --git a/guilib/src/images/t265.png b/guilib/src/images/t265.png index bb8d7e2f67ea7231548594094fbaae724a203fa6..c0738ad174caae145286c82c63ed9d5af22a2287 100644 GIT binary patch delta 3065 zcmV0Dy!50Qvv`0D$NK z0Cg|`0P0`>06Lfe02gqax=}olAw7RfNklb<*EY7J@Rg*!nktL_3+qZ93opXNY`+I+n09;+ohr{9c_pX1hufMTa zEOynaDhh!2;adb$6%asCt$&k#L;D_SocA6PNguC@q%ji4NSHay*7`qAC-r>^_e46Y zv_`gN5R6C~2aSx9?hANL?+AhzNqd#JV45S1mCl^f6(|bYk~^T}hr{9f7E-x>KqtWPJUwP&GcfWu0cmF_DZE4|(wuV!0J^~Y-v;dFyI;*C4!e1k_<`1*0 z*SJnpiURV;WO0L%6eZ2Z>-C~$>gkPiq-)9PQA~F;(#$7FSRd(ZM!ic)lUf0-aqX>l z|ChAC%vyf?>%YZs{N}I!i-^3KjYgy2fBEHC?*7@Ie~VtPY@yZw5axfbCjnFtqYdCK z1vD#bv!NP@_$LS(tW4G8)#F|KsKgw`$zx&$z-T;X&>sZg#d71;?eI*xO31T<@4fteUV7*`~K3ZEV?KB?P@w&$bnU-+UgshHd(6Q(ga72J$-NqWMu@UfKOi*-Fpw`9IkS~8`L);Lc1Wf zfqxwIUHQAe+j08G=Bt(2Di(~fNeRZ7pa>sBs<9j#9PodYul`be*MvO^A-^CT93F9S zc*NUpy~$+nkUYzqv_dFOvSj(SwbUjj?HC?nAVMcB)Umj2Q`TC}PfvLE&K+E}MO82c z*6TISIm8G>(L?n^Jmc<{zD!kBWLbtPcy&1E$@47S^NNb{!V6#I{d@PwvMeNuR{k-yRkob6 zV>g|$@)0nk1%6$Oe027Z+qZ9rY9wS)rqdZ!wdL~a5+lNVK1*rBD?I)5=bOanear#T z3qB$EpVp*F74Z@INEqAH6E?$&3lTOzaVMcXrtsj{{mBR+|;W z!GM!c1ss2;JwojPb;8SfY;$+8@444ciGJj*^bF#**v$gFnj_wWdR%YF^2Qe^Kl%W(S%N@*JtZ#+R0&m&dd|+L?CRgQkEt6 zPfvfjd33;Hv1B&C3{fLijib?s`N=6|S*ERKzV1eCM*sAOfa3B#<@A5Bn+1cDw{XRP zd^xA;@5H1l*lNwrhyRJsd#K4U+cg5XstQ$tD#lpyEXNqbY&Pe~C!b(8o0DaRz1>}m zwW-P7Y`2{T?x*sKvTZh7ymwSpg-Fh1XUu%Sz_Q{{&az3Xh3fAj2MNy;>y;^Os zSr&_#kc_i1vBz#!^zZ*yEX7RfYs_{@wwMA?tgpHqCyFr=J9Yk=jw7E!%^tmtId{+*_4xH z2M~WTM6}M%PMPcN!B`(I*#bU|ngZtopC_O*s)ih3x)|5}batch0BwFO+;v-h86?v9p z6^dSutIJDH@1K(Q3U+qJtd}c>;}OOhP}qNNw^Yt!#55HjVDym%T03dDp1K+G+7YVh zh9wbHM>lVA?}K~kC2K8%L7y^KL8H-_JP&z7ox?lNJ8!>r z-TR;V6(=>u^2TegMw~*Z9gfBv934=WJyc<`yN9(|>}fpb=Vu%o9L3zBX|JXxjgo)m zw3y0T5&9TOw6nveVe;Oyw|~fwKKw8=9`UglOP&`^4Xh3^hPPgSjSoJ!7i#XfM_Yr~ z?n+zV)<^_8ylcgf#ZPWbxR>5(XoLSR=Czj@F_9}0R_i72zxOVAQD9StE+3?eB*U~^xyZ;2)-3*wSp}kzHNXat!}>gzXkya{Jw^6zP2M9 z&mlf(c&ZJ6n)Nh-v6d{$IX*e2a*pR;_#%VBpc@iJSg%*irZb#Z#yb<#w-|q93ChWe zqBtq?;>iVTx*c#mS@*D{;}5>!(e;r>S)Q|hc$4eZiu(^9ps|y@zP{r8{G9oG#%j52 z(YlXJ-)2GyrC>T-dBj4~IYrHA8WT2);L%9zrI|jF0(^|uMrsZ$P!dLjEYFxuFZhpt z|2N9AWN-g~JkPP#QdN%8ctU^CD=5lHOC+VjXgH!M3bKQP{qMZ=wO{_@zy14v#9EWK zm7?e)oo<74i!Z9PbpkC|pa|r7!QqXYcpoV2L-joM)YDCK(>hJ7Au>h+P3_|$O-uLE zmM2XYn=RgZ&>H6i?1i?x<44<3JzWm%%GYdq4~ zj*_U0hR&{KQeJ%VD>&!=S*FwJ!DKRd`v*Vxr^9dk<+oX{wyE?n(onc{w;VY2R?cpD zhu6j;o38+h^FDI_f%mUVxYp~f>&FjK9(`7C8TEfLV>r8RADMzjvGEL` zeR4*FZ#+h6onJ)b2+9d~Y_}Dzs`%XJZqIJqy!pz-#q>}2_xIlYU!4WjgzhzOI{*Lx zC3HntbYx+4WjbSWWnpw>05UK#HZ3qUEip7yF*G_cGdeLaEigAaFfij=0u=xN03~!q zSaf7zbY(hiZ)7cLW_bWIFfukRFf}bPG*mG(Ix#akF)%GKH##sdk_Nh|00000NkvXX Hu0mjfdV&y@ delta 2888 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 D?&+a6 delta 1858 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 M;wyzpxOv$B0r}L&ZU6uP literal 11049 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 F{|9Q84+H=J diff --git a/guilib/src/images/xtion_pro_live.png b/guilib/src/images/xtion_pro_live.png index cf464a8bd6e75e6c312d01a0603ad9a1551f425e..7864d343f0ae16d7332b271209bab4e244612cbe 100644 GIT binary patch delta 4547 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 delta 4375 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 TG;y&V00000NkvXXu0mjfalGJ` delta 2393 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 LNkvXXu0mjflHa%J From a9146f5be6029e7e7d376b2545b7c6ad09c2a123 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Fri, 22 May 2026 09:33:17 -0700 Subject: [PATCH 115/126] Fix #1698 (#1708) --- tools/Reprocess/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/Reprocess/main.cpp b/tools/Reprocess/main.cpp index ee9a8bcd..25ac9e9e 100644 --- a/tools/Reprocess/main.cpp +++ b/tools/Reprocess/main.cpp @@ -1090,7 +1090,7 @@ int main(int argc, char * argv[]) uInsert(globalMapStats, odomInfo.statistics(pose)); } - else if(framesToSkip==0 && intermediateNodes && lastUpdateStamp > 0.0 && (data.stamp() < lastUpdateStamp + rtabmapUpdateRate)) + else if(framesToSkip==0 && intermediateNodes && lastUpdateStamp > 0.0 && data.stamp() > lastUpdateStamp && (data.stamp() < lastUpdateStamp + rtabmapUpdateRate)) { data.setId(-1); // intermediate node } From 49aa56661316470f03a92cc204a932d6d7a6394b Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sat, 23 May 2026 13:36:15 -0700 Subject: [PATCH 116/126] Fixed empty global map on ros (regression from #1698) (#1709) --- corelib/include/rtabmap/core/Memory.h | 2 +- corelib/src/Memory.cpp | 5 +++-- corelib/src/Rtabmap.cpp | 2 +- corelib/src/SensorData.cpp | 16 ++++++++++++++++ 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/corelib/include/rtabmap/core/Memory.h b/corelib/include/rtabmap/core/Memory.h index 52979c14..8c3306ad 100644 --- a/corelib/include/rtabmap/core/Memory.h +++ b/corelib/include/rtabmap/core/Memory.h @@ -144,7 +144,7 @@ public: void deleteLocation(int locationId, std::list * deletedWords = 0); void saveLocationData(int locationId); void removeLink(int idA, int idB); - void removeRawData(int id, bool image = true, bool scan = true, bool userData = true); + void removeRawData(int id, bool image = true, bool scan = true, bool userData = true, bool occupancyGrid = true); int reduceNode(int id, float maxDistance = 0.0f, bool keepLinkedInDb = false, int direction = 0); //getters diff --git a/corelib/src/Memory.cpp b/corelib/src/Memory.cpp index d3d8cdd0..3b22e79f 100644 --- a/corelib/src/Memory.cpp +++ b/corelib/src/Memory.cpp @@ -3176,7 +3176,7 @@ void Memory::removeLink(int oldId, int newId) } } -void Memory::removeRawData(int id, bool image, bool scan, bool userData) +void Memory::removeRawData(int id, bool image, bool scan, bool userData, bool occupancyGrid) { UDEBUG("id=%d image=%d scan=%d userData=%d", id, image?1:0, scan?1:0, userData?1:0); Signature * s = this->_getSignature(id); @@ -3185,7 +3185,8 @@ void Memory::removeRawData(int id, bool image, bool scan, bool userData) s->sensorData().clearRawData( image && (!_reextractLoopClosureFeatures || !_registrationPipeline->isImageRequired()), scan && !_registrationPipeline->isScanRequired(), - userData && !_registrationPipeline->isUserDataRequired()); + userData && !_registrationPipeline->isUserDataRequired(), + occupancyGrid); } } diff --git a/corelib/src/Rtabmap.cpp b/corelib/src/Rtabmap.cpp index 13652171..af11082d 100644 --- a/corelib/src/Rtabmap.cpp +++ b/corelib/src/Rtabmap.cpp @@ -4423,7 +4423,7 @@ bool Rtabmap::process( } if(!_rawDataKept) { - _memory->removeRawData(signature->id(), true, !_neighborLinkRefining && !_proximityBySpace, true); + _memory->removeRawData(signature->id(), true, !_neighborLinkRefining && !_proximityBySpace, true, false); } // Localization mode and saving localization data: save odometry covariance in a prior link diff --git a/corelib/src/SensorData.cpp b/corelib/src/SensorData.cpp index 8b83795d..8954af0f 100644 --- a/corelib/src/SensorData.cpp +++ b/corelib/src/SensorData.cpp @@ -997,6 +997,14 @@ void SensorData::clearCompressedData(bool images, bool scan, bool userData, bool _groundCellsCompressed=cv::Mat(); _emptyCellsCompressed=cv::Mat(); _obstacleCellsCompressed=cv::Mat(); + + if( _groundCellsCompressed.empty() && _groundCellsRaw.empty() && + _obstacleCellsCompressed.empty() && _obstacleCellsRaw.empty() && + _emptyCellsCompressed.empty() && _emptyCellsRaw.empty()) + { + _cellSize = 0.0f; + _viewPoint = cv::Point3f(); + } } } void SensorData::clearRawData(bool images, bool scan, bool userData, bool occupancyGrid) @@ -1024,6 +1032,14 @@ void SensorData::clearRawData(bool images, bool scan, bool userData, bool occupa _groundCellsRaw=cv::Mat(); _emptyCellsRaw=cv::Mat(); _obstacleCellsRaw=cv::Mat(); + + if( _groundCellsCompressed.empty() && _groundCellsRaw.empty() && + _obstacleCellsCompressed.empty() && _obstacleCellsRaw.empty() && + _emptyCellsCompressed.empty() && _emptyCellsRaw.empty()) + { + _cellSize = 0.0f; + _viewPoint = cv::Point3f(); + } } } From 47564743678b77f6d084fa2ddfbb2cedbecd2963 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Tue, 26 May 2026 17:43:34 -0700 Subject: [PATCH 117/126] Adding support for AprilTag v3 library (#1702) * Adding support for AprilTag v3 library * New parameter: Marker/Strategy (default opencv-aruco as before). Optimized multicameras marker detection (do only once with stitched image) * Exposed all AprilTag parameters. Sharing Marker/Dictionary between opencv and apriltag implementation. Added new Marker/Lengths parameter. * Support AprilTAg library not built with aruco * cleanup * Added marker range support * Make apriltag detection in same orientation than opencv * fixed shenanigans when rendering markers with multi cameras * Don't update odom cache (localization mode) when not moving (detecting loop/landmark or not) * fixed some quirks (camera viewer + tag working) * Setting quad decimate to 1 by default * typo * Implemented OptimizerG2O::loadGraph() * added apriltag's aruco support info in the cmake config summary * fixed warning * fixed isam2 assert when using landmarks * Fixed homography on multicam * Adding marker detection time for convenience * Added missing MIP 36h12 tag family in UI --- CMakeLists.txt | 50 +- Version.h.in | 2 + corelib/include/rtabmap/core/MarkerDetector.h | 16 +- corelib/include/rtabmap/core/Parameters.h | 18 +- .../rtabmap/core/optimizer/OptimizerG2O.h | 6 + .../rtabmap/core/optimizer/OptimizerGTSAM.h | 1 + corelib/src/CMakeLists.txt | 7 + corelib/src/Graph.cpp | 9 +- corelib/src/MarkerDetector.cpp | 961 ++++-- corelib/src/Parameters.cpp | 11 +- corelib/src/Rtabmap.cpp | 3 +- corelib/src/optimizer/OptimizerG2O.cpp | 384 +++ corelib/src/optimizer/OptimizerGTSAM.cpp | 47 +- examples/WifiMapping/WifiThread.h | 3 +- guilib/include/rtabmap/gui/CameraViewer.h | 1 + .../include/rtabmap/gui/PreferencesDialog.h | 1 + guilib/src/AboutDialog.cpp | 7 + guilib/src/CameraViewer.cpp | 22 +- guilib/src/MainWindow.cpp | 4 + guilib/src/PreferencesDialog.cpp | 146 +- guilib/src/ui/aboutDialog.ui | 3015 +++++++++-------- guilib/src/ui/preferencesDialog.ui | 966 ++++-- tools/Export/main.cpp | 17 +- 23 files changed, 3561 insertions(+), 2136 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7844ae1c..60986d09 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,7 +22,7 @@ SET(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake_modules") ####################### SET(RTABMAP_MAJOR_VERSION 0) SET(RTABMAP_MINOR_VERSION 23) -SET(RTABMAP_PATCH_VERSION 6) +SET(RTABMAP_PATCH_VERSION 7) SET(RTABMAP_VERSION ${RTABMAP_MAJOR_VERSION}.${RTABMAP_MINOR_VERSION}.${RTABMAP_PATCH_VERSION}) @@ -230,6 +230,7 @@ option(WITH_FASTCV "Include FastCV support" ON) option(WITH_OPENMP "Include OpenMP support" ON) option(WITH_OPENGV "Include OpenGV support" ON) option(BUILD_OPENGV "Build OpenGV internally instead of using the system one" OFF) +option(WITH_APRILTAG "Include AprilTag support" OFF) IF(MOBILE_BUILD) option(PCL_OMP "With PCL OMP implementations" OFF) ELSE() @@ -866,6 +867,20 @@ IF(WITH_FASTCV) ENDIF(FastCV_FOUND) ENDIF(WITH_FASTCV) +IF(WITH_APRILTAG) + FIND_PACKAGE(apriltag QUIET) + IF(apriltag_FOUND) + get_target_property(APRILTAG_LOCATION apriltag::apriltag LOCATION) + get_target_property(APRILTAG_INCLUDES apriltag::apriltag INTERFACE_INCLUDE_DIRECTORIES) + FIND_FILE(apriltag_aruco_4x4_50_header NAMES tagAruco4x4_50.h PATH_SUFFIXES aruco PATHS ${APRILTAG_INCLUDES} NO_DEFAULT_PATH) + SET(WITH_APRILTAG_ARUCO NO) + IF(apriltag_aruco_4x4_50_header) + SET(WITH_APRILTAG_ARUCO YES) + ENDIF() + MESSAGE(STATUS "Found apriltag (with aruco=${WITH_APRILTAG_ARUCO}): ${APRILTAG_LOCATION} ${APRILTAG_INCLUDES}") + ENDIF(apriltag_FOUND) +ENDIF(WITH_APRILTAG) + IF(WITH_OPENGV OR okvis_FOUND) if(NOT BUILD_OPENGV) FIND_PACKAGE(opengv QUIET) @@ -1066,6 +1081,12 @@ ENDIF(NOT Open3D_FOUND) IF(NOT FastCV_FOUND) SET(FASTCV "//") ENDIF(NOT FastCV_FOUND) +IF(NOT apriltag_FOUND) + SET(APRILTAG "//") + SET(APRILTAG_ARUCO "//") +ELSEIF(NOT WITH_APRILTAG_ARUCO) + SET(APRILTAG_ARUCO "//") +ENDIF() IF(NOT opengv_FOUND OR NOT WITH_OPENGV) SET(OPENGV "//") ENDIF(NOT opengv_FOUND OR NOT WITH_OPENGV) @@ -1475,28 +1496,33 @@ ENDIF(PCL_COMPILE_OPTIONS) MESSAGE(STATUS "") MESSAGE(STATUS "Optional dependencies ('*' affects some default parameters) :") IF(OpenCV_FOUND) + IF(OPENCV_ARUCO_FOUND) + set(ARUCO_STR "YES") + ELSE() + set(ARUCO_STR "NO") + ENDIF() IF(OpenCV_VERSION_MAJOR EQUAL 2) IF(OPENCV_NONFREE_FOUND) - MESSAGE(STATUS " *With OpenCV 2 nonfree module (SIFT/SURF) = YES (License: Non commercial)") + MESSAGE(STATUS " *With OpenCV 2 nonfree module (SIFT/SURF) = YES, aruco = ${ARUCO_STR} (License: Non commercial)") ELSE() - MESSAGE(STATUS " *With OpenCV 2 nonfree module (SIFT/SURF) = NO (not found, License: BSD)") + MESSAGE(STATUS " *With OpenCV 2 nonfree module (SIFT/SURF) = NO, aruco = ${ARUCO_STR} (not found, License: BSD)") ENDIF() ELSE() IF(OPENCV_XFEATURES2D_FOUND) IF(NONFREE STREQUAL "//") IF((OpenCV_VERSION_MAJOR LESS 4) OR ((OpenCV_VERSION_MAJOR EQUAL 4) AND (OpenCV_VERSION_MINOR LESS 5))) - MESSAGE(STATUS " *With OpenCV ${OpenCV_VERSION} xfeatures2d = YES, nonfree = NO (License: BSD)") + MESSAGE(STATUS " *With OpenCV ${OpenCV_VERSION} xfeatures2d = YES, nonfree = NO, aruco = ${ARUCO_STR} (License: BSD)") ELSE() - MESSAGE(STATUS " *With OpenCV ${OpenCV_VERSION} xfeatures2d = YES, nonfree = NO (License: Apache 2)") + MESSAGE(STATUS " *With OpenCV ${OpenCV_VERSION} xfeatures2d = YES, nonfree = NO, aruco = ${ARUCO_STR} (License: Apache 2)") ENDIF() ELSE() - MESSAGE(STATUS " *With OpenCV ${OpenCV_VERSION} xfeatures2d = YES, nonfree = YES (License: Non commercial)") + MESSAGE(STATUS " *With OpenCV ${OpenCV_VERSION} xfeatures2d = YES, nonfree = YES, aruco = ${ARUCO_STR} (License: Non commercial)") ENDIF() ELSE() IF((OpenCV_VERSION_MAJOR LESS 4) OR ((OpenCV_VERSION_MAJOR EQUAL 4) AND (OpenCV_VERSION_MINOR LESS 5))) - MESSAGE(STATUS " *With OpenCV ${OpenCV_VERSION} xfeatures2d = NO, nonfree = NO (License: BSD)") + MESSAGE(STATUS " *With OpenCV ${OpenCV_VERSION} xfeatures2d = NO, nonfree = NO, aruco = ${ARUCO_STR} (License: BSD)") ELSE() - MESSAGE(STATUS " *With OpenCV ${OpenCV_VERSION} xfeatures2d = NO, nonfree = NO (License: Apache 2)") + MESSAGE(STATUS " *With OpenCV ${OpenCV_VERSION} xfeatures2d = NO, nonfree = NO, aruco = ${ARUCO_STR} (License: Apache 2)") ENDIF() ENDIF() ENDIF() @@ -1571,6 +1597,14 @@ ELSE() MESSAGE(STATUS " With FastCV = NO (FastCV not found)") ENDIF() +IF(apriltag_FOUND) +MESSAGE(STATUS " With AprilTag ${apriltag_VERSION} = YES (aruco=${WITH_APRILTAG_ARUCO}) (License: BSD 2-Clause License)") +ELSEIF(NOT WITH_APRILTAG) +MESSAGE(STATUS " With AprilTag = NO (WITH_APRILTAG=OFF)") +ELSE() +MESSAGE(STATUS " With AprilTag = NO (apriltag not found)") +ENDIF() + IF(PDAL_FOUND) MESSAGE(STATUS " With PDAL ${PDAL_VERSION} = YES (License: BSD)") ELSEIF(NOT WITH_PDAL) diff --git a/Version.h.in b/Version.h.in index 381dcaa3..2bb9d46a 100644 --- a/Version.h.in +++ b/Version.h.in @@ -93,6 +93,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. @TORCH@#define RTABMAP_TORCH @PYTHON@#define RTABMAP_PYTHON @MADGWICK@#define RTABMAP_MADGWICK +@APRILTAG@#define RTABMAP_APRILTAG +@APRILTAG_ARUCO@#define RTABMAP_APRILTAG_WITH_ARUCO #include diff --git a/corelib/include/rtabmap/core/MarkerDetector.h b/corelib/include/rtabmap/core/MarkerDetector.h index d628ca02..630bf87f 100644 --- a/corelib/include/rtabmap/core/MarkerDetector.h +++ b/corelib/include/rtabmap/core/MarkerDetector.h @@ -57,6 +57,12 @@ private: }; class RTABMAP_CORE_EXPORT MarkerDetector { + +public: + enum Strategy { + kStrategyOpencv, + kStrategyApriltag + }; public: MarkerDetector(const ParametersMap & parameters = ParametersMap()); @@ -84,15 +90,19 @@ public: cv::Mat * imageWithDetections = 0); private: -#ifdef HAVE_OPENCV_ARUCO - cv::Ptr detectorParams_; - float markerLength_; + Strategy strategy_; + float markerLength_; + std::map markerLengths_; float maxDepthError_; float maxRange_; float minRange_; int dictionaryId_; +#ifdef HAVE_OPENCV_ARUCO + cv::Ptr detectorParams_; cv::Ptr dictionary_; #endif + void * apriltagLibDetector_; + void * apriltagLibFamily_; }; } /* namespace rtabmap */ diff --git a/corelib/include/rtabmap/core/Parameters.h b/corelib/include/rtabmap/core/Parameters.h index 23cace9e..3944acf6 100644 --- a/corelib/include/rtabmap/core/Parameters.h +++ b/corelib/include/rtabmap/core/Parameters.h @@ -917,19 +917,29 @@ class RTABMAP_CORE_EXPORT Parameters RTABMAP_PARAM(GridGlobal, ProbClampingMax, float, 0.971, "Probability clamping maximum (value between 0 and 1)."); RTABMAP_PARAM(GridGlobal, FloodFillDepth, unsigned int, 0, "Flood fill filter (0=disabled), used to remove empty cells outside the map. The flood fill is done at the specified depth (between 1 and 16) of the OctoMap."); - RTABMAP_PARAM(Marker, Dictionary, int, 0, "Dictionary to use: DICT_ARUCO_4X4_50=0, DICT_ARUCO_4X4_100=1, DICT_ARUCO_4X4_250=2, DICT_ARUCO_4X4_1000=3, DICT_ARUCO_5X5_50=4, DICT_ARUCO_5X5_100=5, DICT_ARUCO_5X5_250=6, DICT_ARUCO_5X5_1000=7, DICT_ARUCO_6X6_50=8, DICT_ARUCO_6X6_100=9, DICT_ARUCO_6X6_250=10, DICT_ARUCO_6X6_1000=11, DICT_ARUCO_7X7_50=12, DICT_ARUCO_7X7_100=13, DICT_ARUCO_7X7_250=14, DICT_ARUCO_7X7_1000=15, DICT_ARUCO_ORIGINAL = 16, DICT_APRILTAG_16h5=17, DICT_APRILTAG_25h9=18, DICT_APRILTAG_36h10=19, DICT_APRILTAG_36h11=20"); - RTABMAP_PARAM(Marker, Length, float, 0, "The length (m) of the markers' side. 0 means automatic marker length estimation using the depth image (the camera should look at the marker perpendicularly for initialization)."); + RTABMAP_PARAM(Marker, Strategy, int, 0, "Marker detection implementation: 0=OpenCV, 1=AprilTag"); + RTABMAP_PARAM(Marker, Dictionary, int, 0, "Dictionary to use: DICT_ARUCO_4X4_50=0, DICT_ARUCO_4X4_100=1, DICT_ARUCO_4X4_250=2, DICT_ARUCO_4X4_1000=3, DICT_ARUCO_5X5_50=4, DICT_ARUCO_5X5_100=5, DICT_ARUCO_5X5_250=6, DICT_ARUCO_5X5_1000=7, DICT_ARUCO_6X6_50=8, DICT_ARUCO_6X6_100=9, DICT_ARUCO_6X6_250=10, DICT_ARUCO_6X6_1000=11, DICT_ARUCO_7X7_50=12, DICT_ARUCO_7X7_100=13, DICT_ARUCO_7X7_250=14, DICT_ARUCO_7X7_1000=15, DICT_ARUCO_ORIGINAL = 16, DICT_APRILTAG_16h5=17, DICT_APRILTAG_25h9=18, DICT_APRILTAG_36h10=19, DICT_APRILTAG_36h11=20, DICT_ARUCO_MIP_36H12=21"); + RTABMAP_PARAM(Marker, Length, float, 0, "The length (m) of the markers' side. Value <=0 means automatic marker length estimation using the depth image (the camera should look at the marker perpendicularly for initialization). If 0, the length is estimated only on the first marker detected, then re-used for all next detections (i.e., this assumes that markers have all the same length). With <0, the length is estimated once for each unique marker, then re-used for next detections with the same marker ID."); + RTABMAP_PARAM_STR(Marker, Lengths, "", uFormat("List of markers to detect. Format is the marker's ID followed by its length (in meters), multiple markers are separated by a vertical line (\"id1 length|id2 length\"). We can also define a range of markers with \"id1:id2 length\" (id2 included). If empty, all markers of the chosen dictionary can be detected and their length is set/estimated based on %s. For example, to detect markers 12 and 14 with lengths of 8 and 15 cm respectively, and all markers between 30 and 40 with a length of 10 cm, set \"12 0.08|14 0.15|30:40 0.1\".", kMarkerLength().c_str()).c_str()); RTABMAP_PARAM(Marker, MaxDepthError, float, 0.01, uFormat("Maximum depth error between all corners of a marker when estimating the marker length (when %s is 0). The smaller it is, the more perpendicular the camera should be toward the marker to initialize the length.", kMarkerLength().c_str())); RTABMAP_PARAM(Marker, VarianceLinear, float, 0.001, uFormat("Linear variance to set on marker detections. If %s is enabled and %s=2 (GTSAM): it is the variance of the range factor, with 9999 to disable range factor and to do only bearing.", kMarkerVarianceOrientationIgnored().c_str(), kOptimizerStrategy().c_str())); RTABMAP_PARAM(Marker, VarianceAngular, float, 0.01, uFormat("Angular variance to set on marker detections. If %s is enabled, it is ignored with %s=1 (g2o) and it corresponds to bearing variance with %s=2 (GTSAM).", kMarkerVarianceOrientationIgnored().c_str(), kOptimizerStrategy().c_str(), kOptimizerStrategy().c_str())); RTABMAP_PARAM(Marker, VarianceOrientationIgnored, bool, false, uFormat("When this setting is false, the landmark's orientation is optimized during graph optimization. When this setting is true, only the position of the landmark is optimized. This can be useful when the landmark's orientation estimation is not reliable. Note that for %s=1 (g2o), only %s needs be set if we ignore orientation. For %s=2 (GTSAM), instead of optimizing the landmark's position directly, a bearing/range factor is used, with %s as the variance of the range factor (with 9999 to optimize the position with only a bearing factor) and %s as the variance of the bearing factor (pitch/yaw).", kOptimizerStrategy().c_str(), kMarkerVarianceLinear().c_str(), kOptimizerStrategy().c_str(), kMarkerVarianceLinear().c_str(), kMarkerVarianceAngular().c_str())); - RTABMAP_PARAM(Marker, CornerRefinementMethod, int, 0, "Corner refinement method (0: None, 1: Subpixel, 2:contour, 3: AprilTag2). For OpenCV <3.3.0, this is \"doCornerRefinement\" parameter: set 0 for false and 1 for true."); RTABMAP_PARAM(Marker, MaxRange, float, 0.0, "Maximum range in which markers will be detected. <=0 for unlimited range."); RTABMAP_PARAM(Marker, MinRange, float, 0.0, "Miniminum range in which markers will be detected. <=0 for unlimited range."); - RTABMAP_PARAM_STR(Marker, Priors, "", "World prior locations of the markers. The map will be transformed in marker's world frame when a tag is detected. Format is the marker's ID followed by its position (angles in rad), markers are separated by vertical line (\"id1 x y z roll pitch yaw|id2 x y z roll pitch yaw\"). Example: \"1 0 0 1 0 0 0|2 1 0 1 0 0 1.57\" (marker 2 is 1 meter forward than marker 1 with 90 deg yaw rotation)."); + RTABMAP_PARAM_STR(Marker, Priors, "", "World prior locations of the markers. The map will be transformed in marker's world frame when a tag is detected. Format is the marker's ID followed by its position (angles in rad), multiple markers are separated by vertical line (\"id1 x y z roll pitch yaw|id2 x y z roll pitch yaw\"). Example: \"1 0 0 1 0 0 0|2 1 0 1 0 0 1.57\" (marker 2 is 1 meter forward than marker 1 with 90 deg yaw rotation)."); RTABMAP_PARAM(Marker, PriorsVarianceLinear, float, 0.001, "Linear variance to set on marker priors."); RTABMAP_PARAM(Marker, PriorsVarianceAngular, float, 0.001, "Angular variance to set on marker priors."); + RTABMAP_PARAM(MarkerAprilTag, NThreads, int, 1, "How many threads should be used?"); + RTABMAP_PARAM(MarkerAprilTag, QuadDecimate, float, 1.0, "Detection of quads can be done on a lower-resolution image, improving speed at a cost of pose accuracy and a slight decrease in detection rate. Decoding the binary payload is still done at full resolution."); + RTABMAP_PARAM(MarkerAprilTag, QuadSigma, float, 0.0, "What Gaussian blur should be applied to the segmented image (used for quad detection?) Parameter is the standard deviation in pixels. Very noisy images benefit from non-zero values (e.g. 0.8)."); + RTABMAP_PARAM(MarkerAprilTag, RefineEdges, bool, true, uFormat("When true, the edges of the each quad are adjusted to \"snap to\" strong gradients nearby. This is useful when decimation is employed, as it can increase the quality of the initial quad estimate substantially. Generally recommended to be on (true). Very computationally inexpensive. Option is ignored if %s = 1.", kMarkerAprilTagQuadDecimate().c_str())); + RTABMAP_PARAM(MarkerAprilTag, DecodeSharpening, double, 0.25, "How much sharpening should be done to decoded images? This can help decode small tags but may or may not help in odd lighting conditions or low light conditions."); + RTABMAP_PARAM(MarkerAprilTag, Debug, bool, false, uFormat("When true, write a variety of debugging images to the working directory where the app started (not %s) at various stages through the detection process. (Somewhat slow).", kRtabmapWorkingDirectory().c_str())); + + RTABMAP_PARAM(MarkerOpenCV, CornerRefinementMethod, int, 0, "Corner refinement method for OpenCV strategy (0: None, 1: Subpixel, 2:contour, 3: AprilTag2). For OpenCV <3.3.0, this is \"doCornerRefinement\" parameter: set 0 for false and 1 for true."); + RTABMAP_PARAM(ImuFilter, MadgwickGain, double, 0.1, "Gain of the filter. Higher values lead to faster convergence but more noise. Lower values lead to slower convergence but smoother signal, belongs in [0, 1]."); RTABMAP_PARAM(ImuFilter, MadgwickZeta, double, 0.0, "Gyro drift gain (approx. rad/s), belongs in [-1, 1]."); diff --git a/corelib/include/rtabmap/core/optimizer/OptimizerG2O.h b/corelib/include/rtabmap/core/optimizer/OptimizerG2O.h index 30117d6e..d8c95279 100644 --- a/corelib/include/rtabmap/core/optimizer/OptimizerG2O.h +++ b/corelib/include/rtabmap/core/optimizer/OptimizerG2O.h @@ -41,6 +41,12 @@ public: static bool isCSparseAvailable(); static bool isCholmodAvailable(); +public: + static bool loadGraph( + const std::string & fileName, + std::map & poses, + std::multimap & edgeConstraints); + public: OptimizerG2O(const ParametersMap & parameters = ParametersMap()); virtual ~OptimizerG2O() {} diff --git a/corelib/include/rtabmap/core/optimizer/OptimizerGTSAM.h b/corelib/include/rtabmap/core/optimizer/OptimizerGTSAM.h index 2d3eb6ba..c2b5a0f3 100644 --- a/corelib/include/rtabmap/core/optimizer/OptimizerGTSAM.h +++ b/corelib/include/rtabmap/core/optimizer/OptimizerGTSAM.h @@ -77,6 +77,7 @@ private: std::vector lastAddedConstraints_; int lastSwitchId_; std::set addedPoses_; + std::map isLandmarkWithRotation_; // persists across iSAM2 incremental calls std::pair lastRootFactorIndex_; }; diff --git a/corelib/src/CMakeLists.txt b/corelib/src/CMakeLists.txt index f787c40f..8f82257b 100644 --- a/corelib/src/CMakeLists.txt +++ b/corelib/src/CMakeLists.txt @@ -533,6 +533,13 @@ IF(FastCV_FOUND) ) ENDIF(FastCV_FOUND) +IF(apriltag_FOUND) + SET(LIBRARIES + apriltag::apriltag + ${LIBRARIES} + ) +ENDIF(apriltag_FOUND) + IF(opengv_FOUND) SET(LIBRARIES ${LIBRARIES} diff --git a/corelib/src/Graph.cpp b/corelib/src/Graph.cpp index 1cc96a05..05f2ac91 100644 --- a/corelib/src/Graph.cpp +++ b/corelib/src/Graph.cpp @@ -218,7 +218,14 @@ bool importPoses( else if(format == 4) // g2o { std::multimap constraintsTmp; - UERROR("Cannot import from g2o format because it is not yet supported!"); + if(OptimizerG2O::loadGraph(filePath, poses, constraintsTmp)) + { + if(constraints) + { + *constraints = constraintsTmp; + } + return true; + } return false; } else diff --git a/corelib/src/MarkerDetector.cpp b/corelib/src/MarkerDetector.cpp index 9ff9f694..db3a4fd6 100644 --- a/corelib/src/MarkerDetector.cpp +++ b/corelib/src/MarkerDetector.cpp @@ -28,17 +28,179 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include + +#ifdef HAVE_OPENCV_ARUCO +#if CV_MAJOR_VERSION < 4 || (CV_MAJOR_VERSION == 4 && CV_MINOR_VERSION <8) +namespace cv{ + namespace aruco { + static const int DICT_ARUCO_MIP_36h12 = 21; +#if CV_MAJOR_VERSION < 4 || (CV_MAJOR_VERSION == 4 && CV_MINOR_VERSION <7) + typedef PREDEFINED_DICTIONARY_NAME PredefinedDictionaryType; +#endif + } +} +#endif +#endif + +#ifdef RTABMAP_APRILTAG +extern "C" { +#include "apriltag/apriltag.h" +#include "apriltag/apriltag_pose.h" +#ifdef RTABMAP_APRILTAG_WITH_ARUCO +#include "apriltag/aruco/tagAruco4x4_50.h" +#include "apriltag/aruco/tagAruco4x4_100.h" +#include "apriltag/aruco/tagAruco4x4_250.h" +#include "apriltag/aruco/tagAruco4x4_1000.h" +#include "apriltag/aruco/tagAruco5x5_50.h" +#include "apriltag/aruco/tagAruco5x5_100.h" +#include "apriltag/aruco/tagAruco5x5_250.h" +#include "apriltag/aruco/tagAruco5x5_1000.h" +#include "apriltag/aruco/tagAruco6x6_50.h" +#include "apriltag/aruco/tagAruco6x6_100.h" +#include "apriltag/aruco/tagAruco6x6_250.h" +#include "apriltag/aruco/tagAruco6x6_1000.h" +#include "apriltag/aruco/tagAruco7x7_50.h" +#include "apriltag/aruco/tagAruco7x7_100.h" +#include "apriltag/aruco/tagAruco7x7_250.h" +#include "apriltag/aruco/tagAruco7x7_1000.h" +#include "apriltag/aruco/tagArucoMIP36h12.h" +#endif +#include "apriltag/tag16h5.h" +#include "apriltag/tag25h9.h" +#include "apriltag/tag36h10.h" +#include "apriltag/tag36h11.h" +} + +#ifndef HAVE_OPENCV_ARUCO +// To match opencv::aruco module, add opencv::aruco dictionary enum +namespace cv{ +namespace aruco { +enum PredefinedDictionaryType { + DICT_4X4_50 = 0, + DICT_4X4_100, + DICT_4X4_250, + DICT_4X4_1000, + DICT_5X5_50, + DICT_5X5_100, + DICT_5X5_250, + DICT_5X5_1000, + DICT_6X6_50, + DICT_6X6_100, + DICT_6X6_250, + DICT_6X6_1000, + DICT_7X7_50, + DICT_7X7_100, + DICT_7X7_250, + DICT_7X7_1000, + DICT_ARUCO_ORIGINAL, + DICT_APRILTAG_16h5, + DICT_APRILTAG_25h9, + DICT_APRILTAG_36h10, + DICT_APRILTAG_36h11, + DICT_ARUCO_MIP_36h12 +}; +} +} +#endif +#endif namespace rtabmap { -MarkerDetector::MarkerDetector(const ParametersMap & parameters) +#ifdef RTABMAP_APRILTAG + +#define ARUCO_CASE(N, TOTAL) \ +case cv::aruco::DICT_##N##X##N##_##TOTAL: \ + af = tagAruco##N##x##N##_##TOTAL##_create(); \ + break; +#define APRILTAG_CASE(N1, N2) \ +case cv::aruco::DICT_APRILTAG_##N1##h##N2: \ + af = tag##N1##h##N2##_create(); \ + break; + +apriltag_family_t * createAprilTagPredefinedDictionary(int opencvArucoDictionary) +{ + apriltag_family_t * af = NULL; + switch(opencvArucoDictionary) + { + APRILTAG_CASE(16, 5) + APRILTAG_CASE(25, 9) + APRILTAG_CASE(36, 10) + APRILTAG_CASE(36, 11) +#ifdef RTABMAP_APRILTAG_WITH_ARUCO + ARUCO_CASE(4, 50) + ARUCO_CASE(4, 100) + ARUCO_CASE(4, 250) + ARUCO_CASE(4, 1000) + ARUCO_CASE(5, 50) + ARUCO_CASE(5, 100) + ARUCO_CASE(5, 250) + ARUCO_CASE(5, 1000) + ARUCO_CASE(6, 50) + ARUCO_CASE(6, 100) + ARUCO_CASE(6, 250) + ARUCO_CASE(6, 1000) + ARUCO_CASE(7, 50) + ARUCO_CASE(7, 100) + ARUCO_CASE(7, 250) + ARUCO_CASE(7, 1000) + case cv::aruco::DICT_ARUCO_MIP_36h12: + af = tagArucoMIP36h12_create(); + break; +#endif + default: + break; + } + return af; +} +void destroyAprilTagDictionary(apriltag_family_t * dictionary) +{ + if(dictionary == NULL) + { + return; + } + const char* name = dictionary->name; + if(strcmp(name, "tag16h5") == 0) tag16h5_destroy(dictionary); + else if(strcmp(name, "tag25h9") == 0) tag25h9_destroy(dictionary); + else if(strcmp(name, "tag36h10") == 0) tag36h10_destroy(dictionary); + else if(strcmp(name, "tag36h11") == 0) tag36h11_destroy(dictionary); +#ifdef RTABMAP_APRILTAG_WITH_ARUCO + else if(strcmp(name, "tagAruco4x4_50") == 0) tagAruco4x4_50_destroy(dictionary); + else if(strcmp(name, "tagAruco4x4_100") == 0) tagAruco4x4_100_destroy(dictionary); + else if(strcmp(name, "tagAruco4x4_250") == 0) tagAruco4x4_250_destroy(dictionary); + else if(strcmp(name, "tagAruco4x4_1000") == 0) tagAruco4x4_1000_destroy(dictionary); + else if(strcmp(name, "tagAruco5x5_50") == 0) tagAruco5x5_50_destroy(dictionary); + else if(strcmp(name, "tagAruco5x5_100") == 0) tagAruco5x5_100_destroy(dictionary); + else if(strcmp(name, "tagAruco5x5_250") == 0) tagAruco5x5_250_destroy(dictionary); + else if(strcmp(name, "tagAruco5x5_1000") == 0) tagAruco5x5_1000_destroy(dictionary); + else if(strcmp(name, "tagAruco6x6_50") == 0) tagAruco6x6_50_destroy(dictionary); + else if(strcmp(name, "tagAruco6x6_100") == 0) tagAruco6x6_100_destroy(dictionary); + else if(strcmp(name, "tagAruco6x6_250") == 0) tagAruco6x6_250_destroy(dictionary); + else if(strcmp(name, "tagAruco6x6_1000") == 0) tagAruco6x6_1000_destroy(dictionary); + else if(strcmp(name, "tagAruco7x7_50") == 0) tagAruco7x7_50_destroy(dictionary); + else if(strcmp(name, "tagAruco7x7_100") == 0) tagAruco7x7_100_destroy(dictionary); + else if(strcmp(name, "tagAruco7x7_250") == 0) tagAruco7x7_250_destroy(dictionary); + else if(strcmp(name, "tagAruco7x7_1000") == 0) tagAruco7x7_1000_destroy(dictionary); + else if(strcmp(name, "tagArucoMIP_36h12") == 0) tagArucoMIP36h12_destroy(dictionary); +#endif + else + { + UFATAL("AprilTag: Didn't find the right destructor for dictionary \"%s\"", name); + } +} +#endif + +MarkerDetector::MarkerDetector(const ParametersMap & parameters) : + strategy_((Strategy)Parameters::defaultMarkerStrategy()), + markerLength_(Parameters::defaultMarkerLength()), + maxDepthError_(Parameters::defaultMarkerMaxDepthError()), + maxRange_(Parameters::defaultMarkerMaxRange()), + minRange_(Parameters::defaultMarkerMinRange()), + dictionaryId_(Parameters::defaultMarkerDictionary()), + apriltagLibDetector_(NULL), + apriltagLibFamily_(NULL) { #ifdef HAVE_OPENCV_ARUCO - markerLength_ = Parameters::defaultMarkerLength(); - maxDepthError_ = Parameters::defaultMarkerMaxDepthError(); - maxRange_ = Parameters::defaultMarkerMaxRange(); - minRange_ = Parameters::defaultMarkerMinRange(); - dictionaryId_ = Parameters::defaultMarkerDictionary(); #if CV_MAJOR_VERSION > 4 || (CV_MAJOR_VERSION == 4 && CV_MINOR_VERSION >= 7) detectorParams_.reset(new cv::aruco::DetectorParameters()); #elif CV_MAJOR_VERSION > 3 || (CV_MAJOR_VERSION == 3 && CV_MINOR_VERSION >=2) @@ -47,22 +209,107 @@ MarkerDetector::MarkerDetector(const ParametersMap & parameters) detectorParams_.reset(new cv::aruco::DetectorParameters()); #endif #if CV_MAJOR_VERSION > 4 || (CV_MAJOR_VERSION == 4 && CV_MINOR_VERSION >= 7) - detectorParams_->cornerRefinementMethod = (cv::aruco::CornerRefineMethod) Parameters::defaultMarkerCornerRefinementMethod(); + detectorParams_->cornerRefinementMethod = (cv::aruco::CornerRefineMethod) Parameters::defaultMarkerOpenCVCornerRefinementMethod(); #elif CV_MAJOR_VERSION > 3 || (CV_MAJOR_VERSION == 3 && CV_MINOR_VERSION >=3) - detectorParams_->cornerRefinementMethod = Parameters::defaultMarkerCornerRefinementMethod(); + detectorParams_->cornerRefinementMethod = Parameters::defaultMarkerOpenCVCornerRefinementMethod(); #else - detectorParams_->doCornerRefinement = Parameters::defaultMarkerCornerRefinementMethod()!=0; + detectorParams_->doCornerRefinement = Parameters::defaultMarkerOpenCVCornerRefinementMethod()!=0; #endif +#endif + parseParameters(parameters); -#endif } MarkerDetector::~MarkerDetector() { - +#ifdef RTABMAP_APRILTAG + if(apriltagLibDetector_) + { + apriltag_detector_destroy(((apriltag_detector_t*)apriltagLibDetector_)); + } + if(apriltagLibFamily_) + { + destroyAprilTagDictionary((apriltag_family_t*)apriltagLibFamily_); + } +#endif } void MarkerDetector::parseParameters(const ParametersMap & parameters) { + int strategy = strategy_; + Parameters::parse(parameters, Parameters::kMarkerStrategy(), strategy); + strategy_ = (Strategy)strategy; + Parameters::parse(parameters, Parameters::kMarkerLength(), markerLength_); + Parameters::parse(parameters, Parameters::kMarkerMaxDepthError(), maxDepthError_); + Parameters::parse(parameters, Parameters::kMarkerMaxRange(), maxRange_); + Parameters::parse(parameters, Parameters::kMarkerMinRange(), minRange_); + Parameters::parse(parameters, Parameters::kMarkerDictionary(), dictionaryId_); + + if(parameters.find(Parameters::kMarkerLengths()) != parameters.end()) + { + markerLengths_.clear(); + std::string strLengths; + Parameters::parse(parameters, Parameters::kMarkerLengths(), strLengths); + std::list strList = uSplit(strLengths, '|'); + for(std::list::iterator iter=strList.begin(); iter!=strList.end(); ++iter) + { + std::list items = uSplit(*iter, ' '); + if(items.size() != 2) + { + UERROR("Invalid string format \"%s\" for parameter %s, make " + "sure the values are separated by single space and/or '|'. See " + "description of the parameter for example. That parameter " + "will be ignored.", + strLengths.c_str(), + Parameters::kMarkerLengths().c_str()); + markerLengths_.clear(); + break; + } + else + { + float length = uStr2Float(items.back()); + if(uStrContains(items.front(), ":")) + { + std::list range = uSplit(items.front(), ':'); + if(range.size() != 2) + { + UERROR("Invalid string format \"%s\" for parameter %s, make " + "sure the values are separated by single space and/or '|'. See " + "description of the parameter for example. That parameter " + "will be ignored.", + strLengths.c_str(), + Parameters::kMarkerLengths().c_str()); + markerLengths_.clear(); + break; + } + int id1 = uStr2Int(range.front()); + int id2 = uStr2Int(range.back()); + UDEBUG("Adding a range of markers %d -> %d with length %f", id1, id2, length); + for(int id=id1; id<=id2; ++id) + { + auto inserted = markerLengths_.insert(std::make_pair(id, length)); + if(!inserted.second) + { + UWARN("Marker %d is already configured with length %f, not overwriting", inserted.first->first, inserted.first->second); + } + } + } + else { + int id = uStr2Int(items.front()); + auto inserted = markerLengths_.insert(std::make_pair(id, length)); + if(!inserted.second) + { + UWARN("Marker %d is already configured with length %f, overwriting with %f", inserted.first->first, inserted.first->second, length); + inserted.first->second = length; + } + else + { + UDEBUG("Added marker %d with length %f", id, length); + } + } + } + } + } + #ifdef HAVE_OPENCV_ARUCO detectorParams_->adaptiveThreshWinSizeMin = 3; detectorParams_->adaptiveThreshWinSizeMax = 23; @@ -76,13 +323,13 @@ void MarkerDetector::parseParameters(const ParametersMap & parameters) detectorParams_->minMarkerDistanceRate = 0.05; #if CV_MAJOR_VERSION > 4 || (CV_MAJOR_VERSION == 4 && CV_MINOR_VERSION >= 7) int cornerRefinementMethod; - Parameters::parse(parameters, Parameters::kMarkerCornerRefinementMethod(), cornerRefinementMethod); + Parameters::parse(parameters, Parameters::kMarkerOpenCVCornerRefinementMethod(), cornerRefinementMethod); detectorParams_->cornerRefinementMethod = (cv::aruco::CornerRefineMethod)cornerRefinementMethod; #elif CV_MAJOR_VERSION > 3 || (CV_MAJOR_VERSION == 3 && CV_MINOR_VERSION >=3) - Parameters::parse(parameters, Parameters::kMarkerCornerRefinementMethod(), detectorParams_->cornerRefinementMethod); + Parameters::parse(parameters, Parameters::kMarkerOpenCVCornerRefinementMethod(), detectorParams_->cornerRefinementMethod); #else int doCornerRefinement = detectorParams_->doCornerRefinement?1:0; - Parameters::parse(parameters, Parameters::kMarkerCornerRefinementMethod(), doCornerRefinement); + Parameters::parse(parameters, Parameters::kMarkerOpenCVCornerRefinementMethod(), doCornerRefinement); detectorParams_->doCornerRefinement = doCornerRefinement!=0; #endif detectorParams_->cornerRefinementWinSize = 5; @@ -95,13 +342,9 @@ void MarkerDetector::parseParameters(const ParametersMap & parameters) detectorParams_->minOtsuStdDev = 5.0; detectorParams_->errorCorrectionRate = 0.6; - Parameters::parse(parameters, Parameters::kMarkerLength(), markerLength_); - Parameters::parse(parameters, Parameters::kMarkerMaxDepthError(), maxDepthError_); - Parameters::parse(parameters, Parameters::kMarkerMaxRange(), maxRange_); - Parameters::parse(parameters, Parameters::kMarkerMinRange(), minRange_); - Parameters::parse(parameters, Parameters::kMarkerDictionary(), dictionaryId_); + #if CV_MAJOR_VERSION < 3 || (CV_MAJOR_VERSION == 3 && (CV_MINOR_VERSION <4 || (CV_MINOR_VERSION ==4 && CV_SUBMINOR_VERSION<2))) - if(dictionaryId_ >= 17) + if(dictionaryId_ = 17 && strategy_ == 0) { UERROR("Cannot set AprilTag dictionary. OpenCV version should be at least 3.4.2, " "current version is %s. Setting %s to default (%d)", @@ -110,21 +353,108 @@ void MarkerDetector::parseParameters(const ParametersMap & parameters) Parameters::defaultMarkerDictionary()); dictionaryId_ = Parameters::defaultMarkerDictionary(); } +#elif CV_MAJOR_VERSION < 4 || (CV_MAJOR_VERSION == 4 && CV_MINOR_VERSION <8) + if(dictionaryId_ == 21 && strategy_ == 0) + { + UERROR("Cannot set ARUCO_MIP_36h12 dictionary. OpenCV version should be at least 4.8.0, " + "current version is %s. Setting %s to default (%d)", + CV_VERSION, + Parameters::kMarkerDictionary().c_str(), + Parameters::defaultMarkerDictionary()); + dictionaryId_ = Parameters::defaultMarkerDictionary(); + } #endif #if CV_MAJOR_VERSION > 4 || (CV_MAJOR_VERSION == 4 && CV_MINOR_VERSION >= 7) dictionary_.reset(new cv::aruco::Dictionary()); *dictionary_ = cv::aruco::getPredefinedDictionary(cv::aruco::PredefinedDictionaryType(dictionaryId_)); #elif CV_MAJOR_VERSION > 3 || (CV_MAJOR_VERSION == 3 && CV_MINOR_VERSION >=2) - dictionary_ = cv::aruco::getPredefinedDictionary(cv::aruco::PREDEFINED_DICTIONARY_NAME(dictionaryId_)); + dictionary_ = cv::aruco::getPredefinedDictionary(cv::aruco::PredefinedDictionaryType(dictionaryId_)); #else dictionary_.reset(new cv::aruco::Dictionary()); - *dictionary_ = cv::aruco::getPredefinedDictionary(cv::aruco::PREDEFINED_DICTIONARY_NAME(dictionaryId_)); + *dictionary_ = cv::aruco::getPredefinedDictionary(cv::aruco::PredefinedDictionaryType(dictionaryId_)); #endif +#else + if(strategy_ == 0) + { +#ifdef RTABMAP_APRILTAG + UERROR("RTAB-Map is not built with opencv-aruco library! Fallback to AprilTag strategy (%s=1).", Parameters::kMarkerStrategy().c_str()); + strategy_ = kStrategyApriltag; +#else + UERROR("RTAB-Map is not built with opencv-aruco library!"); +#endif + } +#endif + +#ifdef RTABMAP_APRILTAG + if(apriltagLibDetector_ && apriltagLibFamily_) { + apriltag_detector_remove_family(((apriltag_detector_t*)apriltagLibDetector_), (apriltag_family_t*)apriltagLibFamily_); + } + if(apriltagLibFamily_) + { + destroyAprilTagDictionary((apriltag_family_t*)apriltagLibFamily_); + apriltagLibFamily_ = NULL; + } + if(strategy_ == 1) + { + if(apriltagLibDetector_ == NULL) + { + apriltagLibDetector_ = apriltag_detector_create(); + } + Parameters::parse(parameters, Parameters::kMarkerAprilTagNThreads(), ((apriltag_detector_t*)apriltagLibDetector_)->nthreads); + Parameters::parse(parameters, Parameters::kMarkerAprilTagQuadDecimate(), ((apriltag_detector_t*)apriltagLibDetector_)->quad_decimate); + Parameters::parse(parameters, Parameters::kMarkerAprilTagQuadSigma(), ((apriltag_detector_t*)apriltagLibDetector_)->quad_sigma); + Parameters::parse(parameters, Parameters::kMarkerAprilTagRefineEdges(), ((apriltag_detector_t*)apriltagLibDetector_)->refine_edges); + Parameters::parse(parameters, Parameters::kMarkerAprilTagDecodeSharpening(), ((apriltag_detector_t*)apriltagLibDetector_)->decode_sharpening); + Parameters::parse(parameters, Parameters::kMarkerAprilTagDebug(), ((apriltag_detector_t*)apriltagLibDetector_)->debug); + + cv::aruco::PredefinedDictionaryType dictFamily = cv::aruco::PredefinedDictionaryType(dictionaryId_); +#ifndef RTABMAP_APRILTAG_WITH_ARUCO + if((dictFamily >= cv::aruco::DICT_4X4_50 && dictFamily < cv::aruco::DICT_APRILTAG_16h5) || dictFamily==cv::aruco::DICT_ARUCO_MIP_36h12) + { + UERROR("Cannot set aruco dictionaries with current AprilTag library version. " + "Use opencv-aruco implementation instead or rebuild/install latest " + "AprilTag from source. Setting %s to default apriltag dictionary (%d)", + Parameters::kMarkerDictionary().c_str(), + cv::aruco::DICT_APRILTAG_36h11); + dictionaryId_ = cv::aruco::DICT_APRILTAG_36h11; + dictFamily = cv::aruco::PredefinedDictionaryType(dictionaryId_); + } +#endif + if(dictFamily == cv::aruco::DICT_ARUCO_ORIGINAL) + { + UERROR("Cannot set ARUCO_ORIGINAL (%d) dictionary with AprilTag library implementation. " + "Use opencv-aruco implementation instead. Setting %s to default apriltag dictionary (%d)", + dictionaryId_, + Parameters::kMarkerDictionary().c_str(), + cv::aruco::DICT_APRILTAG_36h11); + dictionaryId_ = cv::aruco::DICT_APRILTAG_36h11; + dictFamily = cv::aruco::PredefinedDictionaryType(dictionaryId_); + } + apriltagLibFamily_ = createAprilTagPredefinedDictionary(dictFamily); + UASSERT_MSG(apriltagLibFamily_ != NULL, uFormat("AprilTag library cannot be used with dictionary type %d", (int)dictionaryId_).c_str()); + errno = 0; + apriltag_detector_add_family(((apriltag_detector_t*)apriltagLibDetector_), (apriltag_family_t*)apriltagLibFamily_); + if (errno == ENOMEM) { + UFATAL("Unable to add family to detector due to insufficient memory to allocate the tag-family decoder with the default maximum hamming value of 2. Try choosing an alternative tag family."); + } + } +#else + if(strategy_ == 1) + { +#ifdef HAVE_OPENCV_ARUCO + UERROR("RTAB-Map is not built with apriltag library! Fallback to OpenCV (%s=0).", Parameters::kMarkerStrategy().c_str()); + strategy_ = kStrategyOpencv; +#else + UERROR("RTAB-Map is not built with apriltag library!"); +#endif + } #endif } +// deprecated std::map MarkerDetector::detect(const cv::Mat & image, const CameraModel & model, const cv::Mat & depth, float * markerLengthOut, cv::Mat * imageWithDetections) { + UDEBUG(""); std::map detections; std::map infos = detect(image, model, depth, std::map(), imageWithDetections); @@ -144,221 +474,404 @@ std::map MarkerDetector::detect(const cv::Mat & image, const Cam std::map MarkerDetector::detect(const cv::Mat & image, const std::vector & models, const cv::Mat & depth, - const std::map & markerLengths, + const std::map & extraMarkerLengths, cv::Mat * imageWithDetections) { UASSERT(!models.empty() && !image.empty()); UASSERT(int((image.cols/models.size())*models.size()) == image.cols); UASSERT(int((depth.cols/models.size())*models.size()) == depth.cols); int subRGBWidth = image.cols/models.size(); - int subDepthWidth = depth.cols/models.size(); - std::map allInfo; - for(size_t i=0; i subInfo = detect(subImage, model, subDepth, markerLengths, imageWithDetections?&subImageWithDetections:0); - if(ULogger::level() >= ULogger::kWarning) + rgbToDepthFactorX = float(depth.cols) / float(image.cols); + rgbToDepthFactorY = float(depth.rows) / float(image.rows); + } + else if(markerLength_ == 0) + { + if(depth.empty()) { - for(std::map::iterator iter=subInfo.begin(); iter!=subInfo.end(); ++iter) - { - std::pair::iterator, bool> inserted = allInfo.insert(*iter); - if(!inserted.second) - { - UWARN("Marker %d already added by another camera, ignoring detection from camera %d", iter->first, i); - } - } - } - else - { - allInfo.insert(subInfo.begin(), subInfo.end()); - } - if(imageWithDetections) - { - if(i==0) - { - *imageWithDetections = cv::Mat(image.size(), subImageWithDetections.type()); - } - if(!subImageWithDetections.empty()) - { - subImageWithDetections.copyTo(cv::Mat(*imageWithDetections, cv::Rect(subRGBWidth*i, 0, subRGBWidth, image.rows))); - } + UERROR("Depth image is empty, please set %s parameter to non-null.", Parameters::kMarkerLength().c_str()); + return std::map(); } } - return allInfo; -} - -std::map MarkerDetector::detect(const cv::Mat & image, - const CameraModel & model, - const cv::Mat & depth, - const std::map & markerLengths, - cv::Mat * imageWithDetections) -{ - if(!image.empty() && image.cols != model.imageWidth()) - { - UERROR("This method cannot handle multi-camera marker detection, use the other function version supporting it."); - return std::map(); - } - - std::map detections; - -#ifdef HAVE_OPENCV_ARUCO std::vector< int > ids; - std::vector< std::vector< cv::Point2f > > corners, rejected; - std::vector< cv::Vec3d > rvecs, tvecs; + std::vector< int > cams; + std::vector< std::vector< cv::Point2f > > corners; // in stitched image + std::vector poses; // in optical frame // detect markers and estimate pose -#if CV_MAJOR_VERSION > 3 || (CV_MAJOR_VERSION == 3 && CV_MINOR_VERSION >=2) - cv::aruco::detectMarkers(image, dictionary_, corners, ids, detectorParams_, rejected); -#else - cv::aruco::detectMarkers(image, *dictionary_, corners, ids, *detectorParams_, rejected); -#endif - UDEBUG("Markers detected=%d rejected=%d", (int)ids.size(), (int)rejected.size()); - if(ids.size() > 0) + if(strategy_ == kStrategyApriltag) { - float rgbToDepthFactorX = 1.0f; - float rgbToDepthFactorY = 1.0f; - if(!depth.empty()) +#ifdef RTABMAP_APRILTAG + cv::Mat grayImage = image; + if(image.channels() > 1) { - rgbToDepthFactorX = 1.0f/(model.imageWidth()>0?float(model.imageWidth())/float(depth.cols):1.0f); - rgbToDepthFactorY = 1.0f/(model.imageHeight()>0?float(model.imageHeight())/float(depth.rows):1.0f); + cv::cvtColor(image, grayImage, cv::COLOR_BGR2GRAY); } - else if(markerLength_ == 0) - { - if(depth.empty()) - { - UERROR("Depth image is empty, please set %s parameter to non-null.", Parameters::kMarkerLength().c_str()); - return detections; - } - } + image_u8_t im = {grayImage.cols, grayImage.rows, (int)grayImage.step, grayImage.data}; - cv::aruco::estimatePoseSingleMarkers(corners, markerLength_<=0.0?1.0f:markerLength_, model.K(), model.D(), rvecs, tvecs); - std::vector scales; - for(size_t i=0; i::const_iterator findIter = markerLengths.find(ids[i]); - if(!depth.empty() && (markerLength_ == 0 || (markerLength_<0 && findIter==markerLengths.end()))) + errno = 0; + zarray_t *apriltagDetections = apriltag_detector_detect(((apriltag_detector_t*)apriltagLibDetector_), &im); + if (errno == EAGAIN) { + UERROR("Unable to create the %d threads requested.", ((apriltag_detector_t*)apriltagLibDetector_)->nthreads); + if(apriltagDetections) { - float d = util2d::getDepth(depth, (corners[i][0].x + (corners[i][2].x-corners[i][0].x)/2.0f)*rgbToDepthFactorX, (corners[i][0].y + (corners[i][2].y-corners[i][0].y)/2.0f)*rgbToDepthFactorY, true, 0.02f, true); - float d1 = util2d::getDepth(depth, corners[i][0].x*rgbToDepthFactorX, corners[i][0].y*rgbToDepthFactorY, true, 0.02f, true); - float d2 = util2d::getDepth(depth, corners[i][1].x*rgbToDepthFactorX, corners[i][1].y*rgbToDepthFactorY, true, 0.02f, true); - float d3 = util2d::getDepth(depth, corners[i][2].x*rgbToDepthFactorX, corners[i][2].y*rgbToDepthFactorY, true, 0.02f, true); - float d4 = util2d::getDepth(depth, corners[i][3].x*rgbToDepthFactorX, corners[i][3].y*rgbToDepthFactorY, true, 0.02f, true); - // Accept measurement only if all 4 depth values are valid and - // they are at the same depth (camera should be perpendicular to marker for - // best depth estimation) - if(d>0 && d1>0 && d2>0 && d3>0 && d4>0) + apriltag_detections_destroy(apriltagDetections); + } + return std::map(); + } + + std::set idsAdded; + for (int i = 0; i < zarray_size(apriltagDetections); i++) + { + apriltag_detection_t *det; + zarray_get(apriltagDetections, i, &det); + + // Which camera? + int cameraIndex = int(det->c[0]) / subRGBWidth; + if(cameraIndex<0 || cameraIndex>=(int)models.size()) + { + UWARN("Marker %d detected outside the image! camera index=%d (models=%ld subWidth=%d) marker center=(%f,%f). Ignoring...", + det->id, cameraIndex, models.size(), subRGBWidth, det->c[0], det->c[1]); + continue; + } + + if(!markerLengths_.empty() && + markerLengths_.find(det->id) == markerLengths_.end() && + extraMarkerLengths.find(det->id) == extraMarkerLengths.end()) + { + UDEBUG("Ignoring marker %d because it is not in the list of expected markers (see %s)", + det->id, + Parameters::kMarkerLengths().c_str()); + continue; + } + if(idsAdded.find(det->id)!=idsAdded.end()) + { + UWARN("Marker %d already added by another camera, ignoring detection from camera %d", det->id, cameraIndex); + continue; + } + + const CameraModel & model = models[cameraIndex]; + float offsetX = cameraIndex*subRGBWidth; + + // Convert detection in local camera + det->c[0] -= offsetX; + for(int i=0; i<4; ++i) + { + det->p[i][0] -= offsetX; + } + + // Shift the homography to match the local-camera pixel frame: + // pre-multiply H by T = [[1,0,-offsetX],[0,1,0],[0,0,1]] so it + // stays consistent with the shifted corners. estimate_tag_pose() + // seeds its iterative refinement from H, so leaving it stale + // produces a bad initial guess (often triggers the AprilTag + // "more than one new minimum found" debug print). + MATD_EL(det->H, 0, 0) -= offsetX * MATD_EL(det->H, 2, 0); + MATD_EL(det->H, 0, 1) -= offsetX * MATD_EL(det->H, 2, 1); + MATD_EL(det->H, 0, 2) -= offsetX * MATD_EL(det->H, 2, 2); + + apriltag_detection_info_t info; + info.det = det; + info.tagsize = 1.0f; + info.fx = model.fx(); + info.fy = model.fy(); + info.cx = model.cx(); + info.cy = model.cy(); + + // Then call estimate_tag_pose. + apriltag_pose_t pose; + double err = estimate_tag_pose(&info, &pose); + + if (pose.R && pose.t) + { + if(MATD_EL(pose.t, 2, 0) > 0) { - float scale = d/tvecs[i].val[2]; - - if( fabs(d-d1) < maxDepthError_ && - fabs(d-d2) < maxDepthError_ && - fabs(d-d3) < maxDepthError_ && - fabs(d-d4) < maxDepthError_) + Transform t(MATD_EL(pose.R, 0, 0), MATD_EL(pose.R, 0, 1), MATD_EL(pose.R, 0, 2), MATD_EL(pose.t, 0, 0), + MATD_EL(pose.R, 1, 0), MATD_EL(pose.R, 1, 1), MATD_EL(pose.R, 1, 2), MATD_EL(pose.t, 1, 0), + MATD_EL(pose.R, 2, 0), MATD_EL(pose.R, 2, 1), MATD_EL(pose.R, 2, 2), MATD_EL(pose.t, 2, 0)); + poses.push_back(t*Transform(-1,0,0,0, 0,1,0,0, 0,0,-1,0)); // Flip tag to match same frame than OpenCV (+x->-x, +z->-z) + + corners.push_back(std::vector(4)); + for(int i=0; i<4; ++i) { - length = scale; - scales.push_back(length); - tvecs[i] *= scales.back(); - UWARN("Automatic marker length estimation: id=%d depth=%fm length=%fm", ids[i], d, scales.back()); - } - else - { - UWARN("The four marker's corners should be " - "perpendicular to camera to estimate correctly " - "the marker's length. Errors: %f, %f, %f > %fm (%s). Four corners: %f %f %f %f (middle=%f). " - "Parameter %s can be set to non-null to skip automatic " - "marker length estimation. Detections are ignored.", - fabs(d1-d2), fabs(d1-d3), fabs(d1-d4), maxDepthError_, Parameters::kMarkerMaxDepthError().c_str(), - d1, d2, d3, d4, d, - Parameters::kMarkerLength().c_str()); - continue; + // reconvert in original stitched image + corners.back()[i].x = det->p[i][0]+offsetX; + corners.back()[i].y = det->p[i][1]; } + ids.push_back(det->id); + cams.push_back(cameraIndex); + UDEBUG("Add marker %d (err = %f)", det->id, err); + idsAdded.insert(det->id); } else { - UWARN("Some depth values (%f,%f,%f,%f, middle=%f) cannot be detected on the " - "marker's corners, cannot initialize marker length. " - "Parameter %s can be set to non-null to skip automatic " - "marker length estimation. Detections are ignored.", - d1,d2,d3,d4,d, - Parameters::kMarkerLength().c_str()); - continue; + UWARN("Skipping %d because its estimated pose is behind the camera %d", det->id, cameraIndex); } } - else if(markerLength_ < 0) - { - if(findIter!=markerLengths.end()) - { - length = findIter->second; - tvecs[i] *= length; - } - else - { - UWARN("Cannot find marker length for marker %d, ignoring this marker (count=%d)", ids[i], (int)markerLengths.size()); - continue; - } - } - else if(markerLength_ > 0) - { - length = markerLength_; - } - else - { - continue; - } - - // Limit the detection range to be between the min / max range. - // If the ranges are -1, allow any detection within that direction. - if((maxRange_ <= 0 || tvecs[i].val[2] < maxRange_) && - (minRange_ <= 0 || tvecs[i].val[2] > minRange_)) + else { + UWARN("Failed to compute pose for marker %d, ignoring...", det->id); + } + + // Free pose memory + if (pose.R) matd_destroy(pose.R); + if (pose.t) matd_destroy(pose.t); + } + + apriltag_detections_destroy(apriltagDetections); +#else + UERROR("RTAB-Map is not built with apriltag library."); +#endif + } + else // opencv + { + std::vector< int > cvIds; + std::vector< std::vector< cv::Point2f > > cvCorners, cvRejected; +#ifdef HAVE_OPENCV_ARUCO +#if CV_MAJOR_VERSION > 3 || (CV_MAJOR_VERSION == 3 && CV_MINOR_VERSION >=2) + cv::aruco::detectMarkers(image, dictionary_, cvCorners, cvIds, detectorParams_, cvRejected); +#else + cv::aruco::detectMarkers(image, *dictionary_, cvCorners, cvIds, *detectorParams_, cvRejected); +#endif + UDEBUG("Markers detected=%d rejected=%d", (int)cvIds.size(), (int)cvRejected.size()); + + // split detections per camera + std::set idsAdded; + std::vector< std::vector< std::vector< cv::Point2f > > > cvCornersPerCam(models.size()); + std::vector< std::vector< int > > cvIdsPerCam(models.size()); + for(size_t i=0; i=(int)models.size()) + { + UWARN("Marker %d detected outside the image! camera index=%d (models=%ld subWidth=%d) marker center=(%f,%f). Ignoring...", + id, cameraIndex, models.size(), subRGBWidth, cvCorners[i][0].x, cvCorners[i][0].y); + continue; + } + + if(!markerLengths_.empty() && + markerLengths_.find(id) == markerLengths_.end() && + extraMarkerLengths.find(id) == extraMarkerLengths.end()) + { + UDEBUG("Ignoring marker %d because it is not in the list of expected markers (see %s)", + id, + Parameters::kMarkerLengths().c_str()); + continue; + } + if(idsAdded.find(id) != idsAdded.end()) + { + UWARN("Marker %d already added by another camera, ignoring detection from camera %d", id, cameraIndex); + continue; + } + + float offsetX = cameraIndex*subRGBWidth; + for(size_t j=0; j rvecs, tvecs; + const CameraModel & model = models[cam]; + cv::aruco::estimatePoseSingleMarkers(cvCornersPerCam[cam], 1.0f, model.K(), model.D(), rvecs, tvecs); + float offsetX = cam*subRGBWidth; + for(size_t i=0; i(0,0), R.at(0,1), R.at(0,2), tvecs[i].val[0], - R.at(1,0), R.at(1,1), R.at(1,2), tvecs[i].val[1], - R.at(2,0), R.at(2,1), R.at(2,2), tvecs[i].val[2]); - Transform pose = model.localTransform() * t; - detections.insert(std::make_pair(ids[i], MarkerInfo(ids[i], length, pose))); - UDEBUG("Marker %d detected in base_link: %s, optical_link=%s, local transform=%s", ids[i], pose.prettyPrint().c_str(), t.prettyPrint().c_str(), model.localTransform().prettyPrint().c_str()); - } - } - if(markerLength_ == 0 && !scales.empty()) - { - float sum = 0.0f; - float maxError = 0.0f; - for(size_t i=0; i0) + R.at(1,0), R.at(1,1), R.at(1,2), tvecs[i].val[1], + R.at(2,0), R.at(2,1), R.at(2,2), tvecs[i].val[2]); + poses.push_back(t); + ids.push_back(cvIdsPerCam[cam][i]); + cams.push_back(cam); + // reconvert in original stitched image + for(size_t j=0; j 0.001f) - { - UWARN("The marker's length detected between 2 of the " - "markers doesn't match (%fm vs %fm)." - "Parameter %s can be set to non-null to skip automatic " - "marker length estimation. Detections are ignored.", - scales[i], scales[0], - Parameters::kMarkerLength().c_str()); - detections.clear(); - return detections; - } - if(error > maxError) - { - maxError = error; - } + cvCornersPerCam[cam][i][j].x += offsetX; } - sum += scales[i]; + corners.push_back(cvCornersPerCam[cam][i]); + } + } +#else + UERROR("RTAB-Map is not built with \"aruco\" module from OpenCV."); +#endif + } + + // Estimate scale and fill up output detections + std::vector scales; + std::map detections; + for(size_t i=0; i::const_iterator findIter = extraMarkerLengths.find(ids[i]); + if(markerLengths_.empty() && !depth.empty() && (markerLength_ == 0 || (markerLength_<0 && findIter==extraMarkerLengths.end()))) + { + float d = util2d::getDepth(depth, (corners[i][0].x + (corners[i][2].x-corners[i][0].x)/2.0f)*rgbToDepthFactorX, (corners[i][0].y + (corners[i][2].y-corners[i][0].y)/2.0f)*rgbToDepthFactorY, true, 0.02f, true); + float d1 = util2d::getDepth(depth, corners[i][0].x*rgbToDepthFactorX, corners[i][0].y*rgbToDepthFactorY, true, 0.02f, true); + float d2 = util2d::getDepth(depth, corners[i][1].x*rgbToDepthFactorX, corners[i][1].y*rgbToDepthFactorY, true, 0.02f, true); + float d3 = util2d::getDepth(depth, corners[i][2].x*rgbToDepthFactorX, corners[i][2].y*rgbToDepthFactorY, true, 0.02f, true); + float d4 = util2d::getDepth(depth, corners[i][3].x*rgbToDepthFactorX, corners[i][3].y*rgbToDepthFactorY, true, 0.02f, true); + // Accept measurement only if all 4 depth values are valid and + // they are at the same depth (camera should be perpendicular to marker for + // best depth estimation) + if(d>0 && d1>0 && d2>0 && d3>0 && d4>0) + { + float scale = d / poses[i].z(); + + if( fabs(d-d1) < maxDepthError_ && + fabs(d-d2) < maxDepthError_ && + fabs(d-d3) < maxDepthError_ && + fabs(d-d4) < maxDepthError_) + { + length = scale; + scales.push_back(length); + UWARN("Automatic marker length estimation: id=%d depth=%fm length=%fm", ids[i], d, length); + } + else + { + UWARN("The four marker's corners should be " + "perpendicular to camera to estimate correctly " + "the marker's length. Errors: %f, %f, %f > %fm (%s). Four corners: %f %f %f %f (middle=%f). " + "Parameter %s can be set to non-null to skip automatic " + "marker length estimation. Detections are ignored.", + fabs(d1-d2), fabs(d1-d3), fabs(d1-d4), maxDepthError_, Parameters::kMarkerMaxDepthError().c_str(), + d1, d2, d3, d4, d, + Parameters::kMarkerLength().c_str()); + continue; + } + } + else + { + UWARN("Some depth values (%f,%f,%f,%f, middle=%f) cannot be detected on the " + "marker's corners, cannot initialize marker length. " + "Parameter %s can be set to non-null to skip automatic " + "marker length estimation. Detections are ignored.", + d1,d2,d3,d4,d, + Parameters::kMarkerLength().c_str()); + continue; } - markerLength_ = sum/float(scales.size()); - UWARN("Final marker length estimated = %fm, max error=%fm (used for subsequent detections)", markerLength_, maxError); } + else if(!markerLengths_.empty() || findIter!=extraMarkerLengths.end()) + { + std::map::const_iterator paramIter = markerLengths_.find(ids[i]); + if(paramIter != markerLengths_.end()) + { + if(findIter!=extraMarkerLengths.end() && findIter->second != paramIter->second) + { + UWARN("Marker's length of %d is defined both in extra lengths " + "(%f m) and the parameter %s (%f m), we will use the length " + "from extra lengths.", + findIter->second, + Parameters::kMarkerLengths().c_str(), + paramIter->second); + length = findIter->second; + } + else + { + length = paramIter->second; + } + } + else if(findIter!=extraMarkerLengths.end()) + { + length = findIter->second; + } + else + { + UERROR("Not supposed to reach this case, ignoring detection %d", ids[i]); + continue; + } + } + else if(markerLength_ < 0) + { + UWARN("Cannot find marker length for marker %d, ignoring this marker (count=%d)", ids[i], (int)extraMarkerLengths.size()); + continue; + } + else if(markerLength_ > 0) + { + length = markerLength_; + } + else + { + continue; + } + + UASSERT(length > 0.0f); + poses[i].x() *= length; + poses[i].y() *= length; + poses[i].z() *= length; + + // Limit the detection range to be between the min / max range. + // If the ranges are -1, allow any detection within that direction. + if((maxRange_ <= 0 || poses[i].z() < maxRange_) && + (minRange_ <= 0 || poses[i].z() > minRange_)) + { + Transform pose = models[cams[i]].localTransform() * poses[i]; + detections.insert(std::make_pair(ids[i], MarkerInfo(ids[i], length, pose))); + UDEBUG("Marker %d detected in base_link: %s, optical_link=%s, local transform=%s", + ids[i], + pose.prettyPrint().c_str(), + poses[i].prettyPrint().c_str(), + models[cams[i]].localTransform().prettyPrint().c_str()); + } + else + { + UDEBUG("Filtered marker %d by distance (min=%f max=%f value=%f) from the camera %d", + ids[i], minRange_, maxRange_, poses[i].z(), cams[i]); + } + } + if(markerLength_ == 0 && !scales.empty()) + { + float sum = 0.0f; + float maxError = 0.0f; + for(size_t i=0; i0) + { + float error = fabs(scales[i]-scales[0]); + if(error > 0.001f) + { + UWARN("The marker's length detected between 2 of the " + "markers doesn't match (%fm vs %fm)." + "Parameter %s can be set to non-null to skip automatic " + "marker length estimation or set to a negative value to " + "estimate length for each marker. The current %ld detections " + "are ignored!", + scales[i], scales[0], + Parameters::kMarkerLength().c_str(), + detections.size()); + detections.clear(); + return detections; + } + if(error > maxError) + { + maxError = error; + } + } + sum += scales[i]; + } + markerLength_ = sum/float(scales.size()); + UWARN("Final marker length estimated = %fm, max error=%fm (used for subsequent detections)", markerLength_, maxError); } if(imageWithDetections) @@ -371,31 +884,61 @@ std::map MarkerDetector::detect(const cv::Mat & image, { image.copyTo(*imageWithDetections); } + if(!ids.empty()) { +#ifdef HAVE_OPENCV_ARUCO cv::aruco::drawDetectedMarkers(*imageWithDetections, corners, ids); - +#else + UWARN("RTAB-Map is not built with \"aruco\" module from OpenCV. Cannot draw markers on image."); +#endif for(unsigned int i = 0; i < ids.size(); i++) { std::map::iterator iter = detections.find(ids[i]); if(iter!=detections.end()) { + int cam = cams[i]; + UASSERT(cam >=0 && cam < (int)models.size()); + const CameraModel & model = models[cam]; + int roix = subRGBWidth*cam; + UASSERT(roix >=0 && roix <= imageWithDetections->cols - subRGBWidth); + cv::Mat subImage(*imageWithDetections, cv::Rect(roix, 0, subRGBWidth, imageWithDetections->rows)); + cv::Vec3d rvec; + cv::Vec3d tvec(poses[i].x(), poses[i].y(), poses[i].z()); + cv::Mat R; + poses[i].rotationMatrix().convertTo(R, CV_64F); + cv::Rodrigues(R, rvec); #if CV_MAJOR_VERSION > 4 || (CV_MAJOR_VERSION == 4 && (CV_MINOR_VERSION >1 || (CV_MINOR_VERSION==1 && CV_PATCH_VERSION>=1))) - cv::drawFrameAxes(*imageWithDetections, model.K(), model.D(), rvecs[i], tvecs[i], iter->second.length() * 0.5f); -#else - cv::aruco::drawAxis(*imageWithDetections, model.K(), model.D(), rvecs[i], tvecs[i], iter->second.length() * 0.5f); + cv::drawFrameAxes(subImage, model.K(), model.D(), rvec, tvec, iter->second.length() * 0.5f); +#elif defined(HAVE_OPENCV_ARUCO) + cv::aruco::drawAxis(subImage, model.K(), model.D(), rvec, tvec, iter->second.length() * 0.5f); #endif } } } } -#else - UERROR("RTAB-Map is not built with \"aruco\" module from OpenCV."); -#endif - return detections; } +std::map MarkerDetector::detect(const cv::Mat & image, + const CameraModel & model, + const cv::Mat & depth, + const std::map & markerLengths, + cv::Mat * imageWithDetections) +{ + UDEBUG(""); + if(!image.empty() && image.cols != model.imageWidth()) + { + UERROR("This method cannot handle multi-camera marker detection, use the other function version supporting it."); + return std::map(); + } + + std::vector models; + models.push_back(model); + + return detect(image, models, depth, markerLengths, imageWithDetections); +} + } /* namespace rtabmap */ diff --git a/corelib/src/Parameters.cpp b/corelib/src/Parameters.cpp index 844f4534..83e1135b 100644 --- a/corelib/src/Parameters.cpp +++ b/corelib/src/Parameters.cpp @@ -238,6 +238,9 @@ const std::map > & Parameters::getRemo { // removed parameters + // 0.23.7 + removedParameters_.insert(std::make_pair("Marker/CornerRefinementMethod", std::make_pair(true, Parameters::kMarkerOpenCVCornerRefinementMethod()))); + // 0.23.1 removedParameters_.insert(std::make_pair("OdomVINS/ConfigPath", std::make_pair(true, Parameters::kOdomVINSFusionConfigPath()))); @@ -290,7 +293,7 @@ const std::map > & Parameters::getRemo removedParameters_.insert(std::make_pair("Aruco/MaxDepthError", std::make_pair(true, Parameters::kMarkerMaxDepthError()))); removedParameters_.insert(std::make_pair("Aruco/VarianceLinear", std::make_pair(true, Parameters::kMarkerVarianceLinear()))); removedParameters_.insert(std::make_pair("Aruco/VarianceAngular", std::make_pair(true, Parameters::kMarkerVarianceAngular()))); - removedParameters_.insert(std::make_pair("Aruco/CornerRefinementMethod", std::make_pair(true, Parameters::kMarkerCornerRefinementMethod()))); + removedParameters_.insert(std::make_pair("Aruco/CornerRefinementMethod", std::make_pair(true, Parameters::kMarkerOpenCVCornerRefinementMethod()))); // 0.17.5 removedParameters_.insert(std::make_pair("Grid/OctoMapOccupancyThr", std::make_pair(true, Parameters::kGridGlobalOccupancyThr()))); @@ -681,6 +684,12 @@ ParametersMap Parameters::parseArguments(int argc, char * argv[], bool onlyParam std::cout << str << std::setw(spacing - str.size()) << "true" << std::endl; #else std::cout << str << std::setw(spacing - str.size()) << "false" << std::endl; +#endif + str = "With AprilTag:"; +#ifdef RTABMAP_APRILTAG + std::cout << str << std::setw(spacing - str.size()) << "true" << std::endl; +#else + std::cout << str << std::setw(spacing - str.size()) << "false" << std::endl; #endif str = "With OpenGV:"; #ifdef RTABMAP_OPENGV diff --git a/corelib/src/Rtabmap.cpp b/corelib/src/Rtabmap.cpp index af11082d..7a4c6f33 100644 --- a/corelib/src/Rtabmap.cpp +++ b/corelib/src/Rtabmap.cpp @@ -4504,8 +4504,7 @@ bool Rtabmap::process( (smallDisplacement || tooFastMovement) && _loopClosureHypothesis.first == 0 && lastProximitySpaceClosureId == 0 && - !delayedLocalization && - (rejectedLoopClosure || landmarksDetected.empty())) + !delayedLocalization) { _odomCachePoses.erase(signatureRemoved); for(std::multimap::iterator iter=_odomCacheConstraints.begin(); iter!=_odomCacheConstraints.end();) diff --git a/corelib/src/optimizer/OptimizerG2O.cpp b/corelib/src/optimizer/OptimizerG2O.cpp index 4534e346..0eafcc24 100644 --- a/corelib/src/optimizer/OptimizerG2O.cpp +++ b/corelib/src/optimizer/OptimizerG2O.cpp @@ -2135,6 +2135,390 @@ std::map OptimizerG2O::optimizeBA( return optimizedPoses; } +bool OptimizerG2O::loadGraph( + const std::string & fileName, + std::map & poses, + std::multimap & edgeConstraints) +{ + FILE * file = 0; +#ifdef _MSC_VER + fopen_s(&file, fileName.c_str(), "r"); +#else + file = fopen(fileName.c_str(), "r"); +#endif + + if(!file) + { + UERROR("Cannot open file %s", fileName.c_str()); + return false; + } + + // saveGraph() writes landmarks (originally negative ids, remapped to + // landmarkOffset - id) first in DESCENDING file-id order, then regular + // poses in ASCENDING file-id order. We recover landmarkOffset from this + // order to restore the original negative landmark ids. + struct VertexEntry { + int fileId; + Transform transform; + bool definitelyLandmark; // VERTEX_XY / VERTEX_TRACKXYZ + }; + struct EdgeEntry { + int from; + int to; + Link::Type type; + Transform transform; + cv::Mat info; + bool isPrior; // from==to, prior on a single vertex + bool hasLandmarkEndpoint; // tag implies a landmark on one side + }; + std::vector verticesList; + std::vector edgesList; + + char line[2048]; + while(fgets(line, 2048, file) != NULL) + { + std::list tokenList = uSplit(uReplaceChar(uReplaceChar(line, '\n', ' '), '\r', ' '), ' '); + std::vector v; + v.reserve(tokenList.size()); + for(std::list::const_iterator iter = tokenList.begin(); iter != tokenList.end(); ++iter) + { + if(!iter->empty()) + { + v.push_back(*iter); + } + } + if(v.empty()) + { + continue; + } + const std::string & tag = v[0]; + + // Skip parameters, switch helpers and unrelated entries + if(tag == "PARAMS_SE2OFFSET" || tag == "PARAMS_SE3OFFSET" || + tag == "VERTEX_SWITCH" || tag == "EDGE_SWITCH_PRIOR") + { + continue; + } + + if(tag == "VERTEX_SE2" && v.size() == 5) + { + VertexEntry e; + e.fileId = atoi(v[1].c_str()); + e.transform = Transform(uStr2Float(v[2]), uStr2Float(v[3]), uStr2Float(v[4])); + e.definitelyLandmark = false; + verticesList.push_back(e); + } + else if(tag == "VERTEX_XY" && v.size() == 4) + { + VertexEntry e; + e.fileId = atoi(v[1].c_str()); + e.transform = Transform(uStr2Float(v[2]), uStr2Float(v[3]), 0); + e.definitelyLandmark = true; + verticesList.push_back(e); + } + else if(tag == "VERTEX_SE3:QUAT" && v.size() == 9) + { + VertexEntry e; + e.fileId = atoi(v[1].c_str()); + e.transform = Transform(uStr2Float(v[2]), uStr2Float(v[3]), uStr2Float(v[4]), + uStr2Float(v[5]), uStr2Float(v[6]), uStr2Float(v[7]), uStr2Float(v[8])); + e.definitelyLandmark = false; + verticesList.push_back(e); + } + else if(tag == "VERTEX_TRACKXYZ" && v.size() == 5) + { + VertexEntry e; + e.fileId = atoi(v[1].c_str()); + e.transform = Transform(uStr2Float(v[2]), uStr2Float(v[3]), uStr2Float(v[4]), 0, 0, 0); + e.definitelyLandmark = true; + verticesList.push_back(e); + } + else if(tag == "EDGE_SE2" && v.size() == 12) + { + EdgeEntry e; + e.from = atoi(v[1].c_str()); + e.to = atoi(v[2].c_str()); + e.transform = Transform(uStr2Float(v[3]), uStr2Float(v[4]), uStr2Float(v[5])); + e.info = cv::Mat::eye(6, 6, CV_64FC1); + e.info.at(0, 0) = uStr2Double(v[6]); + e.info.at(0, 1) = e.info.at(1, 0) = uStr2Double(v[7]); + e.info.at(0, 5) = e.info.at(5, 0) = uStr2Double(v[8]); + e.info.at(1, 1) = uStr2Double(v[9]); + e.info.at(1, 5) = e.info.at(5, 1) = uStr2Double(v[10]); + e.info.at(5, 5) = uStr2Double(v[11]); + e.type = Link::kUndef; // disambiguated after we know landmarkOffset + e.isPrior = false; + e.hasLandmarkEndpoint = false; + edgesList.push_back(e); + } + else if(tag == "EDGE_SE2_XY" && v.size() == 8) + { + EdgeEntry e; + e.from = atoi(v[1].c_str()); + e.to = atoi(v[2].c_str()); + e.transform = Transform(uStr2Float(v[3]), uStr2Float(v[4]), 0); + e.info = cv::Mat::eye(6, 6, CV_64FC1); + e.info.at(0, 0) = uStr2Double(v[5]); + e.info.at(0, 1) = e.info.at(1, 0) = uStr2Double(v[6]); + e.info.at(1, 1) = uStr2Double(v[7]); + e.type = Link::kLandmark; + e.isPrior = false; + e.hasLandmarkEndpoint = true; + edgesList.push_back(e); + } + else if((tag == "EDGE_SE3:QUAT" || tag == "EDGE_SE3") && v.size() == 31) + { + EdgeEntry e; + e.from = atoi(v[1].c_str()); + e.to = atoi(v[2].c_str()); + e.transform = Transform(uStr2Float(v[3]), uStr2Float(v[4]), uStr2Float(v[5]), + uStr2Float(v[6]), uStr2Float(v[7]), uStr2Float(v[8]), uStr2Float(v[9])); + e.info = cv::Mat::eye(6, 6, CV_64FC1); + int idx = 10; + for(int r = 0; r < 6; ++r) + { + for(int c = r; c < 6; ++c) + { + e.info.at(r, c) = uStr2Double(v[idx++]); + if(r != c) e.info.at(c, r) = e.info.at(r, c); + } + } + // EDGE_SE3 (no :QUAT) is the landmark variant emitted by saveGraph + bool landmarkTag = (tag == "EDGE_SE3"); + e.type = landmarkTag ? Link::kLandmark : Link::kUndef; + e.isPrior = false; + e.hasLandmarkEndpoint = landmarkTag; + edgesList.push_back(e); + } + else if(tag == "EDGE_SE3_TRACKXYZ" && v.size() == 13) + { + EdgeEntry e; + e.from = atoi(v[1].c_str()); + e.to = atoi(v[2].c_str()); + // v[3] = param_offset id, ignored + e.transform = Transform(uStr2Float(v[4]), uStr2Float(v[5]), uStr2Float(v[6]), 0, 0, 0); + e.info = cv::Mat::eye(6, 6, CV_64FC1); + e.info.at(0, 0) = uStr2Double(v[7]); + e.info.at(0, 1) = e.info.at(1, 0) = uStr2Double(v[8]); + e.info.at(0, 2) = e.info.at(2, 0) = uStr2Double(v[9]); + e.info.at(1, 1) = uStr2Double(v[10]); + e.info.at(1, 2) = e.info.at(2, 1) = uStr2Double(v[11]); + e.info.at(2, 2) = uStr2Double(v[12]); + e.type = Link::kLandmark; + e.isPrior = false; + e.hasLandmarkEndpoint = true; + edgesList.push_back(e); + } + else if(tag == "EDGE_PRIOR_SE2" && v.size() == 11) + { + EdgeEntry e; + e.from = atoi(v[1].c_str()); + e.to = e.from; + e.transform = Transform(uStr2Float(v[2]), uStr2Float(v[3]), uStr2Float(v[4])); + e.info = cv::Mat::eye(6, 6, CV_64FC1); + e.info.at(0, 0) = uStr2Double(v[5]); + e.info.at(0, 1) = e.info.at(1, 0) = uStr2Double(v[6]); + e.info.at(0, 5) = e.info.at(5, 0) = uStr2Double(v[7]); + e.info.at(1, 1) = uStr2Double(v[8]); + e.info.at(1, 5) = e.info.at(5, 1) = uStr2Double(v[9]); + e.info.at(5, 5) = uStr2Double(v[10]); + e.type = Link::kPosePrior; + e.isPrior = true; + e.hasLandmarkEndpoint = false; + edgesList.push_back(e); + } + else if(tag == "EDGE_PRIOR_SE2_XY" && v.size() == 7) + { + EdgeEntry e; + e.from = atoi(v[1].c_str()); + e.to = e.from; + e.transform = Transform(uStr2Float(v[2]), uStr2Float(v[3]), 0); + e.info = cv::Mat::eye(6, 6, CV_64FC1); + e.info.at(0, 0) = uStr2Double(v[4]); + e.info.at(0, 1) = e.info.at(1, 0) = uStr2Double(v[5]); + e.info.at(1, 1) = uStr2Double(v[6]); + // no orientation info on this prior + e.info.at(3, 3) = e.info.at(4, 4) = e.info.at(5, 5) = 1.0 / 9999.0; + e.type = Link::kPosePrior; + e.isPrior = true; + e.hasLandmarkEndpoint = false; + edgesList.push_back(e); + } + else if(tag == "EDGE_SE3_PRIOR" && v.size() == 31) + { + EdgeEntry e; + e.from = atoi(v[1].c_str()); + e.to = e.from; + // v[2] = param_offset id, ignored + e.transform = Transform(uStr2Float(v[3]), uStr2Float(v[4]), uStr2Float(v[5]), + uStr2Float(v[6]), uStr2Float(v[7]), uStr2Float(v[8]), uStr2Float(v[9])); + e.info = cv::Mat::eye(6, 6, CV_64FC1); + int idx = 10; + for(int r = 0; r < 6; ++r) + { + for(int c = r; c < 6; ++c) + { + e.info.at(r, c) = uStr2Double(v[idx++]); + if(r != c) e.info.at(c, r) = e.info.at(r, c); + } + } + e.type = Link::kPosePrior; + e.isPrior = true; + e.hasLandmarkEndpoint = false; + edgesList.push_back(e); + } + else if(tag == "EDGE_POINTXYZ_PRIOR" && v.size() == 11) + { + EdgeEntry e; + e.from = atoi(v[1].c_str()); + e.to = e.from; + e.transform = Transform(uStr2Float(v[2]), uStr2Float(v[3]), uStr2Float(v[4]), 0, 0, 0); + e.info = cv::Mat::eye(6, 6, CV_64FC1); + e.info.at(0, 0) = uStr2Double(v[5]); + e.info.at(0, 1) = e.info.at(1, 0) = uStr2Double(v[6]); + e.info.at(0, 2) = e.info.at(2, 0) = uStr2Double(v[7]); + e.info.at(1, 1) = uStr2Double(v[8]); + e.info.at(1, 2) = e.info.at(2, 1) = uStr2Double(v[9]); + e.info.at(2, 2) = uStr2Double(v[10]); + // no orientation info on this prior + e.info.at(3, 3) = e.info.at(4, 4) = e.info.at(5, 5) = 1.0 / 9999.0; + e.type = Link::kPosePrior; + e.isPrior = true; + e.hasLandmarkEndpoint = false; + edgesList.push_back(e); + } + else if(tag == "EDGE_SE2_SWITCHABLE" && v.size() == 13) + { + EdgeEntry e; + e.from = atoi(v[1].c_str()); + e.to = atoi(v[2].c_str()); + // v[3] = switch vertex id, ignored + e.transform = Transform(uStr2Float(v[4]), uStr2Float(v[5]), uStr2Float(v[6])); + e.info = cv::Mat::eye(6, 6, CV_64FC1); + e.info.at(0, 0) = uStr2Double(v[7]); + e.info.at(0, 1) = e.info.at(1, 0) = uStr2Double(v[8]); + e.info.at(0, 5) = e.info.at(5, 0) = uStr2Double(v[9]); + e.info.at(1, 1) = uStr2Double(v[10]); + e.info.at(1, 5) = e.info.at(5, 1) = uStr2Double(v[11]); + e.info.at(5, 5) = uStr2Double(v[12]); + e.type = Link::kUndef; + e.isPrior = false; + e.hasLandmarkEndpoint = false; + edgesList.push_back(e); + } + else if(tag == "EDGE_SE3_SWITCHABLE" && v.size() == 32) + { + EdgeEntry e; + e.from = atoi(v[1].c_str()); + e.to = atoi(v[2].c_str()); + // v[3] = switch vertex id, ignored + e.transform = Transform(uStr2Float(v[4]), uStr2Float(v[5]), uStr2Float(v[6]), + uStr2Float(v[7]), uStr2Float(v[8]), uStr2Float(v[9]), uStr2Float(v[10])); + e.info = cv::Mat::eye(6, 6, CV_64FC1); + int idx = 11; + for(int r = 0; r < 6; ++r) + { + for(int c = r; c < 6; ++c) + { + e.info.at(r, c) = uStr2Double(v[idx++]); + if(r != c) e.info.at(c, r) = e.info.at(r, c); + } + } + e.type = Link::kUndef; + e.isPrior = false; + e.hasLandmarkEndpoint = false; + edgesList.push_back(e); + } + else + { + UWARN("Unsupported or malformed g2o line: \"%s\" (tag=%s, tokens=%d)", line, tag.c_str(), (int)v.size()); + } + } + fclose(file); + + // Recover landmarkOffset from vertex order: + // file order = [landmarks with DESCENDING file_ids] + [regular poses with ASCENDING file_ids] + // Walk backwards from the end and take the longest ascending suffix as the regular poses. + // landmarkOffset = max regular pose id (= last fileId of that suffix). + int landmarkOffset = 0; + int firstRegularIdx = (int)verticesList.size(); + if(!verticesList.empty()) + { + firstRegularIdx = (int)verticesList.size() - 1; + while(firstRegularIdx > 0 && + verticesList[firstRegularIdx - 1].fileId < verticesList[firstRegularIdx].fileId) + { + --firstRegularIdx; + } + landmarkOffset = verticesList.back().fileId; + + // If the alleged regular suffix actually starts on a definite landmark + // (VERTEX_XY / VERTEX_TRACKXYZ), then there are no regular poses and + // saveGraph used landmarkOffset = 0; restore that case. + if(verticesList[firstRegularIdx].definitelyLandmark) + { + landmarkOffset = 0; + firstRegularIdx = (int)verticesList.size(); + } + } + + // Insert vertices into poses, remapping landmark file ids back to negative. + for(int i = 0; i < (int)verticesList.size(); ++i) + { + int originalId; + if(i < firstRegularIdx) + { + originalId = landmarkOffset - verticesList[i].fileId; // negative + } + else + { + originalId = verticesList[i].fileId; + } + if(poses.find(originalId) == poses.end()) + { + poses.insert(std::make_pair(originalId, verticesList[i].transform)); + } + else + { + UWARN("Vertex %d (file id %d) already exists, ignoring duplicate", originalId, verticesList[i].fileId); + } + } + + // Remap edge endpoints. Any file id > landmarkOffset (or, if landmarkOffset == 0 + // and there are any landmarks at all, any id present in the landmark prefix) + // is a landmark and gets the negative id back. + bool allLandmarks = (landmarkOffset == 0 && firstRegularIdx == (int)verticesList.size() && !verticesList.empty()); + auto remap = [&](int fileId) -> int { + if(landmarkOffset > 0 && fileId > landmarkOffset) + { + return landmarkOffset - fileId; // negative + } + if(allLandmarks) + { + return -fileId; + } + return fileId; + }; + + for(const EdgeEntry & e : edgesList) + { + int from = remap(e.from); + int to = e.isPrior ? from : remap(e.to); + Link::Type type = e.type; + // Promote ambiguous edges (EDGE_SE2) to kLandmark when an endpoint + // turns out to be a landmark after remapping. + if(type == Link::kUndef && (from < 0 || to < 0)) + { + type = Link::kLandmark; + } + edgeConstraints.insert(std::make_pair(from, Link(from, to, type, e.transform, e.info))); + } + + UINFO("Graph loaded from %s (%d poses, %d edges, landmarkOffset=%d)", + fileName.c_str(), (int)poses.size(), (int)edgeConstraints.size(), landmarkOffset); + return true; +} + bool OptimizerG2O::saveGraph( const std::string & fileName, const std::map & poses, diff --git a/corelib/src/optimizer/OptimizerGTSAM.cpp b/corelib/src/optimizer/OptimizerGTSAM.cpp index 56ac51ba..6361f537 100644 --- a/corelib/src/optimizer/OptimizerGTSAM.cpp +++ b/corelib/src/optimizer/OptimizerGTSAM.cpp @@ -239,6 +239,7 @@ std::map OptimizerGTSAM::optimize( isam2_ = new gtsam::ISAM2(params); addedPoses_.clear(); lastAddedConstraints_.clear(); + isLandmarkWithRotation_.clear(); lastRootFactorIndex_.first = 0; lastSwitchId_ = 1000000000; } @@ -308,7 +309,13 @@ std::map OptimizerGTSAM::optimize( UDEBUG("fill poses to gtsam... rootId=%d (priorsIgnored=%d landmarksIgnored=%d)", rootId, priorsIgnored()?1:0, landmarksIgnored()?1:0); gtsam::Values initialEstimate; - std::map isLandmarkWithRotation; + // In batch (non-iSAM2) mode each optimize() call is independent. + // In iSAM2 mode the map persists so we can resolve landmarks added + // in a previous incremental call but referenced by a new edge. + if(!isam2_) + { + isLandmarkWithRotation_.clear(); + } for(std::map::const_iterator iter = newPoses.begin(); iter!=newPoses.end(); ++iter) { UASSERT(!iter->second.isNull()); @@ -328,12 +335,12 @@ std::map OptimizerGTSAM::optimize( if (1 / static_cast(jter->second.infMatrix().at(5,5)) >= 9999.0) { initialEstimate.insert(iter->first, gtsam::Point2(iter->second.x(), iter->second.y())); - isLandmarkWithRotation.insert(std::make_pair(iter->first, false)); + isLandmarkWithRotation_.insert(std::make_pair(iter->first, false)); } else { initialEstimate.insert(iter->first, gtsam::Pose2(iter->second.x(), iter->second.y(), iter->second.theta())); - isLandmarkWithRotation.insert(std::make_pair(iter->first, true)); + isLandmarkWithRotation_.insert(std::make_pair(iter->first, true)); } addedPoses_.insert(iter->first); } @@ -357,12 +364,12 @@ std::map OptimizerGTSAM::optimize( 1 / static_cast(jter->second.infMatrix().at(5,5)) >= 9999.0) { initialEstimate.insert(iter->first, gtsam::Point3(iter->second.x(), iter->second.y(), iter->second.z())); - isLandmarkWithRotation.insert(std::make_pair(iter->first, false)); + isLandmarkWithRotation_.insert(std::make_pair(iter->first, false)); } else { initialEstimate.insert(iter->first, gtsam::Pose3(iter->second.toEigen4d())); - isLandmarkWithRotation.insert(std::make_pair(iter->first, true)); + isLandmarkWithRotation_.insert(std::make_pair(iter->first, true)); } addedPoses_.insert(iter->first); } @@ -390,7 +397,7 @@ std::map OptimizerGTSAM::optimize( { if(isSlam2d()) { - if(id1 < 0 && !isLandmarkWithRotation.at(id1)) + if(id1 < 0 && !isLandmarkWithRotation_.at(id1)) { gtsam::noiseModel::Diagonal::shared_ptr model = gtsam::noiseModel::Diagonal::Variances(gtsam::Vector2( 1/iter->second.infMatrix().at(0,0), @@ -429,7 +436,7 @@ std::map OptimizerGTSAM::optimize( } else { - if(id1 < 0 && !isLandmarkWithRotation.at(id1)) + if(id1 < 0 && !isLandmarkWithRotation_.at(id1)) { gtsam::noiseModel::Diagonal::shared_ptr model = gtsam::noiseModel::Diagonal::Precisions(gtsam::Vector3( iter->second.infMatrix().at(0,0), @@ -501,9 +508,9 @@ std::map OptimizerGTSAM::optimize( t = iter->second.transform().inverse(); std::swap(id1, id2); // should be node -> landmark } - + UASSERT(isLandmarkWithRotation_.find(id2) != isLandmarkWithRotation_.end()); #ifdef RTABMAP_VERTIGO - if(this->isRobust() && isLandmarkWithRotation.at(id2)) + if(this->isRobust() && isLandmarkWithRotation_.at(id2)) { // create new switch variable // Sunderhauf IROS 2012: @@ -521,7 +528,7 @@ std::map OptimizerGTSAM::optimize( gtsam::noiseModel::Diagonal::shared_ptr switchPriorModel = gtsam::noiseModel::Diagonal::Sigmas(gtsam::Vector1(1.0)); graph.add(gtsam::PriorFactor (gtsam::Symbol('s',lastSwitchId_), vertigo::SwitchVariableLinear(prior), switchPriorModel)); } - else if(this->isRobust() && !isLandmarkWithRotation.at(id2)) + else if(this->isRobust() && !isLandmarkWithRotation_.at(id2)) { UWARN("%s cannot be used for landmark constraints without orientation.", Parameters::kOptimizerRobust().c_str()); } @@ -529,7 +536,7 @@ std::map OptimizerGTSAM::optimize( if(isSlam2d()) { - if(isLandmarkWithRotation.at(id2)) + if(isLandmarkWithRotation_.at(id2)) { Eigen::Matrix information = Eigen::Matrix::Identity(); if(!isCovarianceIgnored()) @@ -592,7 +599,7 @@ std::map OptimizerGTSAM::optimize( } else { - if(isLandmarkWithRotation.at(id2)) + if(isLandmarkWithRotation_.at(id2)) { Eigen::Matrix information = Eigen::Matrix::Identity(); if(!isCovarianceIgnored()) @@ -807,9 +814,9 @@ std::map OptimizerGTSAM::optimize( gtsam::Pose2 p = iter->value.cast(); tmpPoses.insert(std::make_pair(key, Transform(p.x(), p.y(), p.theta()))); } - else if(!landmarksIgnored() && isLandmarkWithRotation.find(key)!=isLandmarkWithRotation.end()) + else if(!landmarksIgnored() && isLandmarkWithRotation_.find(key)!=isLandmarkWithRotation_.end()) { - if(isLandmarkWithRotation.at(key)) + if(isLandmarkWithRotation_.at(key)) { newPoses.at(key).getTranslationAndEulerAngles(x,y,z,roll,pitch,yaw); gtsam::Pose2 p = iter->value.cast(); @@ -830,9 +837,9 @@ std::map OptimizerGTSAM::optimize( gtsam::Pose3 p = iter->value.cast(); tmpPoses.insert(std::make_pair(key, Transform::fromEigen4d(p.matrix()))); } - else if(!landmarksIgnored() && isLandmarkWithRotation.find(key)!=isLandmarkWithRotation.end()) + else if(!landmarksIgnored() && isLandmarkWithRotation_.find(key)!=isLandmarkWithRotation_.end()) { - if(isLandmarkWithRotation.at(key)) + if(isLandmarkWithRotation_.at(key)) { gtsam::Pose3 p = iter->value.cast(); tmpPoses.insert(std::make_pair(key, Transform::fromEigen4d(p.matrix()))); @@ -1000,9 +1007,9 @@ std::map OptimizerGTSAM::optimize( gtsam::Pose2 p = iter->value.cast(); optimizedPoses.insert(std::make_pair(key, Transform(p.x(), p.y(), z, roll, pitch, p.theta()))); } - else if(!landmarksIgnored() && isLandmarkWithRotation.find(key)!=isLandmarkWithRotation.end()) + else if(!landmarksIgnored() && isLandmarkWithRotation_.find(key)!=isLandmarkWithRotation_.end()) { - if(isLandmarkWithRotation.at(key)) + if(isLandmarkWithRotation_.at(key)) { poses.at(key).getTranslationAndEulerAngles(x,y,z,roll,pitch,yaw); gtsam::Pose2 p = iter->value.cast(); @@ -1023,9 +1030,9 @@ std::map OptimizerGTSAM::optimize( gtsam::Pose3 p = iter->value.cast(); optimizedPoses.insert(std::make_pair(key, Transform::fromEigen4d(p.matrix()))); } - else if(!landmarksIgnored() && isLandmarkWithRotation.find(key)!=isLandmarkWithRotation.end()) + else if(!landmarksIgnored() && isLandmarkWithRotation_.find(key)!=isLandmarkWithRotation_.end()) { - if(isLandmarkWithRotation.at(key)) + if(isLandmarkWithRotation_.at(key)) { gtsam::Pose3 p = iter->value.cast(); optimizedPoses.insert(std::make_pair(key, Transform::fromEigen4d(p.matrix()))); diff --git a/examples/WifiMapping/WifiThread.h b/examples/WifiMapping/WifiThread.h index 7e3d14c6..27da516f 100644 --- a/examples/WifiMapping/WifiThread.h +++ b/examples/WifiMapping/WifiThread.h @@ -177,7 +177,8 @@ private: struct iwreq req; struct iw_statistics stats; - strncpy(req.ifr_name, interfaceName_.c_str(), IFNAMSIZ); + strncpy(req.ifr_name, interfaceName_.c_str(), IFNAMSIZ - 1); + req.ifr_name[IFNAMSIZ - 1] = '\0'; //make room for the iw_statistics object req.u.data.pointer = (caddr_t) &stats; diff --git a/guilib/include/rtabmap/gui/CameraViewer.h b/guilib/include/rtabmap/gui/CameraViewer.h index 906d1735..9e6a0c5b 100644 --- a/guilib/include/rtabmap/gui/CameraViewer.h +++ b/guilib/include/rtabmap/gui/CameraViewer.h @@ -77,6 +77,7 @@ private: QElapsedTimer fpsTimer_; double lastCapturePeriod_; double previousCaptureStamp_; + std::map _landmarksSize; }; } /* namespace rtabmap */ diff --git a/guilib/include/rtabmap/gui/PreferencesDialog.h b/guilib/include/rtabmap/gui/PreferencesDialog.h index c6be4694..65a105a2 100644 --- a/guilib/include/rtabmap/gui/PreferencesDialog.h +++ b/guilib/include/rtabmap/gui/PreferencesDialog.h @@ -364,6 +364,7 @@ private Q_SLOTS: void updateStereoDisparityVisibility(); void updateFeatureMatchingVisibility(); void updateGlobalDescriptorVisibility(); + void updateAvailableMarkerDictionaries(); void updateOdometryStackedIndex(int index); void useOdomFeatures(); void changeWorkingDirectory(); diff --git a/guilib/src/AboutDialog.cpp b/guilib/src/AboutDialog.cpp index 40a605b5..5d130797 100644 --- a/guilib/src/AboutDialog.cpp +++ b/guilib/src/AboutDialog.cpp @@ -107,6 +107,13 @@ AboutDialog::AboutDialog(QWidget * parent) : _ui->label_fastcv->setText("No"); _ui->label_fastcv_license->setEnabled(false); #endif +#ifdef RTABMAP_APRILTAG + _ui->label_apriltag->setText("Yes"); + _ui->label_apriltag_license->setEnabled(true); +#else + _ui->label_apriltag->setText("No"); + _ui->label_apriltag_license->setEnabled(false); +#endif #ifdef RTABMAP_PDAL _ui->label_pdal->setText("Yes"); _ui->label_pdal_license->setEnabled(true); diff --git a/guilib/src/CameraViewer.cpp b/guilib/src/CameraViewer.cpp index 601cbc6f..6588d51b 100644 --- a/guilib/src/CameraViewer.cpp +++ b/guilib/src/CameraViewer.cpp @@ -36,6 +36,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include #include #include #include @@ -84,7 +85,7 @@ CameraViewer::CameraViewer(QWidget * parent, const ParametersMap & parameters) : showScanCheckbox_->setChecked(true); markerCheckbox_ = new QCheckBox("Detect markers", this); -#ifdef HAVE_OPENCV_ARUCO +#if defined(HAVE_OPENCV_ARUCO) || defined(RTABMAP_APRILTAG) markerCheckbox_->setEnabled(true); markerDetector_ = new MarkerDetector(parameters); #else @@ -151,6 +152,7 @@ void CameraViewer::showImage(const rtabmap::SensorData & data) imageView_->setVisible(!left.empty() || !left.empty()); std::map detections; + UTimer markerDetectionTime; if(!left.empty()) { std::vector models; @@ -165,12 +167,22 @@ void CameraViewer::showImage(const rtabmap::SensorData & data) } } } + else + { + _landmarksSize.clear(); + } if(!models.empty() && models[0].isValidForProjection()) { cv::Mat imageWithDetections; - detections = markerDetector_->detect(left, models, depthOrRight, std::map(), &imageWithDetections); + detections = markerDetector_->detect(left, models, depthOrRight, _landmarksSize, &imageWithDetections); imageView_->setImage(uCvMat2QImage(imageWithDetections)); + for(std::map::iterator iter=detections.begin(); iter!=detections.end(); ++iter) + { + if(iter->second.length() > 0.0f) { + _landmarksSize.insert(std::make_pair(iter->first, iter->second.length())); + } + } } else { @@ -184,6 +196,10 @@ void CameraViewer::showImage(const rtabmap::SensorData & data) sizes.append(QString(" Depth=%1x%2").arg(depthOrRight.cols).arg(depthOrRight.rows)); } sizes.append(QString(" FPS capture=%1 render=%2").arg(lastCapturePeriod_>0.0?(int)round(1.0/lastCapturePeriod_):0).arg((int)round(1.0/fpsTimer_.restart()*1000))); + if(markerCheckbox_->isEnabled() && markerCheckbox_->isChecked()) + { + sizes.append(QString(" Marker=%1ms").arg(int(markerDetectionTime.ticks()*1000))); + } imageSizeLabel_->setText(sizes); if(!depthOrRight.empty() && @@ -236,7 +252,7 @@ void CameraViewer::showImage(const rtabmap::SensorData & data) #if PCL_VERSION_COMPARE(>=, 1, 7, 2) cloudView_->addOrUpdateCoordinate(uFormat("landmark_%d", iter->first), iter->second.pose(), iter->second.length(), false); #endif - std::string num = uNumber2Str(iter->first); + std::string num = uFormat("%d (%.1f cm)", iter->first, iter->second.length()*100.0f); cloudView_->addOrUpdateText( std::string("landmark_str_") + num, num, diff --git a/guilib/src/MainWindow.cpp b/guilib/src/MainWindow.cpp index 92a053f2..8daf26b2 100644 --- a/guilib/src/MainWindow.cpp +++ b/guilib/src/MainWindow.cpp @@ -5239,6 +5239,7 @@ void MainWindow::drawKeypoints(const std::multimap & refWords void MainWindow::drawLandmarks(cv::Mat & image, const Signature & signature) { + UDEBUG("%ld landmarks", signature.getLandmarks().size()); for(std::map::const_iterator iter=signature.getLandmarks().begin(); iter!=signature.getLandmarks().end(); ++iter) { // Project in all cameras in which the landmark is visible @@ -5295,6 +5296,9 @@ void MainWindow::drawLandmarks(cv::Mat & image, const Signature & signature) { imagePoints[j].x += i*model.imageWidth(); } + // Make sure the frame origin is visible + valid = imagePoints[0].x >= i*model.imageWidth() && imagePoints[0].x < (i+1)*model.imageWidth() && + imagePoints[0].y >= 0 && imagePoints[0].y < image.rows; } } if(valid) diff --git a/guilib/src/PreferencesDialog.cpp b/guilib/src/PreferencesDialog.cpp index 93c55d7c..fd0e3cc0 100644 --- a/guilib/src/PreferencesDialog.cpp +++ b/guilib/src/PreferencesDialog.cpp @@ -475,15 +475,14 @@ PreferencesDialog::PreferencesDialog(QWidget * parent) : _ui->checkBox_showOdomFrustums->setChecked(false); #endif - //if OpenCV < 3.4.2 -#if CV_MAJOR_VERSION < 3 || (CV_MAJOR_VERSION == 3 && (CV_MINOR_VERSION <4 || (CV_MINOR_VERSION ==4 && CV_SUBMINOR_VERSION<2))) - _ui->ArucoDictionary->setItemData(17, 0, Qt::UserRole - 1); - _ui->ArucoDictionary->setItemData(18, 0, Qt::UserRole - 1); - _ui->ArucoDictionary->setItemData(19, 0, Qt::UserRole - 1); - _ui->ArucoDictionary->setItemData(20, 0, Qt::UserRole - 1); +#if !defined(HAVE_OPENCV_ARUCO) && !defined(RTABMAP_APRILTAG) + _ui->label_markerDetection->setText(_ui->label_markerDetection->text()+" This option works only if OpenCV has been built with \"aruco\" module and/or RTAB-Map has been built with AprilTag library support."); #endif #ifndef HAVE_OPENCV_ARUCO - _ui->label_markerDetection->setText(_ui->label_markerDetection->text()+" This option works only if OpenCV has been built with \"aruco\" module."); + _ui->MarkerStrategy->setItemData(0, 0, Qt::UserRole - 1); +#endif +#ifndef RTABMAP_APRILTAG + _ui->MarkerStrategy->setItemData(1, 0, Qt::UserRole - 1); #endif #ifndef RTABMAP_MADGWICK @@ -1751,18 +1750,30 @@ PreferencesDialog::PreferencesDialog(QWidget * parent) : _ui->stereosgbm_mode->setObjectName(Parameters::kStereoSGBMMode().c_str()); // Aruco marker - _ui->ArucoDictionary->setObjectName(Parameters::kMarkerDictionary().c_str()); - _ui->ArucoMarkerLength->setObjectName(Parameters::kMarkerLength().c_str()); - _ui->ArucoMaxDepthError->setObjectName(Parameters::kMarkerMaxDepthError().c_str()); - _ui->ArucoVarianceLinear->setObjectName(Parameters::kMarkerVarianceLinear().c_str()); - _ui->ArucoVarianceAngular->setObjectName(Parameters::kMarkerVarianceAngular().c_str()); - _ui->ArucoVarianceOrientationIgnored->setObjectName(Parameters::kMarkerVarianceOrientationIgnored().c_str()); - _ui->ArucoMarkerRangeMin->setObjectName(Parameters::kMarkerMinRange().c_str()); - _ui->ArucoMarkerRangeMax->setObjectName(Parameters::kMarkerMaxRange().c_str()); - _ui->ArucoMarkerPriors->setObjectName(Parameters::kMarkerPriors().c_str()); - _ui->ArucoPriorsVarianceLinear->setObjectName(Parameters::kMarkerPriorsVarianceLinear().c_str()); - _ui->ArucoPriorsVarianceAngular->setObjectName(Parameters::kMarkerPriorsVarianceAngular().c_str()); - _ui->ArucoCornerRefinementMethod->setObjectName(Parameters::kMarkerCornerRefinementMethod().c_str()); + _ui->MarkerStrategy->setObjectName(Parameters::kMarkerStrategy().c_str()); + connect(_ui->MarkerStrategy, SIGNAL(currentIndexChanged(int)), _ui->stackedWidget_markerStrategy, SLOT(setCurrentIndex(int))); + connect(_ui->MarkerStrategy, SIGNAL(currentIndexChanged(int)), this, SLOT(updateAvailableMarkerDictionaries())); + _ui->MarkerStrategy->setCurrentIndex(Parameters::defaultMarkerStrategy()); + updateAvailableMarkerDictionaries(); + _ui->MarkerDictionary->setObjectName(Parameters::kMarkerDictionary().c_str()); + _ui->MarkerLength->setObjectName(Parameters::kMarkerLength().c_str()); + _ui->MarkerLengths->setObjectName(Parameters::kMarkerLengths().c_str()); + _ui->MarkerMaxDepthError->setObjectName(Parameters::kMarkerMaxDepthError().c_str()); + _ui->MarkerVarianceLinear->setObjectName(Parameters::kMarkerVarianceLinear().c_str()); + _ui->MarkerVarianceAngular->setObjectName(Parameters::kMarkerVarianceAngular().c_str()); + _ui->MarkerVarianceOrientationIgnored->setObjectName(Parameters::kMarkerVarianceOrientationIgnored().c_str()); + _ui->MarkerRangeMin->setObjectName(Parameters::kMarkerMinRange().c_str()); + _ui->MarkerRangeMax->setObjectName(Parameters::kMarkerMaxRange().c_str()); + _ui->MarkerPriors->setObjectName(Parameters::kMarkerPriors().c_str()); + _ui->MarkerPriorsVarianceLinear->setObjectName(Parameters::kMarkerPriorsVarianceLinear().c_str()); + _ui->MarkerPriorsVarianceAngular->setObjectName(Parameters::kMarkerPriorsVarianceAngular().c_str()); + _ui->OpenCVCornerRefinementMethod->setObjectName(Parameters::kMarkerOpenCVCornerRefinementMethod().c_str()); + _ui->apriltag_nthreads->setObjectName(Parameters::kMarkerAprilTagNThreads().c_str()); + _ui->apriltag_quad_decimate->setObjectName(Parameters::kMarkerAprilTagQuadDecimate().c_str()); + _ui->apriltag_quad_sigma->setObjectName(Parameters::kMarkerAprilTagQuadSigma().c_str()); + _ui->apriltag_refine_edges->setObjectName(Parameters::kMarkerAprilTagRefineEdges().c_str()); + _ui->apriltag_decode_sharpening->setObjectName(Parameters::kMarkerAprilTagDecodeSharpening().c_str()); + _ui->apriltag_debug->setObjectName(Parameters::kMarkerAprilTagDebug().c_str()); // IMU filter _ui->doubleSpinBox_imuFilterMadgwickGain->setObjectName(Parameters::kImuFilterMadgwickGain().c_str()); @@ -3938,15 +3949,36 @@ bool PreferencesDialog::validateForm() _ui->checkbox_odomDisabled->setChecked(false); } -#if CV_MAJOR_VERSION < 3 || (CV_MAJOR_VERSION == 3 && (CV_MINOR_VERSION <4 || (CV_MINOR_VERSION ==4 && CV_SUBMINOR_VERSION<2))) - if(_ui->ArucoDictionary->currentIndex()>=17) - { - QMessageBox::warning(this, tr("Parameter warning"), - tr("ArUco dictionary: cannot select AprilTag dictionary, OpenCV version should be at least 3.4.2. Setting back to 0.")); - _ui->ArucoDictionary->setCurrentIndex(0); - } -#endif + if(_ui->MarkerStrategy->currentIndex() == 0) + { +#if CV_MAJOR_VERSION < 3 || (CV_MAJOR_VERSION == 3 && (CV_MINOR_VERSION <4 || (CV_MINOR_VERSION ==4 && CV_SUBMINOR_VERSION<2))) + if(_ui->MarkerDictionary->currentIndex()>=17) + { + QMessageBox::warning(this, tr("Parameter warning"), + tr("opencv-aruco: cannot use the selected dictionary (%1), OpenCV version should be at least 3.4.2. Setting back to 0.").arg(_ui->MarkerDictionary->currentIndex())); + _ui->MarkerDictionary->setCurrentIndex(0); + } +#elif CV_MAJOR_VERSION < 4 || (CV_MAJOR_VERSION == 4 && CV_MINOR_VERSION <8) + if(_ui->MarkerDictionary->currentIndex()>=21) + { + QMessageBox::warning(this, tr("Parameter warning"), + tr("Opencv Strategy: cannot use selected dictionary (%1), OpenCV version should be at least 4.8.0. Setting back to 0.").arg(_ui->MarkerDictionary->currentIndex())); + _ui->MarkerDictionary->setCurrentIndex(0); + } +#endif + } + else if(_ui->MarkerStrategy->currentIndex() == 1) + { +#ifndef RTABMAP_APRILTAG_WITH_ARUCO + if(_ui->MarkerDictionary->currentIndex() < 17 || _ui->MarkerDictionary->currentIndex() == 21) + { + QMessageBox::warning(this, tr("Parameter warning"), + tr("AprilTag Strategy: cannot use selected dictionary (%1), AprilTag should be built with aruco support. Setting back to 17.").arg(_ui->MarkerDictionary->currentIndex())); + _ui->MarkerDictionary->setCurrentIndex(17); + } +#endif + } return true; } @@ -5573,6 +5605,64 @@ void PreferencesDialog::updateGlobalDescriptorVisibility() _ui->groupBox_pydescriptor->setVisible(_ui->comboBox_globalDescriptorExtractor->currentIndex() == 1); } +void PreferencesDialog::updateAvailableMarkerDictionaries() +{ + Qt::ItemFlags enableFlags = Qt::ItemIsEnabled | Qt::ItemIsSelectable; + for(int i=0;i<_ui->MarkerDictionary->count();++i) { + _ui->MarkerDictionary->setItemData(i, QVariant(static_cast(enableFlags)), Qt::UserRole - 1); + } + + if(_ui->MarkerStrategy->currentIndex() == 1) // AprilTag Strategy is selected + { + // ARUCO_ORIGINAL not available with AprilTag lib + _ui->MarkerDictionary->setItemData(16, 0, Qt::UserRole - 1); +#ifndef RTABMAP_APRILTAG_WITH_ARUCO + // disable all aruco dictionaries + for(int i=0;i<17;++i) { + _ui->MarkerDictionary->setItemData(i, 0, Qt::UserRole - 1); + } + _ui->MarkerDictionary->setItemData(21, 0, Qt::UserRole - 1); + if(_ui->MarkerDictionary->currentIndex() < 17 || _ui->MarkerDictionary->currentIndex() > 20) + { + _ui->MarkerDictionary->setCurrentIndex(20); // 36h11 by default + } +#endif + } + else //if(_ui->MarkerStrategy->currentIndex() == 0) // OpenCV Strategy is selected + { + //if OpenCV < 3.4.2 +#if CV_MAJOR_VERSION < 3 || (CV_MAJOR_VERSION == 3 && (CV_MINOR_VERSION <4 || (CV_MINOR_VERSION ==4 && CV_SUBMINOR_VERSION<2))) + // disable all apriltag dictionaries + for(int i=17;i<21;++i) { + _ui->MarkerDictionary->setItemData(i, 0, Qt::UserRole - 1); + } + if(_ui->MarkerDictionary->currentIndex() >=17 && _ui->MarkerDictionary->currentIndex() <= 20) + { + _ui->MarkerDictionary->setCurrentIndex(Parameters::defaultMarkerDictionary()); + } +#else + if(_ui->MarkerDictionary->currentIndex() >=17 && _ui->MarkerDictionary->currentIndex() <= 20) + { + // If apriltag is selected, select apriltag refinement by default + _ui->OpenCVCornerRefinementMethod->setCurrentIndex(3); + } + else if(_ui->OpenCVCornerRefinementMethod->currentIndex() == 3) + { + // If not apriltag dictionary selected, reset refinement to default. + _ui->OpenCVCornerRefinementMethod->setCurrentIndex(Parameters::defaultMarkerOpenCVCornerRefinementMethod()); + } +#endif +#if CV_MAJOR_VERSION < 4 || (CV_MAJOR_VERSION == 4 && CV_MINOR_VERSION <8) + // disable aruco MPI dictionary + _ui->MarkerDictionary->setItemData(21, 0, Qt::UserRole - 1); + if(_ui->MarkerDictionary->currentIndex() == 21) + { + _ui->MarkerDictionary->setCurrentIndex(Parameters::defaultMarkerDictionary()); + } +#endif + } +} + void PreferencesDialog::updateOdometryStackedIndex(int index) { if(index == 11) // FLOAM -> LOAM @@ -6237,7 +6327,7 @@ bool PreferencesDialog::isMarkerDetection() const } double PreferencesDialog::getMarkerLength() const { - return _ui->ArucoMarkerLength->value(); + return _ui->MarkerLength->value(); } bool PreferencesDialog::isCloudMeshing() const { diff --git a/guilib/src/ui/aboutDialog.ui b/guilib/src/ui/aboutDialog.ui index ca0e087d..532c9764 100644 --- a/guilib/src/ui/aboutDialog.ui +++ b/guilib/src/ui/aboutDialog.ui @@ -164,7 +164,7 @@ p, li { white-space: pre-wrap; } 0 0 596 - 1257 + 1280 @@ -185,516 +185,6 @@ p, li { white-space: pre-wrap; } - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - BSD - - - true - - - - - - - BSD - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - GPLv2 - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - MIT - - - true - - - - - - - With cvsba : - - - true - - - - - - - MIT - - - true - - - - - - - BSD - - - true - - - - - - - BSD - - - true - - - - - - - With Viso2 : - - - true - - - - - - - With libLAS : - - - true - - - - - - - With K4A : - - - true - - - - - - - Creative Commons [Attribution-NonCommercial-ShareAlike] - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With CCCoreLib : - - - true - - - - - - - With RealSense2 : - - - true - - - - - - - With stereo dc1394 : - - - true - - - - - - - With OpenChisel : - - - true - - - - - - - MIT - - - true - - - - - - - GPLv3 - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With g2o : - - - true - - - - - - - BSD - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - BSD - - - true - - - - - - - With FastCV : - - - true - - - - - - - BSD - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - MPL2 - - - true - - - - - - - MIT - - - true - - - - - - - With stereo FlyCapture2 : - - - true - - - - - - - GPLv3 - - - true - - - - - - - MIT - - - true - - - - - - - With VINS-Fusion : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - BSD - - - true - - - - - - - With Zed Open Capture : - - - true - - - - - - - With ORB SLAM 2 : - - - true - - - - - - - BSD - - - true - - - - - - - BSD - - - true - - - - - - - With MSCKF : - - - true - - - - - - - With CPU-TSDF : - - - true - - - - - - - With Zed SDK : - - - true - - - - - - - With CudaSift : - - - true - - - @@ -705,524 +195,17 @@ p, li { white-space: pre-wrap; } - - + + - With FOVIS : + With CPU-TSDF : true - - - - PSF - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - BSD - - - true - - - - - - - Apache v2 and/or GPLv2 - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - BSD - - - true - - - - - - - With OKVIS : - - - true - - - - - - - BSD - - - true - - - - - - - With libpointmatcher : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With Octomap : - - - true - - - - - - - GPLv2 - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With Ceres : - - - true - - - - - - - LGPL - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With MYNTEYE S : - - - true - - - - - - - GPLv2 - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - BSD - - - true - - - - - - - With OpenNI2 : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With XVisio SDK : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - Apache-2 - - - true - - - - - - - GPLv3 - - - true - - - - - - - NVIDIA ISAAC ROS SOFTWARE LICENSE - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - Apache-2 - - - true - - - - - - - With Freenect2 : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With PDAL : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - MIT - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With Python3 : - - - true - - - - - - - With ORB Octree : - - - true - - - - - - - With DVO : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - + @@ -1235,452 +218,20 @@ p, li { white-space: pre-wrap; } - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With GridMap : - - - true - - - - - - - With cuVSLAM : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - Penn Software License - - - true - - - - - - - Open Source or Commercial - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With OpenVINS : - - - true - - - - - - - GPLv3 - - - true - - - - - - - GPLv3 - - - true - - - - - - - With Open3D : - - - true - - - - - - - - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With TORO : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With GTSAM : - - - true - - - - - - - Apache v2 - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - Apache v2 - - - true - - - - - - - With OpenNI : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With Orbbec SDK : - - - true - - - - + - With Freenect : + With CudaSift : true - - + + - With RealSense : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - With DepthAI : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - <html><head/><body><p><span style=" font-weight:600;">License</span></p></body></html> - - - true - - - - - - - With AliceVision : - - - true - - - - - - - With MRPT : - - - true - - - - - - - MIT - - - - - - - With SuperPoint Rpautrat : - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - PCL version : - - - true - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + With Zed SDK : true @@ -1700,8 +251,18 @@ p, li { white-space: pre-wrap; } - - + + + + With PDAL : + + + true + + + + + BSD @@ -1710,17 +271,47 @@ p, li { white-space: pre-wrap; } - - + + - GPLv3 + With Zed Open Capture : true - + + + + BSD + + + true + + + + + + + With ORB SLAM 2 : + + + true + + + + + + + With MSCKF : + + + true + + + + GPLv3 @@ -1730,8 +321,28 @@ p, li { white-space: pre-wrap; } - - + + + + BSD + + + true + + + + + + + With K4W2 : + + + true + + + + + @@ -1743,6 +354,108 @@ p, li { white-space: pre-wrap; } + + + + With OpenChisel : + + + true + + + + + + + MIT + + + true + + + + + + + With RealSense : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With Freenect2 : + + + true + + + + + + + <html><head/><body><p><span style=" font-weight:600;">License</span></p></body></html> + + + true + + + @@ -1756,20 +469,290 @@ p, li { white-space: pre-wrap; } - - + + - With loam_velodyne : + GPLv3 true - - + + - Qt version : + BSD + + + true + + + + + + + With libpointmatcher : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + BSD + + + true + + + + + + + With MYNTEYE S : + + + true + + + + + + + BSD + + + true + + + + + + + Creative Commons [Attribution-NonCommercial-ShareAlike] + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + BSD + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + GPLv3 + + + true + + + + + + + GPLv3 + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + BSD + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With RealSense2 : + + + true + + + + + + + With g2o : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + MIT + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + Penn Software License + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With Orbbec SDK : true @@ -1789,37 +772,252 @@ p, li { white-space: pre-wrap; } + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With OpenGV : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With XVisio SDK : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With loam_velodyne : + + + true + + + + + + + With TORO : + + + true + + + + + + + With OpenNI : + + + true + + + - + - Apache v2 and/or GPLv2 + Apache v2 true - - + + - With SuperPoint Torch : + With cuVSLAM : true - - + + - VTK version : + With Python3 : true - + + + + With Viso2 : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With FOVIS : + + + true + + + + + + + PCL version : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With cvsba : + + + true + + + + + + + With SuperPoint Rpautrat : + + + + + + + MIT + + + + + + + With OpenNI2 : + + + true + + + + + + + BSD + + + true + + + + @@ -1832,10 +1030,402 @@ p, li { white-space: pre-wrap; } - - + + - With K4W2 : + MPL2 + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + BSD + + + true + + + + + + + With AliceVision : + + + true + + + + + + + BSD + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + MIT + + + true + + + + + + + BSD + + + true + + + + + + + MIT + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With FastCV : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With Open3D : + + + true + + + + + + + Apache-2 + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + BSD + + + true + + + + + + + With VINS-Fusion : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + GPLv2 + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + BSD + + + true + + + + + + + With OKVIS : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With stereo dc1394 : + + + true + + + + + + + GPLv3 + + + true + + + + + + + Apache v2 and/or GPLv2 + + + true + + + + + + + With OpenVINS : + + + true + + + + + + + Apache v2 and/or GPLv2 + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + Qt version : + + + true + + + + + + + GPLv2 true @@ -1852,18 +1442,61 @@ p, li { white-space: pre-wrap; } - - + + - With OpenGV : + NVIDIA ISAAC ROS SOFTWARE LICENSE true - - + + + + Apache-2 + + + true + + + + + + + GPLv3 + + + true + + + + + + + MIT + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + BSD @@ -1872,7 +1505,271 @@ p, li { white-space: pre-wrap; } - + + + + BSD + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + Open Source or Commercial + + + true + + + + + + + With Ceres : + + + true + + + + + + + LGPL + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + GPLv2 + + + true + + + + + + + With K4A : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With DepthAI : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With Freenect : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + MIT + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With MRPT : + + + true + + + + + + + With SuperPoint Torch : + + + true + + + + + + + PSF + + + true + + + + + + + With CCCoreLib : + + + true + + + + + + + + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + With Octomap : + + + true + + + + @@ -1885,6 +1782,142 @@ p, li { white-space: pre-wrap; } + + + + With DVO : + + + true + + + + + + + Apache v2 + + + true + + + + + + + With GridMap : + + + true + + + + + + + With libLAS : + + + true + + + + + + + VTK version : + + + true + + + + + + + With GTSAM : + + + true + + + + + + + With ORB Octree : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + GPLv3 + + + true + + + + + + + With stereo FlyCapture2 : + + + true + + + + + + + BSD + + + true + + + + + + + With AprilTag : + + + true + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + diff --git a/guilib/src/ui/preferencesDialog.ui b/guilib/src/ui/preferencesDialog.ui index 4d11c20e..d03332e4 100644 --- a/guilib/src/ui/preferencesDialog.ui +++ b/guilib/src/ui/preferencesDialog.ui @@ -63,7 +63,7 @@ 0 - -700 + 0 684 5218 @@ -95,7 +95,7 @@ QFrame::Raised - 8 + 18 @@ -15505,7 +15505,7 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - + @@ -15514,30 +15514,29 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - - - - m - - - 2 - - - 0.000000000000000 - - - 999.000000000000000 - - - 1.000000000000000 - - - 0.000000000000000 + + + + + opencv-aruco + + + + + apriltag + + + + + + + + - - + + @@ -15547,9 +15546,6 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag 0.000001000000000 - - 9999.000000000000000 - 0.001000000000000 @@ -15558,8 +15554,8 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - - + + m @@ -15577,23 +15573,10 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - + - World prior locations of the markers. The map will be transformed in marker's world frame when a tag is detected. Format is the marker's ID followed by its position (angles in rad), markers are separated by a vertical line ("id1 x y z roll pitch yaw|id2 x y z roll pitch yaw"). Example: "1 0 0 1 0 0 0|2 1 0 1 0 0 1.57" (marker 2 is 1 meter forward than marker 1 with 90 deg yaw rotation). - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - Minimum detection range (0=disabled). + World prior locations of the markers. The map will be transformed in marker's world frame when a tag is detected. Format is the marker's ID followed by its position (angles in rad), multiple markers are separated by a vertical line ("id1 x y z roll pitch yaw|id2 x y z roll pitch yaw"). Example: "1 0 0 1 0 0 0|2 1 0 1 0 0 1.57" (marker 2 is 1 meter forward than marker 1 with 90 deg yaw rotation). true @@ -15604,100 +15587,7 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - - - - - - 6 - - - 0.000001000000000 - - - 9999.000000000000000 - - - 0.001000000000000 - - - 0.001000000000000 - - - - - - - Linear variance to set on marker priors. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - Angular variance to set on marker priors. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - - - - - - - - - - - 6 - - - 0.000001000000000 - - - 0.001000000000000 - - - 0.001000000000000 - - - - - - - m - - - 4 - - - 0.000100000000000 - - - 0.010000000000000 - - - 0.100000000000000 - - - - - + m @@ -15718,33 +15608,7 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - - - - Maximum depth error between all corners of a marker when estimating the marker length (when marker length above is 0). The smaller it is, the more perpendicular the camera should be toward the marker to initialize the length. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - Linear variance to set on marker detections. If variance is adjusted to ignore orientation (see below) and Optimizer/Strategy=2 (GTSAM): it is the variance of the range factor, with 9999 to disable range factor and to do only bearing. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - + Angular variance to set on marker detections. If variance is adjusted to ignore orientation (see below), the angular variance is ignored with Optimizer/Strategy=1 (g2o) and it corresponds to bearing variance with Optimizer/Strategy=2 (GTSAM). @@ -15757,29 +15621,14 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - - - - + + + + If 0, the length is estimated only on the first marker detected, then re-used for all next detections (i.e., this assumes that markers have all the same length). +With <0, the length is estimated once for each unique marker, then re-used for next detections with the same marker ID. - - 6 - - - 0.000001000000000 - - - 0.001000000000000 - - - 0.001000000000000 - - - - - - Maximum detection range (0=unlimited). + Marker length. The length (m) of the markers' side. Value &lt;=0 means automatic marker length estimation using the depth image (the camera should look at the marker perpendicularly for initialization). Mouse over this label to see the difference between negative and 0. true @@ -15789,17 +15638,159 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - - - - + + + + Linear variance to set on marker priors. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - + + + + + + + 6 + + + 0.000001000000000 + + + 9999.000000000000000 + + + 0.001000000000000 + + + 0.001000000000000 + + + + + + + + ARUCO_4X4_50 + + + + + ARUCO_4X4_100 + + + + + ARUCO_4X4_250 + + + + + ARUCO_4X4_1000 + + + + + ARUCO_5X5_50 + + + + + ARUCO_5X5_100 + + + + + ARUCO_5X5_250 + + + + + ARUCO_5X5_1000 + + + + + ARUCO_6X6_50 + + + + + ARUCO_6X6_100 + + + + + ARUCO_6X6_250 + + + + + ARUCO_6X6_1000 + + + + + ARUCO_7X7_50 + + + + + ARUCO_7X7_100 + + + + + ARUCO_7X7_250 + + + + + ARUCO_7X7_1000 + + + + + ARUCO_ORIGINAL + + + + + APRILTAG_16h5 + + + + + APRILTAG_25h9 + + + + + APRILTAG_36h10 + + + + + APRILTAG_36h11 + + + + + ARUCO_MIP_36h12 + + + + + + - Marker length. The length (m) of the markers' side. 0 means automatic marker length estimation using the depth image (the camera should look at the marker perpendicularly for initialization). + Dictionary to use. true @@ -15822,7 +15813,40 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag - + + + + Linear variance to set on marker detections. If variance is adjusted to ignore orientation (see below) and Optimizer/Strategy=2 (GTSAM): it is the variance of the range factor, with 9999 to disable range factor and to do only bearing. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + + + + + Angular variance to set on marker priors. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + When this setting is false, the landmark's orientation is optimized during graph optimization. When this setting is true, only the position of the landmark is optimized. This can be useful when the landmark's orientation estimation is not reliable. Note that for Optimizer/Strategy=1 (g2o), only Marker/VarianceLinear needs be set if we ignore orientation. For Optimizer/Strategy=2 (GTSAM), instead of optimizing the landmark's position directly, a bearing/range factor is used, with Marker/VarianceLinear as the variance of the range factor (with 9999 to optimize the position with only a bearing factor) and Marker/VarianceAngular as the variance of the bearing factor (pitch/yaw). @@ -15838,181 +15862,409 @@ If set to false, classic RTAB-Map loop closure detection is done using only imag + + + + m + + + 2 + + + 0.000000000000000 + + + 999.000000000000000 + + + 1.000000000000000 + + + 0.000000000000000 + + + + + + + Minimum detection range (0=disabled). + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Maximum detection range (0=unlimited). + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Maximum depth error between all corners of a marker when estimating the marker length (when marker length above is 0). The smaller it is, the more perpendicular the camera should be toward the marker to initialize the length. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + - + + + m + + + 4 + + + 0.000100000000000 + + + 0.010000000000000 + + + 0.100000000000000 + + + + + + + + + + + + 6 + + + 0.000001000000000 + + + 0.001000000000000 + + + 0.001000000000000 + + + + + + + Detector implementation. Note that both aruco and apriltag dictionaries may be used with either strategy. Scroll down to see specific parameters for the strategy used. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + 6 + + + 0.000001000000000 + + + 9999.000000000000000 + + + 0.001000000000000 + + + 0.001000000000000 + + + + + + + + + + + + + + For example, to detect markers 12 and 14 with lengths of 8 and 15 cm respectively, and all markers between 30 and 40 with a length of 10 cm, set "12 0.08|14 0.15|30:40 0.1" + + + List of markers to detect. Format is the marker's ID followed by its length (in meters), markers are separated by a vertical line ("id1 length|id2 length"). We can also define a range of markers with "id1:id2 length" (id2 included). If empty, all markers of the chosen dictionary can be detected and their length is set/estimated based on Marker Length. Mouse over this label for example. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + - - - ArUco + + + 0 - - - - - - 4X4_50 + + + + + + OpenCV - - - - 4X4_100 + + + + + Corner refinement method. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + None + + + + + Subpixel + + + + + Contour + + + + + AprilTag 2 + + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + + + + + + + AprilTag - - - - 4X4_250 - - - - - 4X4_1000 - - - - - 5X5_50 - - - - - 5X5_100 - - - - - 5X5_250 - - - - - 5X5_1000 - - - - - 6X6_50 - - - - - 6X6_100 - - - - - 6X6_250 - - - - - 6X6_1000 - - - - - 7X7_50 - - - - - 7X7_100 - - - - - 7X7_250 - - - - - 7X7_1000 - - - - - ARUCO_ORIGINAL - - - - - APRILTAG_16h5 - - - - - APRILTAG_25h9 - - - - - APRILTAG_36h10 - - - - - APRILTAG_36h11 - - - - - - - - Dictionary to use. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - - None - - - - - Subpixel - - - - - Contour - - - - - AprilTag 2 - - - - - - - - Corner refinement method. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - + + + + + decode_sharpening: How much sharpening should be done to decoded images? This can help decode small tags but may or may not help in odd lighting conditions or low light conditions. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + nthreads: How many threads should be used? + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + 1 + + + 1 + + + + + + + quad_decimate: Detection of quads can be done on a lower-resolution image, improving speed at a cost of pose accuracy and a slight decrease in detection rate. Decoding the binary payload is still done at full resolution. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + refine_edges: When true, the edges of the each quad are adjusted to "snap to" strong gradients nearby. This is useful when decimation is employed, as it can increase the quality of the initial quad estimate substantially. Generally recommended to be on (true). Very computationally inexpensive. Option is ignored if quad_decimate = 1. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + quad_sigma: What Gaussian blur should be applied to the segmented image (used for quad detection?) Parameter is the standard deviation in pixels. Very noisy images benefit from non-zero values (e.g. 0.8). + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + debug: When true, write a variety of debugging images to the working directory where the app started (not RTAB-Map's working directory) at various stages through the detection process. (Somewhat slow). + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + + + + + + + + + + + + + + + 2 + + + 0.000000000000000 + + + 0.050000000000000 + + + 0.250000000000000 + + + + + + + + + + 1 + + + 0.000000000000000 + + + 99.900000000000006 + + + 0.100000000000000 + + + 0.000000000000000 + + + + + + + 1 + + + 1.000000000000000 + + + 2.000000000000000 + + + + + + + + diff --git a/tools/Export/main.cpp b/tools/Export/main.cpp index 18c79d22..572e8453 100644 --- a/tools/Export/main.cpp +++ b/tools/Export/main.cpp @@ -1978,11 +1978,12 @@ int main(int argc, char * argv[]) exportPosesFormat, std::map(robotPoses.lower_bound(1), robotPoses.end()), links, - std::map(robotStamps.lower_bound(1), robotStamps.end())); + std::map(robotStamps.lower_bound(1), robotStamps.end()), + parameters); } else { - rtabmap::graph::exportPoses(outputPath, exportPosesFormat, robotPoses, links, robotStamps); + rtabmap::graph::exportPoses(outputPath, exportPosesFormat, robotPoses, links, robotStamps, parameters); } cv::Vec3f vmin, vmax; graph::computeMinMax(robotPoses, vmin, vmax); @@ -2001,7 +2002,7 @@ int main(int argc, char * argv[]) outputPath = outputDirectory+"/"+baseName+"_camera_poses." + posesExt; else outputPath = outputDirectory+"/"+baseName+"_camera_poses_"+uNumber2Str((int)i)+"." + posesExt; - rtabmap::graph::exportPoses(outputPath, exportPosesFormat, cameraPoses[i], std::multimap(), cameraStamps[i]); + rtabmap::graph::exportPoses(outputPath, exportPosesFormat, cameraPoses[i], std::multimap(), cameraStamps[i], parameters); cv::Vec3f vmin, vmax; graph::computeMinMax(cameraPoses[i], vmin, vmax); printf("%d camera poses exported to \"%s\". (min=[%f,%f,%f] max=[%f,%f,%f])\n", @@ -2014,7 +2015,7 @@ int main(int argc, char * argv[]) if(exportPosesScan) { std::string outputPath=outputDirectory+"/"+baseName+"_scan_poses." + posesExt; - rtabmap::graph::exportPoses(outputPath, exportPosesFormat, scanPoses, std::multimap(), scanStamps); + rtabmap::graph::exportPoses(outputPath, exportPosesFormat, scanPoses, std::multimap(), scanStamps, parameters); cv::Vec3f min, max; graph::computeMinMax(scanPoses, min, max); printf("%d scan poses exported to \"%s\". (min=[%f,%f,%f] max=[%f,%f,%f])\n", @@ -2026,7 +2027,7 @@ int main(int argc, char * argv[]) if(exportPosesScan) { std::string outputPath=outputDirectory+"/"+baseName+"_scan_poses." + posesExt; - rtabmap::graph::exportPoses(outputPath, exportPosesFormat, scanPoses, std::multimap(), scanStamps); + rtabmap::graph::exportPoses(outputPath, exportPosesFormat, scanPoses, std::multimap(), scanStamps, parameters); cv::Vec3f min, max; graph::computeMinMax(scanPoses, min, max); printf("%d scan poses exported to \"%s\". (min=[%f,%f,%f] max=[%f,%f,%f])\n", @@ -2038,7 +2039,7 @@ int main(int argc, char * argv[]) if(exportPosesLandmarks) { std::string outputPath=outputDirectory+"/"+baseName+"_landmark_poses." + posesExt; - rtabmap::graph::exportPoses(outputPath, exportPosesFormat, landmarkPoses, std::multimap(), landmarkStamps); + rtabmap::graph::exportPoses(outputPath, exportPosesFormat, landmarkPoses, std::multimap(), landmarkStamps, parameters); cv::Vec3f min, max; graph::computeMinMax(landmarkPoses, min, max); printf("%d landmark poses exported to \"%s\". (min=[%f,%f,%f] max=[%f,%f,%f])\n", @@ -2050,7 +2051,7 @@ int main(int argc, char * argv[]) if(exportPosesGps) { std::string outputPath=outputDirectory+"/"+baseName+"_gps_poses." + posesExt; - rtabmap::graph::exportPoses(outputPath, exportPosesFormat, gpsPoses, std::multimap(), gpsStamps); + rtabmap::graph::exportPoses(outputPath, exportPosesFormat, gpsPoses, std::multimap(), gpsStamps, parameters); printf("%d GPS poses exported to \"%s\".\n", (int)gpsPoses.size(), outputPath.c_str()); @@ -2058,7 +2059,7 @@ int main(int argc, char * argv[]) if(exportPosesGt) { std::string outputPath=outputDirectory+"/"+baseName+"_gt_poses." + posesExt; - rtabmap::graph::exportPoses(outputPath, exportPosesFormat, gtPoses, std::multimap(), gtStamps); + rtabmap::graph::exportPoses(outputPath, exportPosesFormat, gtPoses, std::multimap(), gtStamps, parameters); printf("%d scan poses exported to \"%s\".\n", (int)gtPoses.size(), outputPath.c_str()); From bb95cc081acb55f233b4034053934262af2bc54c Mon Sep 17 00:00:00 2001 From: matlabbe Date: Wed, 27 May 2026 17:42:21 -0700 Subject: [PATCH 118/126] Fixed backward compatibility with bionic (Qt::ItemFlags) (#1710) * Fixed backward compatibility with bionic (Qt::ItemFlags) * dont remove occupancy grid with not publish last signature data --- corelib/src/Rtabmap.cpp | 2 +- guilib/src/PreferencesDialog.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/corelib/src/Rtabmap.cpp b/corelib/src/Rtabmap.cpp index 7a4c6f33..775572ed 100644 --- a/corelib/src/Rtabmap.cpp +++ b/corelib/src/Rtabmap.cpp @@ -4418,7 +4418,7 @@ bool Rtabmap::process( } if(!_publishLastSignatureData) { - lastSignatureData.sensorData().clearCompressedData(); + lastSignatureData.sensorData().clearCompressedData(true, true, true, false); lastSignatureData.sensorData().clearRawData(); } if(!_rawDataKept) diff --git a/guilib/src/PreferencesDialog.cpp b/guilib/src/PreferencesDialog.cpp index fd0e3cc0..e3da3c46 100644 --- a/guilib/src/PreferencesDialog.cpp +++ b/guilib/src/PreferencesDialog.cpp @@ -5607,7 +5607,7 @@ void PreferencesDialog::updateGlobalDescriptorVisibility() void PreferencesDialog::updateAvailableMarkerDictionaries() { - Qt::ItemFlags enableFlags = Qt::ItemIsEnabled | Qt::ItemIsSelectable; + Qt::ItemFlags enableFlags = Qt::ItemFlags(Qt::ItemIsEnabled) | Qt::ItemIsSelectable; for(int i=0;i<_ui->MarkerDictionary->count();++i) { _ui->MarkerDictionary->setItemData(i, QVariant(static_cast(enableFlags)), Qt::UserRole - 1); } From 6f34498651be5978cb30dbf0d95adde4753cd82a Mon Sep 17 00:00:00 2001 From: matlabbe Date: Fri, 29 May 2026 13:09:21 -0700 Subject: [PATCH 119/126] rtabmap-info: fixing empty parameter strings showing of as NA (#1712) * rtabmap-info: fixing empty parameter strings showing of as NA * show all strings with quotes --- corelib/src/Parameters.cpp | 11 +++++++---- tools/Info/main.cpp | 19 ++++++++++++++----- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/corelib/src/Parameters.cpp b/corelib/src/Parameters.cpp index 83e1135b..ed7e981b 100644 --- a/corelib/src/Parameters.cpp +++ b/corelib/src/Parameters.cpp @@ -113,11 +113,14 @@ ParametersMap Parameters::deserialize(const std::string & parameters) std::list tuplets = uSplit(parameters, ';'); for(std::list::iterator iter=tuplets.begin(); iter!=tuplets.end(); ++iter) { - std::list p = uSplit(*iter, ':'); - if(p.size() == 2) + // Split on the FIRST ':' only. Using uSplit() here would discard + // empty tokens, so a tuplet like "Marker/Lengths:" (legitimate empty + // string value) would lose the value side and be dropped entirely. + size_t colonPos = iter->find(':'); + if(colonPos != std::string::npos && colonPos > 0) { - std::string key = p.front(); - std::string value = p.back(); + std::string key = iter->substr(0, colonPos); + std::string value = iter->substr(colonPos + 1); // look for old parameter name bool addParameter = true; diff --git a/tools/Info/main.cpp b/tools/Info/main.cpp index 192966dc..8604bcc9 100644 --- a/tools/Info/main.cpp +++ b/tools/Info/main.cpp @@ -153,6 +153,15 @@ int main(int argc, char * argv[]) #endif int padding = 35; std::cout << ("Parameters (Yellow=modified, Red=old parameter not used anymore, NA=not in database):\n"); + auto displayValue = [](const std::string & key, const std::string & v) -> std::string { + // Quote string-typed parameters so empty values are visible and + // whitespace-padded values aren't misread as missing. + if(Parameters::getType(key) == "string") + { + return std::string("\"") + v + "\""; + } + return v; + }; for(ParametersMap::iterator iter=parameters.begin(); iter!=parameters.end(); ++iter) { ParametersMap::const_iterator jter = defaultParameters.find(iter->first); @@ -195,7 +204,7 @@ int main(int argc, char * argv[]) #else printf("%s", COLOR_YELLOW); #endif - std::cout << (uFormat("%s%s (%s=%s)\n", uPad(iter->first + "=", padding).c_str(), iter->second.c_str(), otherDatabasePath.empty()?"default":otherDatabasePathName.c_str(), defaultValue.c_str())); + std::cout << (uFormat("%s%s (%s=%s)\n", uPad(iter->first + "=", padding).c_str(), displayValue(iter->first, iter->second).c_str(), otherDatabasePath.empty()?"default":otherDatabasePathName.c_str(), displayValue(iter->first, defaultValue).c_str())); } else if(!diff) { @@ -205,7 +214,7 @@ int main(int argc, char * argv[]) #else printf("%s", COLOR_NORMAL); #endif - std::cout << (uFormat("%s%s\n", uPad(iter->first + "=", padding).c_str(), iter->second.c_str())); + std::cout << (uFormat("%s%s\n", uPad(iter->first + "=", padding).c_str(), displayValue(iter->first, iter->second).c_str())); } } else if(!defaultValueSet) @@ -216,7 +225,7 @@ int main(int argc, char * argv[]) #else printf("%s", COLOR_RED); #endif - std::cout << (uFormat("%s%s (%s=NA)\n", uPad(iter->first + "=", padding).c_str(), iter->second.c_str(), otherDatabasePath.empty()?"default":otherDatabasePathName.c_str())); + std::cout << (uFormat("%s%s (%s=NA)\n", uPad(iter->first + "=", padding).c_str(), displayValue(iter->first, iter->second).c_str(), otherDatabasePath.empty()?"default":otherDatabasePathName.c_str())); } else if(!diff) { @@ -226,7 +235,7 @@ int main(int argc, char * argv[]) #else printf("%s", COLOR_NORMAL); #endif - std::cout << (uFormat("%s%s\n", uPad(iter->first + "=", padding).c_str(), iter->second.c_str())); + std::cout << (uFormat("%s%s\n", uPad(iter->first + "=", padding).c_str(), displayValue(iter->first, iter->second).c_str())); } #ifdef _WIN32 SetConsoleTextAttribute(H,COLOR_NORMAL); @@ -246,7 +255,7 @@ int main(int argc, char * argv[]) #else printf("%s", COLOR_RED); #endif - std::cout << (uFormat("%sNA (%s=\"%s\")\n", uPad(iter->first + "=", padding).c_str(), otherDatabasePath.empty()?"default":otherDatabasePathName.c_str(), iter->second.c_str())); + std::cout << (uFormat("%sNA (%s=%s)\n", uPad(iter->first + "=", padding).c_str(), otherDatabasePath.empty()?"default":otherDatabasePathName.c_str(), displayValue(iter->first, iter->second).c_str())); #ifdef _WIN32 SetConsoleTextAttribute(H,COLOR_NORMAL); From f29676deb0245a6138f5714647f3ae91da91634f Mon Sep 17 00:00:00 2001 From: matlabbe Date: Mon, 1 Jun 2026 21:04:08 -0700 Subject: [PATCH 120/126] PyDetector: adding check if returned descriptors are empty (#1715) * PyDetector: adding check if returned descriptors are empty (addressing #1714) * Throwing error instead of asserting if keypoints and descriptors mistmatch * ading error instead of warning * lets do warnings instead (could be possible that images are blanck) --- corelib/src/Features2d.cpp | 13 +++++++++-- corelib/src/python/PyDetector.cpp | 26 ++++++++++++++++------ corelib/src/superpoint_torch/SuperPoint.cc | 11 ++++++--- 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/corelib/src/Features2d.cpp b/corelib/src/Features2d.cpp index c3c0f93b..1f37ebee 100644 --- a/corelib/src/Features2d.cpp +++ b/corelib/src/Features2d.cpp @@ -887,8 +887,17 @@ cv::Mat Feature2D::generateDescriptors( UASSERT(!image.empty()); UASSERT(image.type() == CV_8UC1); descriptors = generateDescriptorsImpl(image, keypoints); - UASSERT_MSG(descriptors.rows == (int)keypoints.size(), uFormat("descriptors=%d, keypoints=%d", descriptors.rows, (int)keypoints.size()).c_str()); - UDEBUG("Descriptors extracted = %d, remaining kpts=%d", descriptors.rows, (int)keypoints.size()); + if(descriptors.rows != (int)keypoints.size()) + { + UWARN("Descriptor extraction returned %d rows for %d keypoints — " + "clearing keypoints to keep them in sync.", + descriptors.rows, (int)keypoints.size()); + keypoints.clear(); + descriptors = cv::Mat(); + } + else { + UDEBUG("Descriptors extracted = %d, remaining kpts=%d", descriptors.rows, (int)keypoints.size()); + } } return descriptors; } diff --git a/corelib/src/python/PyDetector.cpp b/corelib/src/python/PyDetector.cpp index 8d3101a5..4376b598 100644 --- a/corelib/src/python/PyDetector.cpp +++ b/corelib/src/python/PyDetector.cpp @@ -202,18 +202,30 @@ std::vector PyDetector::generateKeypointsImpl(const cv::Mat & imag arrayPtr = reinterpret_cast(descPtr); int nDesc = PyArray_SHAPE(arrayPtr)[0]; - UASSERT(nDesc = nKpts); int dim = PyArray_SHAPE(arrayPtr)[1]; type = PyArray_TYPE(arrayPtr); UDEBUG("Desc array %dx%d (type=%d)", nDesc, dim, type); - UASSERT_MSG(type == NPY_FLOAT, uFormat("Returned matches should type FLOAT=11, received type=%d", type).c_str()); - c_out = reinterpret_cast(PyArray_DATA(arrayPtr)); - for (int i = 0, kpt_idx = 0; i < nDesc*dim; i+=dim, kpt_idx++) + if(nDesc != nKpts || dim <= 0) { - if(keep_kpt[kpt_idx]) { - cv::Mat descriptor = cv::Mat(1, dim, CV_32FC1, &c_out[i]).clone(); - descriptors_.push_back(descriptor); + UWARN("Python detector returned mismatched arrays: " + "%d keypoints vs %d descriptors (dim=%d). " + "Returning empty features.", + nKpts, nDesc, dim); + keypoints.clear(); + descriptors_ = cv::Mat(); + } + else + { + UASSERT_MSG(type == NPY_FLOAT, uFormat("Returned matches should type FLOAT=11, received type=%d", type).c_str()); + + c_out = reinterpret_cast(PyArray_DATA(arrayPtr)); + for (int i = 0, kpt_idx = 0; i < nDesc*dim; i+=dim, kpt_idx++) + { + if(keep_kpt[kpt_idx]) { + cv::Mat descriptor = cv::Mat(1, dim, CV_32FC1, &c_out[i]).clone(); + descriptors_.push_back(descriptor); + } } } } diff --git a/corelib/src/superpoint_torch/SuperPoint.cc b/corelib/src/superpoint_torch/SuperPoint.cc index 17dbd0cc..94979c1c 100644 --- a/corelib/src/superpoint_torch/SuperPoint.cc +++ b/corelib/src/superpoint_torch/SuperPoint.cc @@ -144,7 +144,12 @@ std::vector SPDetector::detect(const cv::Mat &img, const cv::Mat & UASSERT(img.type() == CV_8UC1); UASSERT(mask.empty() || (mask.type() == CV_8UC1 && img.cols == mask.cols && img.rows == mask.rows)); detected_ = false; - if(model_) + if(!model_) + { + UERROR("No model is loaded!"); + return std::vector(); + } + try { torch::NoGradGuard no_grad_guard; auto x = torch::from_blob(img.data, {1, 1, img.rows, img.cols}, torch::kByte); @@ -199,9 +204,9 @@ std::vector SPDetector::detect(const cv::Mat &img, const cv::Mat & detected_ = true; return keypoints; } - else + catch(const std::exception & e) { - UERROR("No model is loaded!"); + UERROR("SPDetector::detect() threw: %s", e.what()); return std::vector(); } } From 468f6e6f2838e1a185cdf9c4c81ac0c790fa9ca6 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 7 Jun 2026 13:19:31 -0700 Subject: [PATCH 121/126] Fixing empty descriptors on bad signatures (#1714), (bug from #1698) (#1717) * Fixing empty descriptors on bad signatures (#1714), (bug from #1698) * PyDetector: dont assert (just error) if descriptors/keypoints don't match. SuperPoint approaches: try re-initializing superpoint detection on the provided image if descriptors could not be fetched the first time (auto recover). --- corelib/src/Features2d.cpp | 52 +++- corelib/src/Memory.cpp | 257 +++++++++--------- corelib/src/python/PyDetector.cpp | 10 +- .../SuperpointRpautrat.cpp | 2 +- 4 files changed, 185 insertions(+), 136 deletions(-) diff --git a/corelib/src/Features2d.cpp b/corelib/src/Features2d.cpp index 1f37ebee..da83db71 100644 --- a/corelib/src/Features2d.cpp +++ b/corelib/src/Features2d.cpp @@ -2639,7 +2639,31 @@ cv::Mat SuperPointTorch::generateDescriptorsImpl(const cv::Mat & image, std::vec { #ifdef RTABMAP_TORCH UASSERT(!image.empty() && image.channels() == 1 && image.depth() == CV_8U); - return superPoint_->compute(keypoints); + cv::Mat descriptors; + if(!keypoints.empty()) + { + descriptors = superPoint_->compute(keypoints); + if(descriptors.empty()) + { + // superpoint may have been reset between keypoint detection and now, + // re-detect features to re-inialize the descriptors matrix, then + // re-extract descriptors with original keypoints. + UWARN("Re-initializing superpoint on that image to extract descriptors"); + if(!superPoint_->detect(image).empty()) + { + descriptors = superPoint_->compute(keypoints); + if(descriptors.rows == (int)keypoints.size()) + { + UWARN("Sucessfully re-initialized superpoint, returning %d descriptors.", descriptors.rows); + } + } + else + { + UWARN("Failed to re-initialize superpoint on that image, returning empty descriptors."); + } + } + } + return descriptors; #else UWARN("RTAB-Map is not built with Torch support so SuperPoint Torch feature cannot be used!"); return cv::Mat(); @@ -2741,7 +2765,31 @@ cv::Mat SuperPointRpautrat::generateDescriptorsImpl(const cv::Mat & image, std:: { #if defined(RTABMAP_TORCH) && defined(RTABMAP_PYTHON) UASSERT(!image.empty() && image.channels() == 1 && image.depth() == CV_8U); - return superPoint_->compute(keypoints); + cv::Mat descriptors; + if(!keypoints.empty()) + { + descriptors = superPoint_->compute(keypoints); + if(descriptors.empty()) + { + // superpoint may have been reset between keypoint detection and now, + // re-detect features to re-inialize the descriptors matrix, then + // re-extract descriptors with original keypoints. + UWARN("Re-initializing superpoint on that image to extract descriptors"); + if(!superPoint_->detect(image).empty()) + { + descriptors = superPoint_->compute(keypoints); + if(descriptors.rows == (int)keypoints.size()) + { + UWARN("Sucessfully re-initialized superpoint, returning %d descriptors.", descriptors.rows); + } + } + else + { + UWARN("Failed to re-initialize superpoint on that image, returning empty descriptors."); + } + } + } + return descriptors; #else UWARN("RTAB-Map is not built with Torch support so SuperPoint Rpautrat feature cannot be used!"); return cv::Mat(); diff --git a/corelib/src/Memory.cpp b/corelib/src/Memory.cpp index 3b22e79f..37afdbf7 100644 --- a/corelib/src/Memory.cpp +++ b/corelib/src/Memory.cpp @@ -5205,6 +5205,7 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor // is using less features than feature2D->getMaxFeatures() meanWordsPerLocation = 0; } + UDEBUG("ratio=%f, meanWordsPerLocation=%d", _badSignRatio, meanWordsPerLocation); if(_parallelized && !isIntermediateNode) { @@ -5474,41 +5475,90 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor if(stats) stats->addStatistic(Statistics::kTimingMemDescriptors_extraction(), t*1000.0f); UDEBUG("time descriptors (%d) = %fs", descriptors.rows, t); - UDEBUG("ratio=%f, meanWordsPerLocation=%d", _badSignRatio, meanWordsPerLocation); - if(descriptors.rows && descriptors.rows < _badSignRatio * float(meanWordsPerLocation)) + if(!imagesRectified && decimatedData.cameraModels().size()) { - descriptors = cv::Mat(); - } - else - { - if(!imagesRectified && decimatedData.cameraModels().size()) - { - UASSERT_MSG((int)keypoints.size() == descriptors.rows, uFormat("%d vs %d", (int)keypoints.size(), descriptors.rows).c_str()); - std::vector keypointsValid; - keypointsValid.reserve(keypoints.size()); - cv::Mat descriptorsValid; - descriptorsValid.reserve(descriptors.rows); + UASSERT_MSG((int)keypoints.size() == descriptors.rows, uFormat("%d vs %d", (int)keypoints.size(), descriptors.rows).c_str()); + std::vector keypointsValid; + keypointsValid.reserve(keypoints.size()); + cv::Mat descriptorsValid; + descriptorsValid.reserve(descriptors.rows); - //undistort keypoints before projection (RGB-D) - if(decimatedData.cameraModels().size() == 1) + //undistort keypoints before projection (RGB-D) + if(decimatedData.cameraModels().size() == 1) + { + std::vector pointsIn, pointsOut; + cv::KeyPoint::convert(keypoints,pointsIn); + if(decimatedData.cameraModels()[0].D_raw().cols == 6) { +#if CV_MAJOR_VERSION > 2 or (CV_MAJOR_VERSION == 2 and (CV_MINOR_VERSION >4 or (CV_MINOR_VERSION == 4 and CV_SUBMINOR_VERSION >=10))) + // Equidistant / FishEye + // get only k parameters (k1,k2,p1,p2,k3,k4) + cv::Mat D(1, 4, CV_64FC1); + D.at(0,0) = decimatedData.cameraModels()[0].D_raw().at(0,0); + D.at(0,1) = decimatedData.cameraModels()[0].D_raw().at(0,1); + D.at(0,2) = decimatedData.cameraModels()[0].D_raw().at(0,4); + D.at(0,3) = decimatedData.cameraModels()[0].D_raw().at(0,5); + cv::fisheye::undistortPoints(pointsIn, pointsOut, + decimatedData.cameraModels()[0].K_raw(), + D, + decimatedData.cameraModels()[0].R(), + decimatedData.cameraModels()[0].P()); + } + else +#else + UWARN("Too old opencv version (%d,%d,%d) to support fisheye model (min 2.4.10 required)!", + CV_MAJOR_VERSION, CV_MINOR_VERSION, CV_SUBMINOR_VERSION); + } +#endif + { + //RadialTangential + cv::undistortPoints(pointsIn, pointsOut, + decimatedData.cameraModels()[0].K_raw(), + decimatedData.cameraModels()[0].D_raw(), + decimatedData.cameraModels()[0].R(), + decimatedData.cameraModels()[0].P()); + } + UASSERT(pointsOut.size() == keypoints.size()); + for(unsigned int i=0; i=0 && pointsOut.at(i).x=0 && pointsOut.at(i).y= 0 && cameraIndex < (int)decimatedData.cameraModels().size(), + uFormat("cameraIndex=%d, models=%d, kpt.x=%f, subImageWidth=%f (Camera model image width=%d)", + cameraIndex, (int)decimatedData.cameraModels().size(), keypoints[i].pt.x, subImageWidth, decimatedData.cameraModels()[0].imageWidth()).c_str()); + std::vector pointsIn, pointsOut; - cv::KeyPoint::convert(keypoints,pointsIn); - if(decimatedData.cameraModels()[0].D_raw().cols == 6) + pointsIn.push_back(cv::Point2f(keypoints.at(i).pt.x-subImageWidth*cameraIndex, keypoints.at(i).pt.y)); + if(decimatedData.cameraModels()[cameraIndex].D_raw().cols == 6) { #if CV_MAJOR_VERSION > 2 or (CV_MAJOR_VERSION == 2 and (CV_MINOR_VERSION >4 or (CV_MINOR_VERSION == 4 and CV_SUBMINOR_VERSION >=10))) // Equidistant / FishEye // get only k parameters (k1,k2,p1,p2,k3,k4) cv::Mat D(1, 4, CV_64FC1); - D.at(0,0) = decimatedData.cameraModels()[0].D_raw().at(0,0); - D.at(0,1) = decimatedData.cameraModels()[0].D_raw().at(0,1); - D.at(0,2) = decimatedData.cameraModels()[0].D_raw().at(0,4); - D.at(0,3) = decimatedData.cameraModels()[0].D_raw().at(0,5); + D.at(0,0) = decimatedData.cameraModels()[cameraIndex].D_raw().at(0,0); + D.at(0,1) = decimatedData.cameraModels()[cameraIndex].D_raw().at(0,1); + D.at(0,2) = decimatedData.cameraModels()[cameraIndex].D_raw().at(0,4); + D.at(0,3) = decimatedData.cameraModels()[cameraIndex].D_raw().at(0,5); cv::fisheye::undistortPoints(pointsIn, pointsOut, - decimatedData.cameraModels()[0].K_raw(), + decimatedData.cameraModels()[cameraIndex].K_raw(), D, - decimatedData.cameraModels()[0].R(), - decimatedData.cameraModels()[0].P()); + decimatedData.cameraModels()[cameraIndex].R(), + decimatedData.cameraModels()[cameraIndex].P()); } else #else @@ -5519,115 +5569,58 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor { //RadialTangential cv::undistortPoints(pointsIn, pointsOut, - decimatedData.cameraModels()[0].K_raw(), - decimatedData.cameraModels()[0].D_raw(), - decimatedData.cameraModels()[0].R(), - decimatedData.cameraModels()[0].P()); + decimatedData.cameraModels()[cameraIndex].K_raw(), + decimatedData.cameraModels()[cameraIndex].D_raw(), + decimatedData.cameraModels()[cameraIndex].R(), + decimatedData.cameraModels()[cameraIndex].P()); } - UASSERT(pointsOut.size() == keypoints.size()); - for(unsigned int i=0; i=0 && pointsOut[0].x=0 && pointsOut[0].y=0 && pointsOut.at(i).x=0 && pointsOut.at(i).y= 0 && cameraIndex < (int)decimatedData.cameraModels().size(), - uFormat("cameraIndex=%d, models=%d, kpt.x=%f, subImageWidth=%f (Camera model image width=%d)", - cameraIndex, (int)decimatedData.cameraModels().size(), keypoints[i].pt.x, subImageWidth, decimatedData.cameraModels()[0].imageWidth()).c_str()); - - std::vector pointsIn, pointsOut; - pointsIn.push_back(cv::Point2f(keypoints.at(i).pt.x-subImageWidth*cameraIndex, keypoints.at(i).pt.y)); - if(decimatedData.cameraModels()[cameraIndex].D_raw().cols == 6) - { -#if CV_MAJOR_VERSION > 2 or (CV_MAJOR_VERSION == 2 and (CV_MINOR_VERSION >4 or (CV_MINOR_VERSION == 4 and CV_SUBMINOR_VERSION >=10))) - // Equidistant / FishEye - // get only k parameters (k1,k2,p1,p2,k3,k4) - cv::Mat D(1, 4, CV_64FC1); - D.at(0,0) = decimatedData.cameraModels()[cameraIndex].D_raw().at(0,0); - D.at(0,1) = decimatedData.cameraModels()[cameraIndex].D_raw().at(0,1); - D.at(0,2) = decimatedData.cameraModels()[cameraIndex].D_raw().at(0,4); - D.at(0,3) = decimatedData.cameraModels()[cameraIndex].D_raw().at(0,5); - cv::fisheye::undistortPoints(pointsIn, pointsOut, - decimatedData.cameraModels()[cameraIndex].K_raw(), - D, - decimatedData.cameraModels()[cameraIndex].R(), - decimatedData.cameraModels()[cameraIndex].P()); - } - else -#else - UWARN("Too old opencv version (%d,%d,%d) to support fisheye model (min 2.4.10 required)!", - CV_MAJOR_VERSION, CV_MINOR_VERSION, CV_SUBMINOR_VERSION); - } -#endif - { - //RadialTangential - cv::undistortPoints(pointsIn, pointsOut, - decimatedData.cameraModels()[cameraIndex].K_raw(), - decimatedData.cameraModels()[cameraIndex].D_raw(), - decimatedData.cameraModels()[cameraIndex].R(), - decimatedData.cameraModels()[cameraIndex].P()); - } - - if(pointsOut[0].x>=0 && pointsOut[0].x=0 && pointsOut[0].yaddStatistic(Statistics::kTimingMemRectification(), t*1000.0f); - UDEBUG("time rectification = %fs", t); } - if(useProvided3dPoints && keypoints.size() != data.keypoints3D().size()) + keypoints = keypointsValid; + descriptors = descriptorsValid; + + t = timer.ticks(); + if(stats) stats->addStatistic(Statistics::kTimingMemRectification(), t*1000.0f); + UDEBUG("time rectification = %fs", t); + } + + if(useProvided3dPoints && keypoints.size() != data.keypoints3D().size()) + { + UDEBUG("Using provided 3d points (%d->%d)", (int)data.keypoints3D().size(), (int)keypoints.size()); + keypoints3D.resize(keypoints.size()); + for(size_t i=0; i%d)", (int)data.keypoints3D().size(), (int)keypoints.size()); - keypoints3D.resize(keypoints.size()); - for(size_t i=0; igenerateKeypoints3D(decimatedData, keypoints); - t = timer.ticks(); - if(stats) stats->addStatistic(Statistics::kTimingMemKeypoints_3D(), t*1000.0f); - UDEBUG("time keypoints 3D (%d) = %fs", (int)keypoints3D.size(), t); - } - if(depthMask.empty() && (_feature2D->getMinDepth() > 0.0f || _feature2D->getMaxDepth() > 0.0f)) - { - _feature2D->filterKeypointsByDepth(keypoints, descriptors, keypoints3D, _feature2D->getMinDepth(), _feature2D->getMaxDepth()); + UASSERT(keypoints[i].class_id < (int)data.keypoints3D().size()); + keypoints3D[i] = data.keypoints3D()[keypoints[i].class_id]; } } + else if(useProvided3dPoints && keypoints.size() == data.keypoints3D().size()) + { + UDEBUG("Using provided 3d points (%d)", (int)data.keypoints3D().size()); + keypoints3D = data.keypoints3D(); + } + else if((!decimatedData.depthRaw().empty() && decimatedData.cameraModels().size() && decimatedData.cameraModels()[0].isValidForProjection()) || + (!decimatedData.rightRaw().empty() && decimatedData.stereoCameraModels().size() && decimatedData.stereoCameraModels()[0].isValidForProjection())) + { + keypoints3D = _feature2D->generateKeypoints3D(decimatedData, keypoints); + t = timer.ticks(); + if(stats) stats->addStatistic(Statistics::kTimingMemKeypoints_3D(), t*1000.0f); + UDEBUG("time keypoints 3D (%d) = %fs", (int)keypoints3D.size(), t); + } + if(depthMask.empty() && (_feature2D->getMinDepth() > 0.0f || _feature2D->getMaxDepth() > 0.0f)) + { + _feature2D->filterKeypointsByDepth(keypoints, descriptors, keypoints3D, _feature2D->getMinDepth(), _feature2D->getMaxDepth()); + } } else if(data.imageRaw().empty()) { @@ -5853,12 +5846,6 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor t = timer.ticks(); if(stats) stats->addStatistic(Statistics::kTimingMemKeypoints_3D(), t*1000.0f); UDEBUG("time keypoints 3D (%d) = %fs", (int)keypoints3D.size(), t); - - UDEBUG("ratio=%f, meanWordsPerLocation=%d", _badSignRatio, meanWordsPerLocation); - if(descriptors.rows && descriptors.rows < _badSignRatio * float(meanWordsPerLocation)) - { - descriptors = cv::Mat(); - } } } @@ -5884,7 +5871,9 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor bool addedToDictionary = false; if(!keypoints.empty()) { - if(descriptors.rows && !isIntermediateNode) + if(descriptors.rows && + !isIntermediateNode && // don't add intermediate nodes to dictionary + descriptors.rows >= int(_badSignRatio * float(meanWordsPerLocation))) // don't add bad signatures to dictionary { // In case the number of features we want to do quantization is lower // than extracted ones (that would be used for transform estimation) @@ -5991,7 +5980,11 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor else { // Set all words as not used in dictionary - wordIds.resize(keypoints.size(),-1); + int negIndex = -1; + for(size_t i=0; i PyDetector::generateKeypointsImpl(const cv::Mat & imag cv::Mat PyDetector::generateDescriptorsImpl(const cv::Mat & image, std::vector & keypoints) const { - UASSERT((int)keypoints.size() == descriptors_.rows); + if(!keypoints.empty() && (int)keypoints.size() != descriptors_.rows) + { + UERROR("The number of keypoints (%ld) doesn't match the number of buffered " + "descriptors (%d). PyDetector's descriptors extraction should " + "be called right after keypoints detection, with same keypoints " + "returned by the detection. Returning empty descriptors.", + keypoints.size(), descriptors_.rows); + return cv::Mat(); + } return descriptors_; } diff --git a/corelib/src/superpoint_rpautrat/SuperpointRpautrat.cpp b/corelib/src/superpoint_rpautrat/SuperpointRpautrat.cpp index ffdbd760..baa37593 100644 --- a/corelib/src/superpoint_rpautrat/SuperpointRpautrat.cpp +++ b/corelib/src/superpoint_rpautrat/SuperpointRpautrat.cpp @@ -130,7 +130,7 @@ cv::Mat SPDetectorRpautrat::compute(const std::vector &keypoints) { if(!detected_) { - UERROR("SPDetector has been reset before extracting the descriptors! detect() should be called before compute()."); + UERROR("SPDetectorRpautrat has been reset before extracting the descriptors! detect() should be called before compute()."); return cv::Mat(); } if(keypoints.empty()) From 1dddb350b929fd3c476536dd76638456e84d38cf Mon Sep 17 00:00:00 2001 From: matlabbe Date: Tue, 9 Jun 2026 12:05:30 -0700 Subject: [PATCH 122/126] TORO: adding support to load 2d graph (#1718) * TORO: adding support to load 2d graph * making marker detection with multicam less verbose * wrong edge index size * skip landmarks --- corelib/src/MarkerDetector.cpp | 4 +- corelib/src/optimizer/OptimizerTORO.cpp | 92 +++++++++++++++++++++---- 2 files changed, 79 insertions(+), 17 deletions(-) diff --git a/corelib/src/MarkerDetector.cpp b/corelib/src/MarkerDetector.cpp index db3a4fd6..14840827 100644 --- a/corelib/src/MarkerDetector.cpp +++ b/corelib/src/MarkerDetector.cpp @@ -551,7 +551,7 @@ std::map MarkerDetector::detect(const cv::Mat & image, } if(idsAdded.find(det->id)!=idsAdded.end()) { - UWARN("Marker %d already added by another camera, ignoring detection from camera %d", det->id, cameraIndex); + UDEBUG("Marker %d already added by another camera, ignoring detection from camera %d", det->id, cameraIndex); continue; } @@ -668,7 +668,7 @@ std::map MarkerDetector::detect(const cv::Mat & image, } if(idsAdded.find(id) != idsAdded.end()) { - UWARN("Marker %d already added by another camera, ignoring detection from camera %d", id, cameraIndex); + UDEBUG("Marker %d already added by another camera, ignoring detection from camera %d", id, cameraIndex); continue; } diff --git a/corelib/src/optimizer/OptimizerTORO.cpp b/corelib/src/optimizer/OptimizerTORO.cpp index 1ac00398..48c6f075 100644 --- a/corelib/src/optimizer/OptimizerTORO.cpp +++ b/corelib/src/optimizer/OptimizerTORO.cpp @@ -380,7 +380,7 @@ bool OptimizerTORO::saveGraph( if(file) { - for (std::map::const_iterator iter = poses.begin(); iter != poses.end(); ++iter) + for (std::map::const_iterator iter = poses.lower_bound(0); iter != poses.end(); ++iter) { if (isSlam2d()) { @@ -409,7 +409,7 @@ bool OptimizerTORO::saveGraph( for(std::multimap::const_iterator iter = edgeConstraints.begin(); iter!=edgeConstraints.end(); ++iter) { - if (iter->second.type() != Link::kPosePrior && iter->second.type() != Link::kGravity) + if (iter->second.from() != iter->second.to() && iter->second.type() != Link::kLandmark) { if (isSlam2d()) { @@ -494,9 +494,31 @@ bool OptimizerTORO::loadGraph( while ( fgets (line , 400 , file) != NULL ) { std::vector strList = uListToVector(uSplit(uReplaceChar(line, '\n', ' '), ' ')); - if(strList.size() == 8) + if(strList.empty()) { - //VERTEX3 + continue; + } + const std::string & tag = strList[0]; + if(tag.compare("VERTEX2") == 0 && strList.size() == 5) + { + //VERTEX2 id x y theta + int id = atoi(strList[1].c_str()); + float x = uStr2Float(strList[2]); + float y = uStr2Float(strList[3]); + float theta = uStr2Float(strList[4]); + Transform pose(x, y, theta); + if(poses.find(id) == poses.end()) + { + poses.insert(std::make_pair(id, pose)); + } + else + { + UFATAL("Pose %d already added", id); + } + } + else if(tag.compare("VERTEX3") == 0 && strList.size() == 8) + { + //VERTEX3 id x y z roll pitch yaw int id = atoi(strList[1].c_str()); float x = uStr2Float(strList[2]); float y = uStr2Float(strList[3]); @@ -514,9 +536,44 @@ bool OptimizerTORO::loadGraph( UFATAL("Pose %d already added", id); } } - else if(strList.size() == 30) + else if(tag.compare("EDGE2") == 0 && strList.size() == 12) { - //EDGE3 + //EDGE2 observed_vertex_id observing_vertex_id x y theta inf_11 inf_12 inf_13 inf_22 inf_23 inf_33 + int idFrom = atoi(strList[1].c_str()); + int idTo = atoi(strList[2].c_str()); + float x = uStr2Float(strList[3]); + float y = uStr2Float(strList[4]); + float theta = uStr2Float(strList[5]); + cv::Mat informationMatrix = cv::Mat::eye(6,6,CV_64FC1); + informationMatrix.at(0,0) = uStr2Float(strList[6]); // x-x + informationMatrix.at(0,1) = uStr2Float(strList[7]); // x-y + informationMatrix.at(0,5) = uStr2Float(strList[8]); // x-theta + informationMatrix.at(1,1) = uStr2Float(strList[9]); // y-y + informationMatrix.at(1,5) = uStr2Float(strList[10]); // y-theta + informationMatrix.at(5,5) = uStr2Float(strList[11]); // theta-theta + // symmetric counterparts + informationMatrix.at(1,0) = informationMatrix.at(0,1); + informationMatrix.at(5,0) = informationMatrix.at(0,5); + informationMatrix.at(5,1) = informationMatrix.at(1,5); + informationMatrix.at(2,2) = 0.00010001; // 9999 cov + informationMatrix.at(3,3) = 0.00010001; // 9999 cov + informationMatrix.at(4,4) = 0.00010001; // 9999 cov + UASSERT_MSG(informationMatrix.at(0,0) > 0.0 && informationMatrix.at(1,1) > 0.0 && informationMatrix.at(5,5) > 0.0, uFormat("Information matrix should not be null! line=\"%s\"", line).c_str()); + Transform transform(x, y, theta); + if(poses.find(idFrom) != poses.end() && poses.find(idTo) != poses.end()) + { + //Link type is unknown + Link link(idFrom, idTo, Link::kUndef, transform, informationMatrix); + edgeConstraints.insert(std::pair(idFrom, link)); + } + else + { + UERROR("Referred poses from the link (%d->%d) don't exist! Link ignored!", idFrom, idTo); + } + } + else if(tag.compare("EDGE3") == 0 && strList.size() == 30) + { + //EDGE3 observed_vertex_id observing_vertex_id x y z roll pitch yaw inf_11 inf_12 .. inf_16 inf_22 .. inf_66 int idFrom = atoi(strList[1].c_str()); int idTo = atoi(strList[2].c_str()); float x = uStr2Float(strList[3]); @@ -525,15 +582,20 @@ bool OptimizerTORO::loadGraph( float roll = uStr2Float(strList[6]); float pitch = uStr2Float(strList[7]); float yaw = uStr2Float(strList[8]); + // upper triangle is stored row by row (same order as saveGraph) cv::Mat informationMatrix(6,6,CV_64FC1); - informationMatrix.at(3,3) = uStr2Float(strList[9]); - informationMatrix.at(4,4) = uStr2Float(strList[15]); - informationMatrix.at(5,5) = uStr2Float(strList[20]); - UASSERT_MSG(informationMatrix.at(3,3) > 0.0 && informationMatrix.at(4,4) > 0.0 && informationMatrix.at(5,5) > 0.0, uFormat("Information matrix should not be null! line=\"%s\"", line).c_str()); - informationMatrix.at(0,0) = uStr2Float(strList[24]); - informationMatrix.at(1,1) = uStr2Float(strList[27]); - informationMatrix.at(2,2) = uStr2Float(strList[29]); - UASSERT_MSG(informationMatrix.at(0,0) > 0.0 && informationMatrix.at(1,1) > 0.0 && informationMatrix.at(2,2) > 0.0, uFormat("Information matrix should not be null! line=\"%s\"", line).c_str()); + int index = 9; + for(int i=0; i<6; ++i) + { + for(int j=i; j<6; ++j) + { + double value = uStr2Float(strList[index++]); + informationMatrix.at(i,j) = value; + informationMatrix.at(j,i) = value; + } + } + UASSERT_MSG(informationMatrix.at(0,0) > 0.0 && informationMatrix.at(1,1) > 0.0 && informationMatrix.at(2,2) > 0.0 && + informationMatrix.at(3,3) > 0.0 && informationMatrix.at(4,4) > 0.0 && informationMatrix.at(5,5) > 0.0, uFormat("Information matrix should not be null! line=\"%s\"", line).c_str()); Transform transform(x, y, z, roll, pitch, yaw); if(poses.find(idFrom) != poses.end() && poses.find(idTo) != poses.end()) { @@ -546,7 +608,7 @@ bool OptimizerTORO::loadGraph( UERROR("Referred poses from the link (%d->%d) don't exist! Link ignored!", idFrom, idTo); } } - else if(strList.size()) + else { UFATAL("Error parsing graph file %s on line \"%s\" (strList.size()=%d)", fileName.c_str(), line, (int)strList.size()); } From 9f464db7c9d87867cb0dad98871a1614db0b62cf Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sat, 13 Jun 2026 16:20:37 -0700 Subject: [PATCH 123/126] OpenVINS config file support (#1719) --- CMakeLists.txt | 21 +-- cmake_modules/FindOpenVINS.cmake | 44 ++++++ corelib/include/rtabmap/core/Parameters.h | 113 +++++++------- corelib/src/CMakeLists.txt | 8 +- corelib/src/odometry/OdometryOpenVINS.cpp | 79 ++++++++++ .../include/rtabmap/gui/PreferencesDialog.h | 1 + guilib/src/PreferencesDialog.cpp | 19 +++ guilib/src/ui/preferencesDialog.ui | 145 +++++++++++------- 8 files changed, 293 insertions(+), 137 deletions(-) create mode 100644 cmake_modules/FindOpenVINS.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 60986d09..2f2206b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -842,22 +842,7 @@ IF(WITH_VINS_FUSION) ENDIF(WITH_VINS_FUSION) IF(WITH_OPENVINS) - FIND_PACKAGE(ov_msckf) - # On ROS2, the indirect includes and libraries - # are not forwarded by ov_msckf target, append them manually - FIND_PACKAGE(ov_core) - FIND_PACKAGE(ov_init) - IF(ov_msckf_FOUND AND ov_core_FOUND AND ov_init_FOUND) - SET(ov_msckf_INCLUDE_DIRS - ${ov_msckf_INCLUDE_DIRS} - ${ov_core_INCLUDE_DIRS} - ${ov_init_INCLUDE_DIRS}) - SET(ov_msckf_LIBRARIES - ${ov_msckf_LIBRARIES} - ${ov_core_LIBRARIES} - ${ov_init_LIBRARIES}) - MESSAGE(STATUS "Found OpenVINS: ${ov_msckf_INCLUDE_DIRS}") - ENDIF() + FIND_PACKAGE(OpenVINS) ENDIF(WITH_OPENVINS) IF(WITH_FASTCV) @@ -1223,7 +1208,7 @@ ENDIF() IF(NOT vins_FOUND) SET(VINSFUSION "//") ENDIF() -IF(NOT ov_msckf_FOUND) +IF(NOT OpenVINS_FOUND) SET(OPENVINS "//") ENDIF() IF(NOT CUVSLAM_FOUND) @@ -1981,7 +1966,7 @@ ELSE() MESSAGE(STATUS " With VINS-Fusion = NO (VINS-Fusion not found)") ENDIF() -IF(ov_msckf_FOUND) +IF(OpenVINS_FOUND) MESSAGE(STATUS " With OpenVINS = YES (License: GPLv3)") ELSEIF(NOT WITH_OPENVINS) MESSAGE(STATUS " With OpenVINS = NO (WITH_OPENVINS=OFF)") diff --git a/cmake_modules/FindOpenVINS.cmake b/cmake_modules/FindOpenVINS.cmake new file mode 100644 index 00000000..66029218 --- /dev/null +++ b/cmake_modules/FindOpenVINS.cmake @@ -0,0 +1,44 @@ +# Find OpenVINS +# +# We search for a vins installation in ROS/ROS2 first, then fallback on +# ros-free library in common install paths + +FIND_PACKAGE(ov_msckf QUIET) +IF(ov_msckf_FOUND) + # On ROS2, the indirect includes and libraries + # are not forwarded by ov_msckf target, append them manually + FIND_PACKAGE(ov_core) + FIND_PACKAGE(ov_init) + IF(ov_msckf_FOUND AND ov_core_FOUND AND ov_init_FOUND) + SET(OpenVINS_FOUND TRUE) + SET(OpenVINS_INCLUDE_DIRS + ${ov_msckf_INCLUDE_DIRS} + ${ov_core_INCLUDE_DIRS} + ${ov_init_INCLUDE_DIRS}) + SET(OpenVINS_LIBRARIES + ${ov_msckf_LIBRARIES} + ${ov_core_LIBRARIES} + ${ov_init_LIBRARIES}) + ENDIF() +ELSE() + find_path(OpenVINS_INCLUDE_DIR NAMES core/VioManager.h PATH_SUFFIXES open_vins) + find_library(OpenVINS_LIBRARY NAMES ov_msckf_lib) + IF (OpenVINS_INCLUDE_DIR AND OpenVINS_LIBRARY) + SET(OpenVINS_FOUND TRUE) + SET(OpenVINS_INCLUDE_DIRS ${OpenVINS_INCLUDE_DIR}) + SET(OpenVINS_LIBRARIES ${OpenVINS_LIBRARY}) + ENDIF() +ENDIF() + +IF (OpenVINS_FOUND) + # show which OpenVINS was found only if not quiet + IF (NOT OpenVINS_FIND_QUIETLY) + MESSAGE(STATUS "Found OpenVINS: ${OpenVINS_LIBRARIES} ${OpenVINS_INCLUDE_DIRS}") + ENDIF (NOT OpenVINS_FIND_QUIETLY) +ELSE (OpenVINS_FOUND) + # fatal error if OpenVINS is required but not found + IF (OpenVINS_FIND_REQUIRED) + MESSAGE(FATAL_ERROR "Could not find OpenVINS") + ENDIF (OpenVINS_FIND_REQUIRED) +ENDIF (OpenVINS_FOUND) + diff --git a/corelib/include/rtabmap/core/Parameters.h b/corelib/include/rtabmap/core/Parameters.h index 3944acf6..e3c8d574 100644 --- a/corelib/include/rtabmap/core/Parameters.h +++ b/corelib/include/rtabmap/core/Parameters.h @@ -620,66 +620,67 @@ class RTABMAP_CORE_EXPORT Parameters RTABMAP_PARAM_STR(OdomVINSFusion, ConfigPath, "", "Path of VINS-Fusion config file."); // Odometry OpenVINS - RTABMAP_PARAM(OdomOpenVINS, UseStereo, bool, true, "If we have more than 1 camera, if we should try to track stereo constraints between pairs"); - RTABMAP_PARAM(OdomOpenVINS, UseKLT, bool, true, "If true we will use KLT, otherwise use a ORB descriptor + robust matching"); - RTABMAP_PARAM(OdomOpenVINS, NumPts, int, 200, "Number of points (per camera) we will extract and try to track"); - RTABMAP_PARAM(OdomOpenVINS, MinPxDist, int, 15, "Eistance between features (features near each other provide less information)"); - RTABMAP_PARAM(OdomOpenVINS, FiTriangulate1d, bool, false, "If we should perform 1d triangulation instead of 3d"); - RTABMAP_PARAM(OdomOpenVINS, FiRefineFeatures, bool, true, "If we should perform Levenberg-Marquardt refinement"); - RTABMAP_PARAM(OdomOpenVINS, FiMaxRuns, int, 5, "Max runs for Levenberg-Marquardt"); - RTABMAP_PARAM(OdomOpenVINS, FiMaxBaseline, double, 40, "Max baseline ratio to accept triangulated features"); - RTABMAP_PARAM(OdomOpenVINS, FiMaxCondNumber, double, 10000, "Max condition number of linear triangulation matrix accept triangulated features"); + RTABMAP_PARAM_STR(OdomOpenVINS, ConfigPath, "", "Path of OpenVINS config file (*.yaml). Same format used than OpenVINS library. Note that any parameter from that config file will overwrite the same parameter in OdomOpenVINS group."); + RTABMAP_PARAM(OdomOpenVINS, UseStereo, bool, true, "If we have more than 1 camera, if we should try to track stereo constraints between pairs."); + RTABMAP_PARAM(OdomOpenVINS, UseKLT, bool, true, "If true we will use KLT, otherwise use a ORB descriptor + robust matching."); + RTABMAP_PARAM(OdomOpenVINS, NumPts, int, 200, "Number of points (per camera) we will extract and try to track."); + RTABMAP_PARAM(OdomOpenVINS, MinPxDist, int, 15, "Eistance between features (features near each other provide less information)."); + RTABMAP_PARAM(OdomOpenVINS, FiTriangulate1d, bool, false, "If we should perform 1d triangulation instead of 3d."); + RTABMAP_PARAM(OdomOpenVINS, FiRefineFeatures, bool, true, "If we should perform Levenberg-Marquardt refinement."); + RTABMAP_PARAM(OdomOpenVINS, FiMaxRuns, int, 5, "Max runs for Levenberg-Marquardt."); + RTABMAP_PARAM(OdomOpenVINS, FiMaxBaseline, double, 40, "Max baseline ratio to accept triangulated features."); + RTABMAP_PARAM(OdomOpenVINS, FiMaxCondNumber, double, 10000, "Max condition number of linear triangulation matrix accept triangulated features."); - RTABMAP_PARAM(OdomOpenVINS, UseFEJ, bool, true, "If first-estimate Jacobians should be used (enable for good consistency)"); - RTABMAP_PARAM(OdomOpenVINS, Integration, int, 1, "0=discrete, 1=rk4, 2=analytical (if rk4 or analytical used then analytical covariance propagation is used)"); - RTABMAP_PARAM(OdomOpenVINS, CalibCamExtrinsics, bool, false, "Bool to determine whether or not to calibrate imu-to-camera pose"); - RTABMAP_PARAM(OdomOpenVINS, CalibCamIntrinsics, bool, false, "Bool to determine whether or not to calibrate camera intrinsics"); - RTABMAP_PARAM(OdomOpenVINS, CalibCamTimeoffset, bool, false, "Bool to determine whether or not to calibrate camera to IMU time offset"); - RTABMAP_PARAM(OdomOpenVINS, CalibIMUIntrinsics, bool, false, "Bool to determine whether or not to calibrate the IMU intrinsics"); - RTABMAP_PARAM(OdomOpenVINS, CalibIMUGSensitivity, bool, false, "Bool to determine whether or not to calibrate the Gravity sensitivity"); - RTABMAP_PARAM(OdomOpenVINS, MaxClones, int, 11, "Max clone size of sliding window"); - RTABMAP_PARAM(OdomOpenVINS, MaxSLAM, int, 50, "Max number of estimated SLAM features"); - RTABMAP_PARAM(OdomOpenVINS, MaxSLAMInUpdate, int, 25, "Max number of SLAM features we allow to be included in a single EKF update."); - RTABMAP_PARAM(OdomOpenVINS, MaxMSCKFInUpdate, int, 50, "Max number of MSCKF features we will use at a given image timestep."); - RTABMAP_PARAM(OdomOpenVINS, FeatRepMSCKF, int, 0, "What representation our features are in (msckf features)"); - RTABMAP_PARAM(OdomOpenVINS, FeatRepSLAM, int, 4, "What representation our features are in (slam features)"); - RTABMAP_PARAM(OdomOpenVINS, DtSLAMDelay, double, 0.0, "Delay, in seconds, that we should wait from init before we start estimating SLAM features"); - RTABMAP_PARAM(OdomOpenVINS, GravityMag, double, 9.81, "Gravity magnitude in the global frame (i.e. should be 9.81 typically)"); - RTABMAP_PARAM_STR(OdomOpenVINS, LeftMaskPath, "", "Mask for left image"); - RTABMAP_PARAM_STR(OdomOpenVINS, RightMaskPath, "", "Mask for right image"); + RTABMAP_PARAM(OdomOpenVINS, UseFEJ, bool, true, "If first-estimate Jacobians should be used (enable for good consistency)."); + RTABMAP_PARAM(OdomOpenVINS, Integration, int, 1, "0=discrete, 1=rk4, 2=analytical (if rk4 or analytical used then analytical covariance propagation is used)."); + RTABMAP_PARAM(OdomOpenVINS, CalibCamExtrinsics, bool, false, "Bool to determine whether or not to calibrate imu-to-camera pose."); + RTABMAP_PARAM(OdomOpenVINS, CalibCamIntrinsics, bool, false, "Bool to determine whether or not to calibrate camera intrinsics."); + RTABMAP_PARAM(OdomOpenVINS, CalibCamTimeoffset, bool, false, "Bool to determine whether or not to calibrate camera to IMU time offset."); + RTABMAP_PARAM(OdomOpenVINS, CalibIMUIntrinsics, bool, false, "Bool to determine whether or not to calibrate the IMU intrinsics."); + RTABMAP_PARAM(OdomOpenVINS, CalibIMUGSensitivity, bool, false, "Bool to determine whether or not to calibrate the Gravity sensitivity."); + RTABMAP_PARAM(OdomOpenVINS, MaxClones, int, 11, "Max clone size of sliding window."); + RTABMAP_PARAM(OdomOpenVINS, MaxSLAM, int, 50, "Max number of estimated SLAM features."); + RTABMAP_PARAM(OdomOpenVINS, MaxSLAMInUpdate, int, 25, "Max number of SLAM features we allow to be included in a single EKF update.."); + RTABMAP_PARAM(OdomOpenVINS, MaxMSCKFInUpdate, int, 50, "Max number of MSCKF features we will use at a given image timestep.."); + RTABMAP_PARAM(OdomOpenVINS, FeatRepMSCKF, int, 0, "What representation our features are in (msckf features)."); + RTABMAP_PARAM(OdomOpenVINS, FeatRepSLAM, int, 4, "What representation our features are in (slam features)."); + RTABMAP_PARAM(OdomOpenVINS, DtSLAMDelay, double, 0.0, "Delay, in seconds, that we should wait from init before we start estimating SLAM features."); + RTABMAP_PARAM(OdomOpenVINS, GravityMag, double, 9.81, "Gravity magnitude in the global frame (i.e. should be 9.81 typically)."); + RTABMAP_PARAM_STR(OdomOpenVINS, LeftMaskPath, "", "Mask for left image."); + RTABMAP_PARAM_STR(OdomOpenVINS, RightMaskPath, "", "Mask for right image."); - RTABMAP_PARAM(OdomOpenVINS, InitWindowTime, double, 2.0, "Amount of time we will initialize over (seconds)"); - RTABMAP_PARAM(OdomOpenVINS, InitIMUThresh, double, 1.0, "Variance threshold on our acceleration to be classified as moving"); - RTABMAP_PARAM(OdomOpenVINS, InitMaxDisparity, double, 10.0, "Max disparity to consider the platform stationary (dependent on resolution)"); - RTABMAP_PARAM(OdomOpenVINS, InitMaxFeatures, int, 50, "How many features to track during initialization (saves on computation)"); - RTABMAP_PARAM(OdomOpenVINS, InitDynUse, bool, false, "If dynamic initialization should be used"); - RTABMAP_PARAM(OdomOpenVINS, InitDynMLEOptCalib, bool, false, "If we should optimize calibration during intialization (not recommended)"); - RTABMAP_PARAM(OdomOpenVINS, InitDynMLEMaxIter, int, 50, "How many iterations the MLE refinement should use (zero to skip the MLE)"); - RTABMAP_PARAM(OdomOpenVINS, InitDynMLEMaxTime, double, 0.05, "How many seconds the MLE should be completed in"); - RTABMAP_PARAM(OdomOpenVINS, InitDynMLEMaxThreads, int, 6, "How many threads the MLE should use"); - RTABMAP_PARAM(OdomOpenVINS, InitDynNumPose, int, 6, "Number of poses to use within our window time (evenly spaced)"); - RTABMAP_PARAM(OdomOpenVINS, InitDynMinDeg, double, 10.0, "Orientation change needed to try to init"); - RTABMAP_PARAM(OdomOpenVINS, InitDynInflationOri, double, 10.0, "What to inflate the recovered q_GtoI covariance by"); - RTABMAP_PARAM(OdomOpenVINS, InitDynInflationVel, double, 100.0, "What to inflate the recovered v_IinG covariance by"); - RTABMAP_PARAM(OdomOpenVINS, InitDynInflationBg, double, 10.0, "What to inflate the recovered bias_g covariance by"); - RTABMAP_PARAM(OdomOpenVINS, InitDynInflationBa, double, 100.0, "What to inflate the recovered bias_a covariance by"); - RTABMAP_PARAM(OdomOpenVINS, InitDynMinRecCond, double, 1e-15, "Reciprocal condition number thresh for info inversion"); + RTABMAP_PARAM(OdomOpenVINS, InitWindowTime, double, 2.0, "Amount of time we will initialize over (seconds)."); + RTABMAP_PARAM(OdomOpenVINS, InitIMUThresh, double, 1.0, "Variance threshold on our acceleration to be classified as moving."); + RTABMAP_PARAM(OdomOpenVINS, InitMaxDisparity, double, 10.0, "Max disparity to consider the platform stationary (dependent on resolution)."); + RTABMAP_PARAM(OdomOpenVINS, InitMaxFeatures, int, 50, "How many features to track during initialization (saves on computation)."); + RTABMAP_PARAM(OdomOpenVINS, InitDynUse, bool, false, "If dynamic initialization should be used."); + RTABMAP_PARAM(OdomOpenVINS, InitDynMLEOptCalib, bool, false, "If we should optimize calibration during intialization (not recommended)."); + RTABMAP_PARAM(OdomOpenVINS, InitDynMLEMaxIter, int, 50, "How many iterations the MLE refinement should use (zero to skip the MLE)."); + RTABMAP_PARAM(OdomOpenVINS, InitDynMLEMaxTime, double, 0.05, "How many seconds the MLE should be completed in."); + RTABMAP_PARAM(OdomOpenVINS, InitDynMLEMaxThreads, int, 6, "How many threads the MLE should use."); + RTABMAP_PARAM(OdomOpenVINS, InitDynNumPose, int, 6, "Number of poses to use within our window time (evenly spaced)."); + RTABMAP_PARAM(OdomOpenVINS, InitDynMinDeg, double, 10.0, "Orientation change needed to try to init."); + RTABMAP_PARAM(OdomOpenVINS, InitDynInflationOri, double, 10.0, "What to inflate the recovered q_GtoI covariance by."); + RTABMAP_PARAM(OdomOpenVINS, InitDynInflationVel, double, 100.0, "What to inflate the recovered v_IinG covariance by."); + RTABMAP_PARAM(OdomOpenVINS, InitDynInflationBg, double, 10.0, "What to inflate the recovered bias_g covariance by."); + RTABMAP_PARAM(OdomOpenVINS, InitDynInflationBa, double, 100.0, "What to inflate the recovered bias_a covariance by."); + RTABMAP_PARAM(OdomOpenVINS, InitDynMinRecCond, double, 1e-15, "Reciprocal condition number thresh for info inversion."); - RTABMAP_PARAM(OdomOpenVINS, TryZUPT, bool, true, "If we should try to use zero velocity update"); - RTABMAP_PARAM(OdomOpenVINS, ZUPTChi2Multiplier, double, 0.0, "Chi2 multiplier for zero velocity"); - RTABMAP_PARAM(OdomOpenVINS, ZUPTMaxVelodicy, double, 0.1, "Max velocity we will consider to try to do a zupt (i.e. if above this, don't do zupt)"); - RTABMAP_PARAM(OdomOpenVINS, ZUPTNoiseMultiplier, double, 10.0, "Multiplier of our zupt measurement IMU noise matrix (default should be 1.0)"); - RTABMAP_PARAM(OdomOpenVINS, ZUPTMaxDisparity, double, 0.5, "Max disparity we will consider to try to do a zupt (i.e. if above this, don't do zupt)"); - RTABMAP_PARAM(OdomOpenVINS, ZUPTOnlyAtBeginning, bool, false, "If we should only use the zupt at the very beginning static initialization phase"); + RTABMAP_PARAM(OdomOpenVINS, TryZUPT, bool, true, "If we should try to use zero velocity update."); + RTABMAP_PARAM(OdomOpenVINS, ZUPTChi2Multiplier, double, 0.0, "Chi2 multiplier for zero velocity."); + RTABMAP_PARAM(OdomOpenVINS, ZUPTMaxVelodicy, double, 0.1, "Max velocity we will consider to try to do a zupt (i.e. if above this, don't do zupt)."); + RTABMAP_PARAM(OdomOpenVINS, ZUPTNoiseMultiplier, double, 10.0, "Multiplier of our zupt measurement IMU noise matrix (default should be 1.0)."); + RTABMAP_PARAM(OdomOpenVINS, ZUPTMaxDisparity, double, 0.5, "Max disparity we will consider to try to do a zupt (i.e. if above this, don't do zupt)."); + RTABMAP_PARAM(OdomOpenVINS, ZUPTOnlyAtBeginning, bool, false, "If we should only use the zupt at the very beginning static initialization phase."); - RTABMAP_PARAM(OdomOpenVINS, AccelerometerNoiseDensity, double, 0.01, "[m/s^2/sqrt(Hz)] (accel \"white noise\")"); - RTABMAP_PARAM(OdomOpenVINS, AccelerometerRandomWalk, double, 0.001, "[m/s^3/sqrt(Hz)] (accel bias diffusion)"); - RTABMAP_PARAM(OdomOpenVINS, GyroscopeNoiseDensity, double, 0.001, "[rad/s/sqrt(Hz)] (gyro \"white noise\")"); - RTABMAP_PARAM(OdomOpenVINS, GyroscopeRandomWalk, double, 0.0001, "[rad/s^2/sqrt(Hz)] (gyro bias diffusion)"); - RTABMAP_PARAM(OdomOpenVINS, UpMSCKFSigmaPx, double, 1.0, "Pixel noise for MSCKF features"); - RTABMAP_PARAM(OdomOpenVINS, UpMSCKFChi2Multiplier, double, 1.0, "Chi2 multiplier for MSCKF features"); - RTABMAP_PARAM(OdomOpenVINS, UpSLAMSigmaPx, double, 1.0, "Pixel noise for SLAM features"); - RTABMAP_PARAM(OdomOpenVINS, UpSLAMChi2Multiplier, double, 1.0, "Chi2 multiplier for SLAM features"); + RTABMAP_PARAM(OdomOpenVINS, AccelerometerNoiseDensity, double, 0.01, "[m/s^2/sqrt(Hz)] (accel \"white noise\")."); + RTABMAP_PARAM(OdomOpenVINS, AccelerometerRandomWalk, double, 0.001, "[m/s^3/sqrt(Hz)] (accel bias diffusion)."); + RTABMAP_PARAM(OdomOpenVINS, GyroscopeNoiseDensity, double, 0.001, "[rad/s/sqrt(Hz)] (gyro \"white noise\")."); + RTABMAP_PARAM(OdomOpenVINS, GyroscopeRandomWalk, double, 0.0001, "[rad/s^2/sqrt(Hz)] (gyro bias diffusion)."); + RTABMAP_PARAM(OdomOpenVINS, UpMSCKFSigmaPx, double, 1.0, "Pixel noise for MSCKF features."); + RTABMAP_PARAM(OdomOpenVINS, UpMSCKFChi2Multiplier, double, 1.0, "Chi2 multiplier for MSCKF features."); + RTABMAP_PARAM(OdomOpenVINS, UpSLAMSigmaPx, double, 1.0, "Pixel noise for SLAM features."); + RTABMAP_PARAM(OdomOpenVINS, UpSLAMChi2Multiplier, double, 1.0, "Chi2 multiplier for SLAM features."); // Odometry Open3D RTABMAP_PARAM(OdomOpen3D, MaxDepth, float, 3.0, "Maximum depth."); diff --git a/corelib/src/CMakeLists.txt b/corelib/src/CMakeLists.txt index 8f82257b..e6bf74c9 100644 --- a/corelib/src/CMakeLists.txt +++ b/corelib/src/CMakeLists.txt @@ -781,16 +781,16 @@ IF(vins_FOUND) ) ENDIF(vins_FOUND) -IF(ov_msckf_FOUND) +IF(OpenVINS_FOUND) SET(INCLUDE_DIRS - ${ov_msckf_INCLUDE_DIRS} + ${OpenVINS_INCLUDE_DIRS} ${INCLUDE_DIRS} ) SET(LIBRARIES - ${ov_msckf_LIBRARIES} + ${OpenVINS_LIBRARIES} ${LIBRARIES} ) -ENDIF(ov_msckf_FOUND) +ENDIF(OpenVINS_FOUND) IF(ORB_SLAM_FOUND) SET(INCLUDE_DIRS diff --git a/corelib/src/odometry/OdometryOpenVINS.cpp b/corelib/src/odometry/OdometryOpenVINS.cpp index da81f7ee..e9539688 100644 --- a/corelib/src/odometry/OdometryOpenVINS.cpp +++ b/corelib/src/odometry/OdometryOpenVINS.cpp @@ -152,6 +152,85 @@ OdometryOpenVINS::OdometryOpenVINS(const ParametersMap & parameters) : params_->init_options.sigma_wb = params_->imu_noises.sigma_wb; params_->init_options.sigma_pix = params_->slam_options.sigma_pix; params_->init_options.gravity_mag = params_->gravity_mag; + + if(parameters.find(Parameters::kOdomOpenVINSConfigPath()) != parameters.end()) + { + // Load the config: will override all parameters above! + std::string configPath = parameters.at(Parameters::kOdomOpenVINSConfigPath()); + if(!configPath.empty()) + { + if(UFile::exists(configPath)) + { + UWARN("OpenVINS config file is provided (%s=\"%s\"), reading it. The parameters from the config file will overwrite OdomOpenVINS/*** parameters.", + Parameters::kOdomOpenVINSConfigPath().c_str(), configPath.c_str()); + auto parser = std::make_shared(configPath); + + // The sequence of loading is based on VioManagerOptions::print_and_load() + // We removed all parts about intrinsics/extrinsics, which will be loaded later + // when we receive the data (which should already include intrinsics and extrinsics). + + params_->state_options.print(parser); + + params_->init_options.print_and_load_initializer(parser); + params_->init_options.print_and_load_noise(parser); + parser->parse_config("gravity_mag", params_->init_options.gravity_mag); + parser->parse_config("max_cameras", params_->init_options.num_cameras); + parser->parse_config("use_stereo", params_->init_options.use_stereo); + parser->parse_config("downsample_cameras", params_->init_options.downsample_cameras); + + parser->parse_config("dt_slam_delay", params_->dt_slam_delay); + parser->parse_config("try_zupt", params_->try_zupt); + parser->parse_config("zupt_max_velocity",params_-> zupt_max_velocity); + parser->parse_config("zupt_noise_multiplier", params_->zupt_noise_multiplier); + parser->parse_config("zupt_max_disparity", params_->zupt_max_disparity); + parser->parse_config("zupt_only_at_beginning", params_->zupt_only_at_beginning); + parser->parse_config("record_timing_information", params_->record_timing_information); + parser->parse_config("record_timing_filepath", params_->record_timing_filepath); + + params_->print_and_load_trackers(parser); + params_->print_and_load_noise(parser); + + if(params_->state_options.num_cameras > 2) + { + UFATAL("OpenVINS integration in RTAB-Map doesn't support more than 2 cameras (num_cameras=%d).", params_->state_options.num_cameras); + } + + parser->parse_config("gravity_mag", params_->gravity_mag); + parser->parse_config("use_mask", params_->use_mask); + params_->masks.clear(); + if (params_->use_mask) { + for (int i = 0; i < params_->state_options.num_cameras; i++) { + std::string mask_path; + std::string mask_node = "mask" + std::to_string(i); + parser->parse_config(mask_node, mask_path); + std::string total_mask_path = parser->get_config_folder() + mask_path; + if (!boost::filesystem::exists(total_mask_path)) { + PRINT_ERROR(RED "VioManager(): invalid mask path:\n" RESET); + PRINT_ERROR(RED "\t- mask%d - %s\n" RESET, i, total_mask_path.c_str()); + std::exit(EXIT_FAILURE); + } + params_->masks.emplace(i, cv::imread(total_mask_path, cv::IMREAD_GRAYSCALE)); + } + } + + if (!parser->successful()) { + UWARN("Not all expected OpenVINS parameters were read successfully " + "from \"%s\". Values from RTAB-Map's OpenOpenVINS/* parameters " + "will be used instead for the missing ones.", + configPath.c_str()); + } + else { + UINFO("OpenVINS config file(%s=\"%s\") read.", + Parameters::kOdomOpenVINSConfigPath().c_str(), configPath.c_str()); + } + } + else + { + UERROR("OpenVINS config file is provided (%s=\"%s\") but it doesn't exist!", + Parameters::kOdomOpenVINSConfigPath().c_str(), configPath.c_str()); + } + } + } #endif } diff --git a/guilib/include/rtabmap/gui/PreferencesDialog.h b/guilib/include/rtabmap/gui/PreferencesDialog.h index 65a105a2..5fef4cba 100644 --- a/guilib/include/rtabmap/gui/PreferencesDialog.h +++ b/guilib/include/rtabmap/gui/PreferencesDialog.h @@ -372,6 +372,7 @@ private Q_SLOTS: void changeOdometryORBSLAMVocabulary(); void changeOdometryOKVISConfigPath(); void changeOdometryVINSFusionConfigPath(); + void changeOdometryOpenVINSConfigPath(); void changeOdometryLIOSAMConfigPath(); void changeOdometryOpenVINSLeftMask(); void changeOdometryOpenVINSRightMask(); diff --git a/guilib/src/PreferencesDialog.cpp b/guilib/src/PreferencesDialog.cpp index e3da3c46..2ccdd09e 100644 --- a/guilib/src/PreferencesDialog.cpp +++ b/guilib/src/PreferencesDialog.cpp @@ -1617,6 +1617,8 @@ PreferencesDialog::PreferencesDialog(QWidget * parent) : connect(_ui->toolButton_OdomVinsFusionPath, SIGNAL(clicked()), this, SLOT(changeOdometryVINSFusionConfigPath())); // Odometry OpenVINS + _ui->lineEdit_openvinsConfigPath->setObjectName(Parameters::kOdomOpenVINSConfigPath().c_str()); + connect(_ui->toolButton_openvinsConfigPath, SIGNAL(clicked()), this, SLOT(changeOdometryOpenVINSConfigPath())); _ui->checkBox_OdomOpenVINSUseStereo->setObjectName(Parameters::kOdomOpenVINSUseStereo().c_str()); _ui->checkBox_OdomOpenVINSUseKLT->setObjectName(Parameters::kOdomOpenVINSUseKLT().c_str()); _ui->spinBox_OdomOpenVINSNumPts->setObjectName(Parameters::kOdomOpenVINSNumPts().c_str()); @@ -5792,6 +5794,23 @@ void PreferencesDialog::changeOdometryVINSFusionConfigPath() } } +void PreferencesDialog::changeOdometryOpenVINSConfigPath() +{ + QString path; + if(_ui->lineEdit_openvinsConfigPath->text().isEmpty()) + { + path = QFileDialog::getOpenFileName(this, tr("OpenVINS Config"), this->getWorkingDirectory(), tr("OpenVINS config (*.yaml)")); + } + else + { + path = QFileDialog::getOpenFileName(this, tr("OpenVINS Config"), _ui->lineEdit_openvinsConfigPath->text(), tr("OpenVINS config (*.yaml)")); + } + if(!path.isEmpty()) + { + _ui->lineEdit_openvinsConfigPath->setText(path); + } +} + void PreferencesDialog::changeOdometryLIOSAMConfigPath() { QString path; diff --git a/guilib/src/ui/preferencesDialog.ui b/guilib/src/ui/preferencesDialog.ui index d03332e4..64eff092 100644 --- a/guilib/src/ui/preferencesDialog.ui +++ b/guilib/src/ui/preferencesDialog.ui @@ -63,7 +63,7 @@ 0 - 0 + -648 684 5218 @@ -95,7 +95,7 @@ QFrame::Raised - 18 + 19 @@ -16763,7 +16763,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - 1 + 10 @@ -20388,7 +20388,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo OpenVINS - + @@ -20405,6 +20405,33 @@ With <0, the length is estimated once for each unique marker, then re-used fo + + + + + + ... + + + + + + + + + + Configuration file (*.yaml). Same format used than OpenVINS library. Note that any parameter from that config file will overwrite the same parameter below. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + @@ -20421,7 +20448,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - If we should process two cameras are being stereo or binocular. If binocular, we do monocular feature tracking on each image. + Stereo mode. If we should process two cameras are being stereo or binocular. If binocular, we do monocular feature tracking on each image. Ignored if provided input data is not stereo. true @@ -20441,7 +20468,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - If we should use KLT tracking, or descriptor matcher + KLT tracking. Uncheck to use descriptor matcher. true @@ -20467,7 +20494,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Number of points (per camera) we will extract and try to track + Number of points (per camera) we will extract and try to track. true @@ -20490,7 +20517,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Will check after doing KLT track and remove any features closer than this + Minimum pixel distance. Will check after doing KLT track and remove any features closer than this. true @@ -20510,7 +20537,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - If we should perform 1d triangulation instead of 3d + If we should perform 1d triangulation instead of 3d. true @@ -20530,7 +20557,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - If we should perform Levenberg-Marquardt refinement + If we should perform Levenberg-Marquardt refinement. true @@ -20553,7 +20580,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Max runs for Levenberg-Marquardt + Max runs for Levenberg-Marquardt. true @@ -20579,7 +20606,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Max baseline ratio to accept triangulated features + Max baseline ratio to accept triangulated features. true @@ -20605,7 +20632,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Max condition number of linear triangulation matrix accept triangulated features + Max condition number of linear triangulation matrix accept triangulated features. true @@ -20634,7 +20661,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - If first-estimate Jacobians should be used (enable for good consistency) + If first-estimate Jacobians should be used (enable for good consistency). true @@ -20672,7 +20699,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Numerical integration methods + Numerical integration methods. true @@ -20692,7 +20719,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - If the transform between camera and IMU should be optimized (R_ItoC, p_CinI) + If the transform between camera and IMU should be optimized (R_ItoC, p_CinI). true @@ -20712,7 +20739,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - If camera intrinsics should be optimized (focal, center, distortion) + If camera intrinsics should be optimized (focal, center, distortion). true @@ -20732,7 +20759,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - If timeoffset between camera and IMU should be optimized + If timeoffset between camera and IMU should be optimized. true @@ -20752,7 +20779,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - If imu intrinsics should be calibrated (rotation and skew-scale matrix) + If imu intrinsics should be calibrated (rotation and skew-scale matrix). true @@ -20772,7 +20799,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - If gyroscope gravity sensitivity (Tg) should be calibrated + If gyroscope gravity sensitivity (Tg) should be calibrated. true @@ -20795,7 +20822,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Max clone size of sliding window + Max clone size of sliding window. true @@ -20821,7 +20848,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Max number of estimated SLAM features + Max number of estimated SLAM features. true @@ -20926,7 +20953,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - What representation our features are in (msckf features) + What representation our features are in (msckf features). true @@ -20942,7 +20969,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo 4 - QComboBox::AdjustToContents + QComboBox::AdjustToContentsOnFirstShow @@ -20979,7 +21006,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - What representation our features are in (slam features) + What representation our features are in (slam features). true @@ -21008,7 +21035,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Delay before initializing (helps with stability from bad initialization...) + Delay before initializing (helps with stability from bad initialization...). true @@ -21034,7 +21061,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Magnitude of gravity in this location + Magnitude of gravity in this location. true @@ -21061,7 +21088,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Mask for left image + Mask for left image (stereo mode) or mono image (RGB-D mode). For RGB-D mode, to use depth as mask, enable that option under Visual Registration panel. true @@ -21088,7 +21115,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Mask for right image + Mask for right image. true @@ -21126,7 +21153,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Amount of time we will initialize over + Amount of time we will initialize over. true @@ -21155,7 +21182,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Variance threshold on our acceleration to be classified as moving + Variance threshold on our acceleration to be classified as moving. true @@ -21181,7 +21208,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Max disparity to consider the platform stationary (dependent on resolution) + Max disparity to consider the platform stationary (dependent on resolution). true @@ -21207,7 +21234,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - How many features to track during initialization (saves on computation) + How many features to track during initialization (saves on computation). true @@ -21227,7 +21254,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - If we should perform dynamic initialization + If we should perform dynamic initialization. true @@ -21247,7 +21274,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - If we should optimize and recover the calibration in our MLE + If we should optimize and recover the calibration in our MLE. true @@ -21270,7 +21297,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Max number of MLE iterations for dynamic initialization + Max number of MLE iterations for dynamic initialization. true @@ -21299,7 +21326,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Max time for MLE optimization + Max time for MLE optimization. true @@ -21322,7 +21349,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Max number of MLE threads for dynamic initialization + Max number of MLE threads for dynamic initialization. true @@ -21345,7 +21372,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Number of poses to use during initialization (max should be cam freq * window) + Number of poses to use during initialization (max should be cam freq * window). true @@ -21374,7 +21401,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Minimum degrees we need to rotate before we try to init (sum of norm) + Minimum degrees we need to rotate before we try to init (sum of norm). true @@ -21400,7 +21427,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Magnitude we will inflate initial covariance of orientation + Magnitude we will inflate initial covariance of orientation. true @@ -21426,7 +21453,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Magnitude we will inflate initial covariance of velocity + Magnitude we will inflate initial covariance of velocity. true @@ -21452,7 +21479,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Magnitude we will inflate initial covariance of gyroscope bias + Magnitude we will inflate initial covariance of gyroscope bias. true @@ -21478,7 +21505,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Magnitude we will inflate initial covariance of accelerometer bias + Magnitude we will inflate initial covariance of accelerometer bias. true @@ -21507,7 +21534,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Minimum reciprocal condition number acceptable for our covariance recovery + Minimum reciprocal condition number acceptable for our covariance recovery. true @@ -21536,7 +21563,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - If we should try to use zero velocity update + If we should try to use zero velocity update. true @@ -21565,7 +21592,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Chi2 multiplier for zero velocity + Chi2 multiplier for zero velocity. true @@ -21594,7 +21621,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Max velocity we will consider to try to do a zupt (i.e. if above this, don't do zupt) + Max velocity we will consider to try to do a zupt (i.e. if above this, don't do zupt). true @@ -21623,7 +21650,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Multiplier of our zupt measurement IMU noise matrix (default should be 1.0) + Multiplier of our zupt measurement IMU noise matrix (default should be 1.0). true @@ -21652,7 +21679,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Max disparity we will consider to try to do a zupt (i.e. if above this, don't do zupt) + Max disparity we will consider to try to do a zupt (i.e. if above this, don't do zupt). true @@ -21672,7 +21699,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - If we should only use the zupt at the very beginning static initialization phase + If we should only use the zupt at the very beginning static initialization phase. true @@ -21713,7 +21740,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Accel "white noise" + Accel "white noise". true @@ -21745,7 +21772,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Accel bias diffusion + Accel bias diffusion. true @@ -21777,7 +21804,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Gyro "white noise" + Gyro "white noise". true @@ -21809,7 +21836,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Gyro bias diffusion + Gyro bias diffusion. true @@ -21838,7 +21865,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Pixel noise for MSCKF features + Pixel noise for MSCKF features. true @@ -21867,7 +21894,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Chi2 multiplier for MSCKF features + Chi2 multiplier for MSCKF features. true @@ -21896,7 +21923,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Pixel noise for SLAM features + Pixel noise for SLAM features. true @@ -21925,7 +21952,7 @@ With <0, the length is estimated once for each unique marker, then re-used fo - Chi2 multiplier for SLAM features + Chi2 multiplier for SLAM features. true From a9dcc52d4c175bcaca23d61facc880f4cf33bb3f Mon Sep 17 00:00:00 2001 From: matlabbe Date: Tue, 16 Jun 2026 16:21:38 -0700 Subject: [PATCH 124/126] Reprocess: adding --stop_loop option (#1720) --- tools/Reprocess/main.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tools/Reprocess/main.cpp b/tools/Reprocess/main.cpp index 25ac9e9e..a810ed8c 100644 --- a/tools/Reprocess/main.cpp +++ b/tools/Reprocess/main.cpp @@ -82,6 +82,7 @@ void showUsage() " -stop # Last node to process.\n" " -start_s # Start from this map session ID.\n" " -stop_s # Last map session to process.\n" + " -stop_loop Stop after the first loop closure is detected.\n" " -a Append mode: if Mem/IncrementalMemory is true, RTAB-Map is initialized with the first input database,\n" " then next databases are reprocessed on top of the first one.\n" " -cam # Camera index to stream. Ignored if a database doesn't contain multi-camera data. Can also be multiple \n" @@ -271,6 +272,7 @@ int main(int argc, char * argv[]) int stopId = 0; int startMapId = 0; int stopMapId = -1; + bool stopOnLoopClosure = false; bool appendMode = false; std::vector cameraIndices; std::vector cameraLocalTransformOverrides; @@ -419,6 +421,10 @@ int main(int argc, char * argv[]) showUsage(); } } + else if(strcmp(argv[i], "-stop_loop") == 0 || strcmp(argv[i], "--stop_loop") == 0) + { + stopOnLoopClosure = true; + } else if (strcmp(argv[i], "-a") == 0 || strcmp(argv[i], "--a") == 0) { appendMode = true; @@ -1288,6 +1294,11 @@ int main(int argc, char * argv[]) ++loopIntra; } printf("[%f] Processed %d/%d nodes [id=%d map=%d graph=%d hyp=%d]... %dms %s on %d [%d]\n", data.stamp(), ++processed, totalIds, refId, refMapId, int(stats.poses().size()), int(uValue(stats.data(), Statistics::kLoopHighest_hypothesis_value())*100.0f), int(iterationTime.ticks() * 1000), stats.loopClosureId() > 0?"Loop":"Prox", loopId, loopMapId); + if(stopOnLoopClosure) + { + printf("First loop closure has been detected and --stop_loop option is enabled, stop processing...\n"); + break; + } } else if(landmarkId != 0) { From 1a3beebcbbd28edaf99b2b0959a80d97559471ed Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sat, 20 Jun 2026 18:06:55 -0700 Subject: [PATCH 125/126] CI Ubuntu 26.04 / ROS2 Lyrical and Rolling (#1716) * CI Ubuntu 26.04 / ROS2 Lyrical and Rolling * Added resolute docker * added skip keys * rolling * Disabling rolling * Updated ci config * Enabling docker build (no push) on PR * cancel pr jobs on recommit * Added resolute to docker ci matrix * OpenGV min cmake version * bump package.xml version * enabling rolling * refactored cmake ros ci jobs --- .devcontainer/resolute/Dockerfile | 25 +++++ .devcontainer/resolute/devcontainer.json | 30 ++++++ .github/workflows/cmake-linux.yml | 6 +- .github/workflows/cmake-ros.yml | 63 ++++-------- .github/workflows/docker.yml | 40 +++++++- CMakeLists.txt | 3 + docker/resolute/Dockerfile | 20 ++++ docker/resolute/deps/Dockerfile | 119 +++++++++++++++++++++++ package.xml | 2 +- 9 files changed, 255 insertions(+), 53 deletions(-) create mode 100644 .devcontainer/resolute/Dockerfile create mode 100644 .devcontainer/resolute/devcontainer.json create mode 100644 docker/resolute/Dockerfile create mode 100644 docker/resolute/deps/Dockerfile diff --git a/.devcontainer/resolute/Dockerfile b/.devcontainer/resolute/Dockerfile new file mode 100644 index 00000000..244d9ca4 --- /dev/null +++ b/.devcontainer/resolute/Dockerfile @@ -0,0 +1,25 @@ +FROM introlab3it/rtabmap:resolute-deps + +# For devcontainer +# remove ubuntu user +RUN touch /var/mail/ubuntu && chown ubuntu /var/mail/ubuntu && userdel -r ubuntu + +RUN apt-get update && apt-get install -y sudo && \ + apt-get clean && rm -rf /var/lib/apt/lists/ + +ARG USERNAME=vscode +ARG USER_UID=1000 +ARG USER_GID=1000 + +RUN set -ex && \ + groupadd --gid ${USER_GID} ${USERNAME} && \ + useradd --uid ${USER_UID} --gid ${USER_GID} -m ${USERNAME} && \ + usermod -a -G sudo ${USERNAME} && \ + echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/${USERNAME} && \ + chmod 0440 /etc/sudoers.d/${USERNAME} + +RUN mkdir -p /home/${USERNAME}/Documents/RTAB-Map && chown -R ${USERNAME} /home/${USERNAME} + +RUN echo "source /usr/share/bash-completion/completions/git" >> /home/${USERNAME}/.bashrc + + diff --git a/.devcontainer/resolute/devcontainer.json b/.devcontainer/resolute/devcontainer.json new file mode 100644 index 00000000..387a00c1 --- /dev/null +++ b/.devcontainer/resolute/devcontainer.json @@ -0,0 +1,30 @@ +{ + "build": { + "dockerfile": "Dockerfile" + }, + "customizations": { + "vscode": { + "extensions": ["ms-vscode.cpptools-themes", "ms-vscode.cmake-tools", "ms-vscode.cpptools-extension-pack"] + } + }, + "workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/rtabmap,type=bind", + "workspaceFolder": "/home/vscode/rtabmap", + //"mounts": ["source=${localEnv:HOME}/Documents/RTAB-Map,target=/home/vscode/Documents/RTAB-Map,type=bind,consistency=cached"], + "settings": { + "terminal.integrated.defaultProfile.linux": "bash" + }, + "remoteUser": "vscode", + "hostRequirements": { + "gpu": "optional" + }, + "runArgs": ["--privileged", + "--network=host", + "--gpus=all", + //"--runtime=nvidia", // uncommment this if rtabmap doesn't show up in nvidia-smi on the host computer + "--env=DISPLAY", + "--env=QT_X11_NO_MITSHM=1", + "--volume=/tmp/.X11-unix:/tmp/.X11-unix"], + "containerEnv": { + "NVIDIA_VISIBLE_DEVICES": "all" + } +} diff --git a/.github/workflows/cmake-linux.yml b/.github/workflows/cmake-linux.yml index 8c752db4..bf2b51fb 100644 --- a/.github/workflows/cmake-linux.yml +++ b/.github/workflows/cmake-linux.yml @@ -22,7 +22,7 @@ jobs: strategy: fail-fast: true matrix: - build_name: [ubuntu-22.04, ubuntu-24.04, ubuntu-24.04-with-opengv] + build_name: [ubuntu-22.04, ubuntu-24.04, ubuntu-24.04-with-opengv, ubuntu-26.04] include: - build_name: ubuntu-22.04 os: ubuntu-22.04 @@ -36,6 +36,10 @@ jobs: os: ubuntu-24.04 extra_deps: "libg2o-dev libceres-dev" extra_cmake_def: "-DWITH_CERES=ON -DWITH_PYTHON=ON -DBUILD_OPENGV=ON" + - build_name: ubuntu-26.04 + os: ubuntu-26.04 + extra_deps: "libg2o-dev libceres-dev" + extra_cmake_def: "-DWITH_CERES=ON -DWITH_PYTHON=ON -DBUILD_OPENGV=ON" steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/cmake-ros.yml b/.github/workflows/cmake-ros.yml index 5c6d704f..393608f1 100644 --- a/.github/workflows/cmake-ros.yml +++ b/.github/workflows/cmake-ros.yml @@ -22,62 +22,33 @@ jobs: # well on Windows or Mac. You can convert this to a matrix build if you need # cross-platform coverage. # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix - name: ${{ matrix.ros_distribution }}-${{ matrix.os }} - runs-on: ${{ matrix.os }} + name: ${{ matrix.ros_distribution }} + runs-on: ubuntu-latest strategy: fail-fast: false matrix: - ros_distribution: [ humble, jazzy, kilted, rolling ] + ros_distribution: [ humble, jazzy, kilted, lyrical, rolling] include: - ros_distribution: 'humble' - os: ubuntu-22.04 + skip_keys: "" - ros_distribution: 'jazzy' - os: ubuntu-24.04 + skip_keys: "" - ros_distribution: 'kilted' - os: ubuntu-24.04 + skip_keys: "" + - ros_distribution: 'lyrical' + skip_keys: "libpointmatcher" - ros_distribution: 'rolling' - os: ubuntu-24.04 - + skip_keys: "" + container: + image: osrf/ros:${{ matrix.ros_distribution }}-desktop-full steps: - - name: Setup ROS2 - # https://docs.ros.org/en/humble/Installation/Ubuntu-Install-Debs.html - run: | - sudo apt install software-properties-common - sudo add-apt-repository universe - sudo apt update && sudo apt install curl -y - export ROS_APT_SOURCE_VERSION=$(curl -s https://api.github.com/repos/ros-infrastructure/ros-apt-source/releases/latest | grep -F "tag_name" | awk -F\" '{print $4}') - curl -L -o /tmp/ros2-apt-source.deb "https://github.com/ros-infrastructure/ros-apt-source/releases/download/${ROS_APT_SOURCE_VERSION}/ros2-apt-source_${ROS_APT_SOURCE_VERSION}.$(. /etc/os-release && echo ${UBUNTU_CODENAME:-${VERSION_CODENAME}})_all.deb" - sudo apt install /tmp/ros2-apt-source.deb - sudo apt update - + - uses: actions/checkout@v4 - uses: ros-tooling/setup-ros@v0.7 with: required-ros-distributions: ${{ matrix.ros_distribution }} - - - uses: actions/checkout@v4 - - - name: Point rosdep to latest Rolling on Noble until github runners support ubuntu 26.04 - if: matrix.ros_distribution == 'rolling' - run: | - echo "ROSDISTRO_INDEX_URL=https://raw.githubusercontent.com/ros/rosdistro/6527fa694360d609c1491528ed50437bd853af5b/index-v4.yaml" >> $GITHUB_ENV - - - name: Install dependencies - run: | - source /opt/ros/${{ matrix.ros_distribution }}/setup.bash - rosdep update - rosdep install --from-paths ${{github.workspace}} -y - - - name: Configure CMake - run: | - source /opt/ros/${{ matrix.ros_distribution }}/setup.bash - cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} - - - name: Build - run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} - - - name: Info - working-directory: ${{github.workspace}}/build/bin - run: | - source /opt/ros/${{ matrix.ros_distribution }}/setup.bash - ./rtabmap-console --version + - uses: ros-tooling/action-ros-ci@v0.4 + with: + package-name: rtabmap + target-ros2-distro: ${{ matrix.ros_distribution }} + rosdep-skip-keys: "${{ matrix.skip_keys }}" diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 5bcf160c..3fcfd2c1 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -4,6 +4,13 @@ on: push: branches: - 'master' + pull_request: + branches: + - '**' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: docker_deps: @@ -15,14 +22,15 @@ jobs: # $ 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 - # if: false + # Skipped on pull requests; built and pushed only on push to master. + if: github.event_name != 'pull_request' runs-on: ubuntu-latest strategy: fail-fast: false matrix: - docker_tag: [focal-deps, jammy-deps, noble-deps, noble-kilted-deps] + docker_tag: [focal-deps, jammy-deps, noble-deps, noble-kilted-deps, resolute-deps] include: - docker_tag: focal-deps docker_tags: | @@ -52,6 +60,13 @@ jobs: 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: - @@ -85,12 +100,14 @@ jobs: docker: needs: docker_deps + # Run even when docker_deps is skipped (it is, on pull requests). + if: ${{ !cancelled() && !failure() }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: - docker_tag: [bionic, focal, jammy, noble, noble-kilted, android23, android24, android26, android30] + docker_tag: [bionic, focal, jammy, noble, noble-kilted, resolute, android23, android24, android26, android30] include: - docker_tag: bionic docker_tags: | @@ -142,6 +159,16 @@ jobs: 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 @@ -190,6 +217,9 @@ jobs: 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 }} @@ -199,8 +229,8 @@ jobs: uses: docker/build-push-action@v6 with: context: . - push: true - platforms: ${{ matrix.docker_platforms }} + 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 }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 2f2206b9..3fd1801d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -901,6 +901,9 @@ IF(WITH_OPENGV OR okvis_FOUND) set(BUILD_TESTS OFF) set(CMAKE_BUILD_TYPE Release) set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) + # OpenGV's CMakeLists.txt declares cmake_minimum_required(VERSION 2.x), + # which CMake >= 4.0 (e.g. recent Ubuntu) rejects. Allow it to configure. + set(CMAKE_POLICY_VERSION_MINIMUM 3.5) # Eigen should have been already added by PCL, just populate the compatible variables IF(EIGEN_INCLUDE_DIRS) set(EIGEN_INCLUDE_DIRS "${EIGEN_INCLUDE_DIRS}" CACHE PATH "Eigen include dirs" FORCE) diff --git a/docker/resolute/Dockerfile b/docker/resolute/Dockerfile new file mode 100644 index 00000000..8e043f61 --- /dev/null +++ b/docker/resolute/Dockerfile @@ -0,0 +1,20 @@ +# Image: introlab3it/rtabmap:resolute + +FROM introlab3it/rtabmap:resolute-deps + +# Will be used to read/store databases on host +RUN mkdir -p /root/Documents/RTAB-Map && chmod 777 /root/Documents/RTAB-Map + +# Copy current source code +COPY . /root/rtabmap + +# Build RTAB-Map project +RUN source /ros_entrypoint.sh && \ + cd rtabmap/build && \ + cmake -DWITH_OPENGV=ON .. && \ + make -j4 && \ + make install && \ + cd ../.. && \ + rm -rf rtabmap && \ + ldconfig + diff --git a/docker/resolute/deps/Dockerfile b/docker/resolute/deps/Dockerfile new file mode 100644 index 00000000..67999aa8 --- /dev/null +++ b/docker/resolute/deps/Dockerfile @@ -0,0 +1,119 @@ + +# Image: introlab3it/rtabmap:resolute-deps + +FROM ubuntu:26.04 + +ARG TARGETPLATFORM +ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64} +RUN echo "I am building for $TARGETPLATFORM" + +ENV DEBIAN_FRONTEND=noninteractive + +# Install ROS2 +RUN apt update && \ + apt install software-properties-common -y && \ + add-apt-repository universe && \ + apt update && \ + apt install curl -y && \ + curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg && \ + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(. /etc/os-release && echo $UBUNTU_CODENAME) main" | tee /etc/apt/sources.list.d/ros2.list > /dev/null && \ + apt-get clean && rm -rf /var/lib/apt/lists/ + +# Install build dependencies +RUN apt-get update && \ + apt upgrade -y && \ + apt-get install -y \ + git \ + wget \ + libtbb-dev \ + libproj-dev \ + libpcl-dev \ + liboctomap-dev \ + libfreenect-dev \ + libceres-dev \ + ros-lyrical-ros-base \ + ros-dev-tools \ + ros-lyrical-cv-bridge \ + ros-lyrical-image-geometry \ + ros-lyrical-laser-geometry \ + ros-lyrical-pcl-conversions \ + ros-lyrical-rviz-common \ + ros-lyrical-rviz-rendering \ + ros-lyrical-rviz-default-plugins \ + ros-lyrical-pcl-ros \ + ros-lyrical-imu-filter-madgwick \ + ros-lyrical-image-transport \ + ros-lyrical-octomap-msgs \ + ros-lyrical-libg2o \ + ros-lyrical-gtsam \ + ros-lyrical-qt-gui-cpp \ + ros-lyrical-diagnostic-updater && \ + apt-get clean && rm -rf /var/lib/apt/lists/ + +WORKDIR /root/ + +# libfreenect2 +RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then echo "Installing libfreenect2..." && \ + apt-get update && apt-get install -y mesa-utils xserver-xorg-video-all libusb-1.0-0-dev libturbojpeg0-dev libglfw3-dev && \ + apt-get clean && rm -rf /var/lib/apt/lists/ && \ + git clone https://github.com/OpenKinect/libfreenect2 && \ + cd libfreenect2 && \ + mkdir build && \ + cd build && \ + cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_POLICY_VERSION_MINIMUM=3.5 .. && \ + make -j4 && \ + make install && \ + cd && \ + rm -r libfreenect2; fi + +# zed open capture +RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then echo "Installing zed-open-capture..." && \ + apt-get update && apt install -y libusb-1.0-0-dev libhidapi-libusb0 libhidapi-dev wget && \ + apt-get clean && rm -rf /var/lib/apt/lists/ && \ + git clone https://github.com/stereolabs/zed-open-capture.git && \ + cd zed-open-capture && \ + mkdir build && \ + cd build && \ + cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_POLICY_VERSION_MINIMUM=3.5 .. && \ + make -j4 && \ + make install && \ + cd && \ + rm -r zed-open-capture; fi + +# OpenCV with all modules (same version than distro version to avoid conflicts with cv_bridge ros package) +RUN git clone --branch 4.10.0 https://github.com/opencv/opencv.git && \ + git clone --branch 4.10.0 https://github.com/opencv/opencv_contrib.git && \ + cd opencv && \ + mkdir build && \ + cd build && \ + cmake -DCMAKE_BUILD_TYPE=Release -DWITH_TBB=ON -DWITH_ADE=OFF -DWITH_OPENMP=ON -DBUILD_opencv_python3=OFF -DBUILD_opencv_python_bindings_generator=OFF -DBUILD_opencv_python_tests=OFF -DBUILD_PERF_TESTS=OFF -DBUILD_TESTS=OFF -DOPENCV_ENABLE_NONFREE=ON -DOPENCV_EXTRA_MODULES_PATH=/root/opencv_contrib/modules .. && \ + make -j4 && \ + make install && \ + cd ../.. && \ + rm -rf opencv opencv_contrib + +RUN git clone https://github.com/laurentkneip/opengv.git && \ + cd opengv && \ + git checkout 91f4b19c73450833a40e463ad3648aae80b3a7f3 && \ + wget https://gist.githubusercontent.com/matlabbe/a412cf7c4627253874f81a00745a7fbb/raw/accc3acf465d1ffd0304a46b17741f62d4d354ef/opengv_disable_march_native.patch && \ + git apply opengv_disable_march_native.patch && \ + mkdir build && \ + cd build && \ + cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTS=OFF -DCMAKE_POLICY_VERSION_MINIMUM=3.5 .. && \ + make -j4 && \ + make install && \ + cd && \ + rm -r opengv + +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 +RUN chmod +x /ros_entrypoint.sh +ENTRYPOINT [ "/ros_entrypoint.sh" ] + +# ros2 seems not sourcing by default its multi-arch folders +ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/ros/lyrical/lib/x86_64-linux-gnu:/opt/ros/lyrical/lib/aarch64-linux-gnu + +# for jetson (https://github.com/introlab/rtabmap/issues/776) +ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/lib/aarch64-linux-gnu/tegra + diff --git a/package.xml b/package.xml index 4de2235c..4b178ba1 100644 --- a/package.xml +++ b/package.xml @@ -1,7 +1,7 @@ rtabmap - 0.23.5 + 0.23.7 RTAB-Map's standalone library. RTAB-Map is a RGB-D SLAM approach with real-time constraints. Mathieu Labbe Mathieu Labbe From bc724a21254a055cee7581d764e4513a0d6fa8ce Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 21 Jun 2026 12:44:03 -0700 Subject: [PATCH 126/126] iOS CI (#1722) * iOS CI * g2o hard-coded path * bump ios version * Fixed opencv 4.10.0 build on docker resolute --- .github/workflows/ios.yml | 114 +++++++++++++++++++ app/ios/RTABMapApp.xcodeproj/project.pbxproj | 4 +- app/ios/RTABMapApp/install_deps.sh | 12 ++ docker/resolute/deps/Dockerfile | 2 + 4 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/ios.yml diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml new file mode 100644 index 00000000..a035941d --- /dev/null +++ b/.github/workflows/ios.yml @@ -0,0 +1,114 @@ +name: iOS + +on: + push: + branches: + - master + paths: &ios_paths + - '.github/workflows/ios.yml' + - 'app/ios/**' + - 'app/android/jni/**' + - 'corelib/**' + - 'utilite/**' + - 'cmake_modules/**' + - 'CMakeLists.txt' + pull_request: + branches: + - '**' + paths: *ios_paths + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + # Pre-built iOS dependencies (content of app/ios/RTABMapApp/Libraries, generated by install_deps.sh). + # Bump this when the dependency set changes (must match the Xcode toolchain below). + DEPS_URL: https://github.com/introlab/rtabmap/releases/download/0.23.1/libraries-ios-xcode26.5.zip + XCODE_VERSION: '26.5' + +jobs: + build: + name: build-ios + # macos-26 (Tahoe) ships Xcode 26.x, matching the toolchain used to build the prebuilt libraries. + runs-on: macos-26 + + steps: + - uses: actions/checkout@v4 + + - name: Select Xcode ${{ env.XCODE_VERSION }} + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: ${{ env.XCODE_VERSION }} + + - name: Versions + run: | + xcodebuild -version + cmake --version || brew install cmake + + - name: Cache prebuilt dependencies archive + id: deps-cache + uses: actions/cache@v4 + with: + path: deps.zip + # Keyed on the archive URL (release tag + filename), so the cache is + # reused until DEPS_URL is bumped, regardless of other workflow edits. + key: ${{ runner.os }}-ios-deps-${{ env.DEPS_URL }} + + - name: Download prebuilt dependencies + if: steps.deps-cache.outputs.cache-hit != 'true' + run: curl -L "$DEPS_URL" -o deps.zip + + - name: Extract dependencies into Libraries + run: | + set -eux + mkdir -p app/ios/RTABMapApp/Libraries + rm -rf deps_extract && mkdir -p deps_extract + unzip -q deps.zip -d deps_extract + # The archive holds the *content* of the Libraries folder (include/ lib/ share/), + # but tolerate an extra top-level Libraries/ wrapper just in case. + if [ -d deps_extract/Libraries ]; then + SRC=deps_extract/Libraries + else + SRC=deps_extract + fi + cp -R "$SRC"/. app/ios/RTABMapApp/Libraries/ + test -d app/ios/RTABMapApp/Libraries/include + test -d app/ios/RTABMapApp/Libraries/lib + + - name: Build rtabmap core (third-party deps are skipped, already provided by the archive) + working-directory: app/ios/RTABMapApp + run: ./install_deps.sh + + - name: Build RTABMapApp + run: | + xcodebuild \ + -project app/ios/RTABMapApp.xcodeproj \ + -scheme RTABMapApp \ + -configuration Release \ + -sdk iphoneos \ + -destination 'generic/platform=iOS' \ + -derivedDataPath build \ + CODE_SIGNING_ALLOWED=NO \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGN_IDENTITY="" \ + DEVELOPMENT_TEAM="" \ + build + + - name: Package app (unsigned .ipa) + run: | + set -eux + APP_DIR="build/Build/Products/Release-iphoneos" + rm -rf Payload && mkdir Payload + cp -R "$APP_DIR/RTABMapApp.app" Payload/ + # Unsigned .ipa: not installable as-is, but ready for later (re)signing. + zip -q -r RTABMapApp-unsigned.ipa Payload + + - name: Upload app artifact + uses: actions/upload-artifact@v4 + with: + name: RTABMapApp-ios-unsigned + path: RTABMapApp-unsigned.ipa + compression-level: 0 + if-no-files-found: error + retention-days: ${{ github.event_name == 'pull_request' && 1 || 90 }} diff --git a/app/ios/RTABMapApp.xcodeproj/project.pbxproj b/app/ios/RTABMapApp.xcodeproj/project.pbxproj index 9074aa74..6fc3ba60 100644 --- a/app/ios/RTABMapApp.xcodeproj/project.pbxproj +++ b/app/ios/RTABMapApp.xcodeproj/project.pbxproj @@ -1065,7 +1065,7 @@ "$(PROJECT_DIR)/RTABMapApp/Libraries/share/OpenCV/3rdparty/lib", "$(PROJECT_DIR)/RTABMapApp/Libraries/lib/opencv4/3rdparty", ); - MARKETING_VERSION = 0.22.0; + MARKETING_VERSION = 0.23.7; OTHER_CFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = com.introlab.rtabmap; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1125,7 +1125,7 @@ "$(PROJECT_DIR)/RTABMapApp/Libraries/share/OpenCV/3rdparty/lib", "$(PROJECT_DIR)/RTABMapApp/Libraries/lib/opencv4/3rdparty", ); - MARKETING_VERSION = 0.22.0; + MARKETING_VERSION = 0.23.7; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = com.introlab.rtabmap; diff --git a/app/ios/RTABMapApp/install_deps.sh b/app/ios/RTABMapApp/install_deps.sh index 6eac2b07..15f67021 100755 --- a/app/ios/RTABMapApp/install_deps.sh +++ b/app/ios/RTABMapApp/install_deps.sh @@ -175,6 +175,18 @@ cd $pwd #rm -rf g2o fi +# g2o's installed CMake config hard-codes the absolute build-time prefix of its +# external dependencies (e.g. suitesparse) in INTERFACE_INCLUDE_DIRECTORIES. That +# path doesn't exist when the prebuilt Libraries archive is unpacked on another +# machine (CI), breaking find_package(g2o) with "includes non-existent path". +# Rewrite those absolute paths to be relocatable (relative to the config file). +# Run unconditionally (outside the build guard above) so it also fixes the prebuilt +# archive in CI, where the g2o build step is skipped. +find "$prefix/lib" -path '*/cmake/g2o/*.cmake' -print0 | while IFS= read -r -d '' f +do + sed -i '' -E 's#[^";]*/Libraries#${CMAKE_CURRENT_LIST_DIR}/../../..#g' "$f" +done + # VTK if [ ! -e $prefix/lib/vtk.framework ] then diff --git a/docker/resolute/deps/Dockerfile b/docker/resolute/deps/Dockerfile index 67999aa8..3e2986c2 100644 --- a/docker/resolute/deps/Dockerfile +++ b/docker/resolute/deps/Dockerfile @@ -84,6 +84,8 @@ RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then echo "Installing zed-open-cap RUN git clone --branch 4.10.0 https://github.com/opencv/opencv.git && \ git clone --branch 4.10.0 https://github.com/opencv/opencv_contrib.git && \ cd opencv && \ + # FFmpeg 7/8 compatibility (Ubuntu 26.04): avcodec_close / av_stream_get_side_data removed + git -c user.email=docker@build -c user.name=docker cherry-pick -x 90c444abd387ffa70b2e72a34922903a2f0f4f5a 443d0ae63fad6dfd8c485d609203db16c8bd0ec3 && \ mkdir build && \ cd build && \ cmake -DCMAKE_BUILD_TYPE=Release -DWITH_TBB=ON -DWITH_ADE=OFF -DWITH_OPENMP=ON -DBUILD_opencv_python3=OFF -DBUILD_opencv_python_bindings_generator=OFF -DBUILD_opencv_python_tests=OFF -DBUILD_PERF_TESTS=OFF -DBUILD_TESTS=OFF -DOPENCV_ENABLE_NONFREE=ON -DOPENCV_EXTRA_MODULES_PATH=/root/opencv_contrib/modules .. && \

CMake Linux Build Status
CMake Windows Build Status
CMake ROS Build Status
Docker Build Status +
+ CMake Linux Build Status
+
CMake Windows Build Status
+
CMake MaCOS Build Status
+
CMake ROS Build Status
+
Docker Build Status