From c55c05a97ea40e9252cb63550661be654b4a70c1 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Tue, 23 Jun 2026 19:13:18 -0700 Subject: [PATCH 01/51] CameraImages: support reading multicamera image and calibration files (#1721) * CameraImages: support reading multicamera image and calibration files * Added multi camera support for CameraStereoImages. Refactored single calib per frame option. * refactored * refactor * bump 0.23.8 --- CMakeLists.txt | 2 +- corelib/include/rtabmap/core/CameraModel.h | 6 +- .../include/rtabmap/core/StereoCameraModel.h | 4 +- .../rtabmap/core/camera/CameraImages.h | 53 +- .../rtabmap/core/camera/CameraStereoImages.h | 25 + corelib/src/CameraModel.cpp | 8 +- corelib/src/StereoCameraModel.cpp | 6 +- corelib/src/camera/CameraImages.cpp | 488 ++++++++--- corelib/src/camera/CameraStereoImages.cpp | 251 +++++- guilib/src/DatabaseViewer.cpp | 6 +- guilib/src/PreferencesDialog.cpp | 16 +- guilib/src/ui/preferencesDialog.ui | 803 +++++++++--------- package.xml | 2 +- 13 files changed, 1125 insertions(+), 545 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3fd1801d..47fac7b6 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 7) +SET(RTABMAP_PATCH_VERSION 8) SET(RTABMAP_VERSION ${RTABMAP_MAJOR_VERSION}.${RTABMAP_MINOR_VERSION}.${RTABMAP_PATCH_VERSION}) diff --git a/corelib/include/rtabmap/core/CameraModel.h b/corelib/include/rtabmap/core/CameraModel.h index b24b971b..7689c5ab 100644 --- a/corelib/include/rtabmap/core/CameraModel.h +++ b/corelib/include/rtabmap/core/CameraModel.h @@ -126,8 +126,10 @@ public: double verticalFOV() const; // in degrees bool isFisheye() const {return D_.cols == 6;} - bool load(const std::string & filePath); - bool load(const std::string & directory, const std::string & cameraName); + // Set initRectificationMaps=false to skip building the (potentially large) + // rectification maps when rectification won't be used (saves time and memory). + bool load(const std::string & filePath, bool initRectificationMaps = true); + bool load(const std::string & directory, const std::string & cameraName, bool initRectificationMaps = true); bool save(const std::string & directory) const; std::vector serialize() const; unsigned int deserialize(const std::vector& data); diff --git a/corelib/include/rtabmap/core/StereoCameraModel.h b/corelib/include/rtabmap/core/StereoCameraModel.h index 07cf2e77..c8d531a0 100644 --- a/corelib/include/rtabmap/core/StereoCameraModel.h +++ b/corelib/include/rtabmap/core/StereoCameraModel.h @@ -94,7 +94,9 @@ public: // backward compatibility void setImageSize(const cv::Size & size) {left_.setImageSize(size); right_.setImageSize(size);} - bool load(const std::string & directory, const std::string & cameraName, bool ignoreStereoTransform = true); + // Set initRectificationMaps=false to skip building the (potentially large) left/right + // rectification maps when rectification won't be used (saves time and memory). + bool load(const std::string & directory, const std::string & cameraName, bool ignoreStereoTransform = true, bool initRectificationMaps = true); bool save(const std::string & directory, bool ignoreStereoTransform = true) const; bool saveStereoTransform(const std::string & directory) const; std::vector serialize() const; diff --git a/corelib/include/rtabmap/core/camera/CameraImages.h b/corelib/include/rtabmap/core/camera/CameraImages.h index 2e234b9a..ce0a6362 100644 --- a/corelib/include/rtabmap/core/camera/CameraImages.h +++ b/corelib/include/rtabmap/core/camera/CameraImages.h @@ -76,6 +76,28 @@ public: { _hasConfigForEachFrame = value; } + bool isConfigForEachFrame() const {return _hasConfigForEachFrame;} + + // Enable multi-camera mode. Each image in the folder is expected to be the + // horizontal concatenation of N sub-camera images of a rig, sharing the same + // base name (timestamp or node id), e.g. "1780687370.031791.jpg" or "1.jpg". + // One calibration file per sub-camera must exist in the calibrationFolder + // passed to init(), named "_.yaml" with index starting at 0 + // (e.g. "1_0.yaml", "1_1.yaml", ...). The number of cameras is auto-detected. + // Each sub-image width is taken from the corresponding model's calibrated image + // size; if a model has no size, a uniform split (stackedWidth / N) is assumed. + // If setConfigForEachFrame() is enabled, one calibration set is loaded per frame + // using each image's base name as prefix. Otherwise a single calibration set is + // loaded and reused for all frames, using the cameraName passed to init() as + // prefix (it may differ from any image name), falling back to the first image's + // base name when cameraName is empty. + // Note that it is not recommended to use setConfigForEachFrame() if images have + // to be rectified, a rectification matrix would need to be re-initilaized for + // each frame. + void setMultiCameraCalibration(bool enabled) + { + _multiCameraCalib = enabled; + } void setScanPath( const std::string & dir, @@ -121,6 +143,22 @@ public: protected: virtual SensorData captureImage(SensorCaptureInfo * info = 0); + // File name (with extension, no directory) of the image returned by the last + // captureImage() call. Used by subclasses (e.g. stereo) to key per-frame + // calibration loaded on demand. Empty if no image was read. + const std::string & lastImageFileName() const {return _lastImageFileName;} + + // Calibration folder passed to init(), kept to load per-frame calibration on + // demand. Shared with subclasses (e.g. stereo) that load calibration themselves. + std::string _calibrationFolder; + + // Multi-camera mode state, shared with subclasses (e.g. stereo) that layer their + // own multi-camera handling over the base reader. Such subclasses temporarily set + // _multiCameraCalib to false while delegating to base init()/captureImage() so the + // base returns the raw stacked image instead of doing its own split, then restore it. + bool _multiCameraCalib; + int _multiCameraCount; // number of sub-cameras detected in multi-camera mode + private: bool readPoses( std::list & outputPoses, @@ -129,6 +167,16 @@ private: int format, double maxTimeDiff) const; + // Load the sub-camera models of a multi-camera rig from calibration files named + // "_.yaml" (index 0.._multiCameraCount-1) in _calibrationFolder. + // Returns an empty vector (and logs an error) if any model is missing or invalid. + std::vector loadMultiCameraModels(const std::string & baseName) const; + + // Load the single-camera model for one frame from a per-frame config file (RTAB-Map + // calibration or 3DScannerApp format). Returns an invalid model (and logs an error) + // on failure. + CameraModel loadConfigModel(const std::string & filePath); + private: std::string _path; int _startAt; @@ -144,6 +192,7 @@ private: int _framesPublished; UDirectory * _dir; std::string _lastFileName; + std::string _lastImageFileName; // file name of the image returned by the last captureImage() int _countScan; UDirectory * _scanDir; @@ -173,7 +222,9 @@ private: std::list covariances_; std::list groundTruth_; CameraModel _model; - std::list _models; + std::list _modelFileNames; // per-frame single-camera config file paths (config-for-each-frame) + bool _configLocalTransformWarned; // warn only once when a per-frame config has no local_transform + std::vector _multiModels; // sub-camera models, shared by all frames (multi-camera mode); empty when loaded per-frame UTimer _captureTimer; double _captureDelay; diff --git a/corelib/include/rtabmap/core/camera/CameraStereoImages.h b/corelib/include/rtabmap/core/camera/CameraStereoImages.h index 6571d8b6..35ac637c 100644 --- a/corelib/include/rtabmap/core/camera/CameraStereoImages.h +++ b/corelib/include/rtabmap/core/camera/CameraStereoImages.h @@ -58,6 +58,24 @@ public: void setRightGrayScale(bool enabled = true) {rightGrayScale_ = enabled;} + // Enable multi-camera stereo mode. Each left/right image in the folders is + // expected to be the horizontal concatenation of N sub-camera images of a + // stereo rig, all sharing the same base name (timestamp or node id), e.g. + // "1779318290.502227.jpg". Two calibration files per sub-camera must exist in + // the calibrationFolder passed to init(), named "__left.yaml" + // and "__right.yaml" with index starting at 0. The number of + // cameras is auto-detected. Sub-images are split using a uniform width + // (stackedWidth / N). + // If setConfigForEachFrame() is enabled, one calibration set is loaded per frame + // using each image's base name as prefix. Otherwise a single calibration set is + // loaded and reused for all frames, using the cameraName passed to init() as + // prefix (it may differ from any image name), falling back to the first image's + // base name when cameraName is empty. + // Note that it is not recommended to use setConfigForEachFrame() if images have + // to be rectified, a rectification matrix would need to be re-initilaized for each + // frame. + void setMultiCameraCalibration(bool enabled) {CameraImages::setMultiCameraCalibration(enabled);} + virtual bool init(const std::string & calibrationFolder = ".", const std::string & cameraName = ""); virtual bool isCalibrated() const; virtual std::string getSerial() const; @@ -68,10 +86,17 @@ public: protected: virtual SensorData captureImage(SensorCaptureInfo * info = 0); +private: + // Load the sub-camera stereo models of a frame from calibration files named + // "__{left,right}.yaml" (index 0.._multiCameraCount-1) in + // _calibrationFolder. Returns an empty vector (and logs an error) on failure. + std::vector loadStereoCameraModels(const std::string & baseName, bool rectify) const; + private: CameraImages * camera2_; StereoCameraModel stereoModel_; bool rightGrayScale_; + std::vector multiStereoModels_; // sub-camera stereo models shared by all frames (multi-camera mode); empty when loaded per-frame }; diff --git a/corelib/src/CameraModel.cpp b/corelib/src/CameraModel.cpp index 04b77329..de1e2c06 100644 --- a/corelib/src/CameraModel.cpp +++ b/corelib/src/CameraModel.cpp @@ -212,7 +212,7 @@ void CameraModel::setImageSize(const cv::Size & size) } } -bool CameraModel::load(const std::string & filePath) +bool CameraModel::load(const std::string & filePath, bool initRectificationMaps) { K_ = cv::Mat(); D_ = cv::Mat(); @@ -377,7 +377,7 @@ bool CameraModel::load(const std::string & filePath) fs.release(); - if(isValidForRectification()) + if(initRectificationMaps && isValidForRectification()) { initRectificationMap(); } @@ -396,9 +396,9 @@ bool CameraModel::load(const std::string & filePath) return false; } -bool CameraModel::load(const std::string & directory, const std::string & cameraName) +bool CameraModel::load(const std::string & directory, const std::string & cameraName, bool initRectificationMaps) { - return load(directory+"/"+cameraName+".yaml"); + return load(directory+"/"+cameraName+".yaml", initRectificationMaps); } bool CameraModel::save(const std::string & directory) const diff --git a/corelib/src/StereoCameraModel.cpp b/corelib/src/StereoCameraModel.cpp index 421d3f4c..9c338174 100644 --- a/corelib/src/StereoCameraModel.cpp +++ b/corelib/src/StereoCameraModel.cpp @@ -220,11 +220,11 @@ void StereoCameraModel::updateStereoRectification() right_ = CameraModel(right_.name(), right_.imageSize(), right_.K_raw(), right_.D_raw(), R2, P2, right_.localTransform()); } -bool StereoCameraModel::load(const std::string & directory, const std::string & cameraName, bool ignoreStereoTransform) +bool StereoCameraModel::load(const std::string & directory, const std::string & cameraName, bool ignoreStereoTransform, bool initRectificationMaps) { name_ = cameraName; - bool leftLoaded = left_.load(directory, cameraName+"_"+getLeftSuffix()); - bool rightLoaded = right_.load(directory, cameraName+"_"+getRightSuffix()); + bool leftLoaded = left_.load(directory, cameraName+"_"+getLeftSuffix(), initRectificationMaps); + bool rightLoaded = right_.load(directory, cameraName+"_"+getRightSuffix(), initRectificationMaps); if(leftLoaded && rightLoaded) { if(ignoreStereoTransform) diff --git a/corelib/src/camera/CameraImages.cpp b/corelib/src/camera/CameraImages.cpp index 97674121..be6383a2 100644 --- a/corelib/src/camera/CameraImages.cpp +++ b/corelib/src/camera/CameraImages.cpp @@ -41,6 +41,8 @@ namespace rtabmap { CameraImages::CameraImages() : + _multiCameraCalib(false), + _multiCameraCount(0), _startAt(0), _maxFrames(0), _refreshDir(false), @@ -71,6 +73,8 @@ CameraImages::CameraImages(const std::string & path, float imageRate, const Transform & localTransform) : Camera(imageRate, localTransform), + _multiCameraCalib(false), + _multiCameraCount(0), _path(path), _startAt(0), _maxFrames(0), @@ -111,14 +115,22 @@ CameraImages::~CameraImages() bool CameraImages::init(const std::string & calibrationFolder, const std::string & cameraName) { _lastFileName.clear(); + _lastImageFileName.clear(); _lastScanFileName.clear(); _count = 0; _countScan = 0; _captureDelay = 0.0; _framesPublished=0; _model = cameraModel(); - _models.clear(); + _modelFileNames.clear(); + _configLocalTransformWarned = false; + _multiModels.clear(); + _calibrationFolder.clear(); + _multiCameraCount = 0; covariances_.clear(); + _stamps.clear(); + odometry_.clear(); + groundTruth_.clear(); UDEBUG(""); if(_dir) @@ -199,107 +211,117 @@ bool CameraImages::init(const std::string & calibrationFolder, const std::string return false; } + bool success = _dir|| _scanDir; + if(_dir) { - // look for calibration files - UINFO("calibration folder=%s name=%s", calibrationFolder.c_str(), cameraName.c_str()); - if(!calibrationFolder.empty() && !cameraName.empty()) + if(_multiCameraCalib) { - if(!_model.load(calibrationFolder, cameraName)) + // Multi-camera mode: each image is a horizontal stack of N sub-camera + // images. Load one CameraModel per sub-camera from calibration files + // named "_.yaml" (index starting at 0). In config-for-each-frame + // mode the prefix is each image's base name (one calibration set per frame); + // otherwise a single calibration set is shared by all frames, using cameraName + // as the prefix when provided (it may differ from any image name), falling back + // to the first image's base name. + if(calibrationFolder.empty()) { - UWARN("Missing calibration files for camera \"%s\" in \"%s\" folder, you should calibrate the camera!", - cameraName.c_str(), calibrationFolder.c_str()); + UERROR("Multi-camera calibration is enabled but no calibration folder was provided."); + return false; + } + const std::list & imageFiles = _dir->getFileNames(); + std::string firstBase = imageFiles.front().substr(0, imageFiles.front().find_last_of('.')); + + // In config-for-each-frame mode the prefix is each image's base name. Otherwise + // the shared prefix is cameraName when provided. cameraName may point to a specific + // sub-camera calibration "_" (e.g. when a single calibration file is + // selected); in that case strip the trailing "_" to recover the rig prefix + // shared by all sub-cameras. Fall back to the first image's base name if empty. + std::string sharedBase = cameraName; + if(!sharedBase.empty() && !UFile::exists(calibrationFolder + "/" + sharedBase + "_0.yaml")) + { + std::size_t us = sharedBase.find_last_of('_'); + if(us != std::string::npos && us+1 < sharedBase.size() && + sharedBase.find_first_not_of("0123456789", us+1) == std::string::npos && + UFile::exists(calibrationFolder + "/" + sharedBase.substr(0, us) + "_0.yaml")) + { + sharedBase = sharedBase.substr(0, us); + } + } + if(sharedBase.empty()) + { + sharedBase = firstBase; + } + + // auto-detect the number of cameras + int numCameras = 0; + std::string detectBase = _hasConfigForEachFrame ? firstBase : sharedBase; + while(UFile::exists(calibrationFolder + "/" + detectBase + "_" + uNumber2Str(numCameras) + ".yaml")) + { + ++numCameras; + } + + if(numCameras == 0) + { + UERROR("Multi-camera calibration is enabled but no calibration file matching " + "\"%s/%s_.yaml\" was found.", + calibrationFolder.c_str(), detectBase.c_str()); + return false; + } + + UINFO("Multi-camera mode: %d sub-cameras detected from \"%s\" (%s).", numCameras, calibrationFolder.c_str(), + _hasConfigForEachFrame?"one calibration per frame, loaded on demand": + uFormat("calibration \"%s_\" reused for all frames", sharedBase.c_str()).c_str()); + _calibrationFolder = calibrationFolder; + _multiCameraCount = numCameras; + if(_hasConfigForEachFrame) + { + // Validate the first frame now; the per-frame sub-camera models are loaded + // on demand in captureImage() (keyed by each image's base name) so we don't + // keep every frame's models - and their rectification maps - in memory. + if(loadMultiCameraModels(firstBase).empty()) + { + return false; + } } else { - UINFO("Camera parameters: fx=%f fy=%f cx=%f cy=%f", - _model.fx(), - _model.fy(), - _model.cx(), - _model.cy()); - - cv::FileStorage fs(calibrationFolder+"/"+cameraName+".yaml", 0); - cv::FileNode poseNode = fs["local_transform"]; - if(!poseNode.isNone()) + // A single calibration set is shared by all frames: load it once. + std::vector models = loadMultiCameraModels(sharedBase); + if(models.empty()) { - UWARN("Using local transform from calibration file (%s) instead of the parameter one (%s).", - _model.localTransform().prettyPrint().c_str(), - this->getLocalTransform().prettyPrint().c_str()); - this->setLocalTransform(_model.localTransform()); + return false; } + _multiModels = models; } } - _model.setName(cameraName); - - _model.setLocalTransform(this->getLocalTransform()); - if(_rectifyImages && !_model.isValidForRectification()) - { - UERROR("Parameter \"rectifyImages\" is set, but no camera model is loaded or valid."); - return false; - } - } - - bool success = _dir|| _scanDir; - _stamps.clear(); - odometry_.clear(); - groundTruth_.clear(); - if(success) - { - if(_dir && _hasConfigForEachFrame) + else if(_hasConfigForEachFrame) { + // Per-frame calibration: one config file per image, taken from the + // calibration folder (same convention as the single/multi-camera cases). + // If the config files are in the same folder than the images, just set the + // calibration folder to the images folder. #if CV_MAJOR_VERSION < 3 || (CV_MAJOR_VERSION == 3 && CV_MAJOR_VERSION < 2) - UDirectory dirJson(_path, "yaml xml"); + UDirectory dirJson(calibrationFolder, "yaml xml"); #else - UDirectory dirJson(_path, "yaml xml json"); + UDirectory dirJson(calibrationFolder, "yaml xml json"); #endif if(dirJson.getFileNames().size() == _dir->getFileNames().size()) { - bool modelsWarned = false; - bool localTWarned = false; + // The per-frame models are loaded on demand in captureImage() (see + // loadConfigModel) so we don't keep every frame's model - and its + // rectification map - in memory. Only the stamps and poses of the + // 3DScannerApp format are read eagerly here (needed for synchronization). for(std::list::const_iterator iter=dirJson.getFileNames().begin(); iter!=dirJson.getFileNames().end() && success; ++iter) { - std::string filePath = _path+"/"+*iter; + std::string filePath = calibrationFolder+"/"+*iter; cv::FileStorage fs(filePath, 0); cv::FileNode poseNode = fs["cameraPoseARFrame"]; // Check if it is 3DScannerApp(iOS) format - if(poseNode.isNone()) - { - cv::FileNode n = fs["local_transform"]; - bool hasLocalTransform = !n.isNone(); - - fs.release(); - if(_model.isValidForProjection() && !modelsWarned) - { - UWARN("Camera model loaded for each frame is overridden by " - "general calibration file provided. Remove general calibration " - "file to use camera model of each frame. This warning will " - "be shown only one time."); - modelsWarned = true; - } - else - { - CameraModel model; - model.load(filePath); - - if(!hasLocalTransform) - { - if(!localTWarned) - { - UWARN("Loaded calibration file doesn't have local_transform field, " - "the global local_transform parameter is used by default (%s).", - this->getLocalTransform().prettyPrint().c_str()); - localTWarned = true; - } - model.setLocalTransform(this->getLocalTransform()); - } - - _models.push_back(model); - } - } - else + if(!poseNode.isNone()) { cv::FileNode timeNode = fs["time"]; cv::FileNode intrinsicsNode = fs["intrinsics"]; - if(poseNode.isNone() || poseNode.size() != 16) + if(poseNode.size() != 16) { UERROR("Failed reading \"cameraPoseARFrame\" parameter, it should have 16 values (file=%s)", filePath.c_str()); success = false; @@ -320,23 +342,6 @@ bool CameraImages::init(const std::string & calibrationFolder, const std::string else { _stamps.push_back((double)timeNode); - if(_model.isValidForProjection() && !modelsWarned) - { - UWARN("Camera model loaded for each frame is overridden by " - "general calibration file provided. Remove general calibration " - "file to use camera model of each frame. This warning will " - "be shown only one time."); - modelsWarned = true; - } - else - { - _models.push_back(CameraModel( - (double)intrinsicsNode[0], //fx - (double)intrinsicsNode[4], //fy - (double)intrinsicsNode[2], //cx - (double)intrinsicsNode[5], //cy - CameraModel::opticalRotation())); - } // we need to rotate from opengl world to rtabmap world Transform pose( (float)poseNode[0], (float)poseNode[1], (float)poseNode[2], (float)poseNode[3], @@ -346,12 +351,18 @@ bool CameraImages::init(const std::string & calibrationFolder, const std::string odometry_.push_back(pose); } } + _modelFileNames.push_back(filePath); + } + // Validate the first frame's calibration now to fail early. + if(success && !_modelFileNames.empty() && !loadConfigModel(_modelFileNames.front()).isValidForProjection()) + { + success = false; } if(!success) { odometry_.clear(); _stamps.clear(); - _models.clear(); + _modelFileNames.clear(); } } else @@ -365,12 +376,60 @@ bool CameraImages::init(const std::string & calibrationFolder, const std::string "of images (%d) in this directory \"%s\".%s", (int)dirJson.getFileNames().size(), (int)_dir->getFileNames().size(), - _path.c_str(), + calibrationFolder.c_str(), opencv32warn.c_str()); success = false; } } + else + { + // Config-for-each-frame is disabled: load a single general calibration + // model (the per-frame branch above handles the config-for-each-frame case). + // look for calibration files + UINFO("calibration folder=%s name=%s", calibrationFolder.c_str(), cameraName.c_str()); + if(!calibrationFolder.empty() && !cameraName.empty()) + { + if(!_model.load(calibrationFolder, cameraName, _rectifyImages)) + { + UWARN("Missing calibration files for camera \"%s\" in \"%s\" folder, you should calibrate the camera!", + cameraName.c_str(), calibrationFolder.c_str()); + } + else + { + UINFO("Camera parameters: fx=%f fy=%f cx=%f cy=%f", + _model.fx(), + _model.fy(), + _model.cx(), + _model.cy()); + cv::FileStorage fs(calibrationFolder+"/"+cameraName+".yaml", 0); + cv::FileNode poseNode = fs["local_transform"]; + if(!poseNode.isNone()) + { + UWARN("Using local transform from calibration file (%s) instead of the parameter one (%s).", + _model.localTransform().prettyPrint().c_str(), + this->getLocalTransform().prettyPrint().c_str()); + this->setLocalTransform(_model.localTransform()); + } + } + + // Only validate rectification when a general calibration was requested. + // Without it (e.g. images that are already rectified) the single model is + // expected to be empty and must not fail init here. + if(_rectifyImages && !_model.isValidForRectification()) + { + UERROR("Parameter \"rectifyImages\" is set, but no camera model is loaded or valid."); + return false; + } + } + _model.setName(cameraName); + + _model.setLocalTransform(this->getLocalTransform()); + } + } + + if(success) + { if(_stamps.empty()) { if(_filenamesAreTimestamps) @@ -653,10 +712,97 @@ bool CameraImages::readPoses( bool CameraImages::isCalibrated() const { - return (_dir && (_model.isValidForProjection() || (_models.size() && _models.front().isValidForProjection()))) || + return (_dir && (_model.isValidForProjection() || + _modelFileNames.size() || // per-frame single-camera models loaded on demand (validated in init) + (!_multiModels.empty() && _multiModels.front().isValidForProjection()) || + (_multiCameraCalib && _hasConfigForEachFrame && _multiCameraCount > 0))) || // per-frame multi-camera models loaded on demand _scanDir; } +std::vector CameraImages::loadMultiCameraModels(const std::string & baseName) const +{ + std::vector models(_multiCameraCount); + for(int i=0; i<_multiCameraCount; ++i) + { + std::string name = baseName + "_" + uNumber2Str(i); + if(!models[i].load(_calibrationFolder, name, _rectifyImages) || !models[i].isValidForProjection()) + { + UERROR("Failed to load a valid calibration \"%s/%s.yaml\" for multi-camera frame base \"%s\".", + _calibrationFolder.c_str(), name.c_str(), baseName.c_str()); + return std::vector(); + } + if(models[i].localTransform().isNull()) + { + // In multi-camera mode the rig extrinsics are required: each + // sub-camera calibration must provide a "local_transform". + UERROR("Calibration \"%s/%s.yaml\" has no \"local_transform\"; it is " + "required in multi-camera mode (rig extrinsics).", + _calibrationFolder.c_str(), name.c_str()); + return std::vector(); + } + if(_rectifyImages && !models[i].isValidForRectification()) + { + UERROR("Parameter \"rectifyImages\" is set, but calibration \"%s/%s.yaml\" is not valid for rectification.", + _calibrationFolder.c_str(), name.c_str()); + return std::vector(); + } + } + return models; +} + +CameraModel CameraImages::loadConfigModel(const std::string & filePath) +{ + cv::FileStorage fs(filePath, 0); + cv::FileNode poseNode = fs["cameraPoseARFrame"]; // Check if it is 3DScannerApp(iOS) format + CameraModel model; + if(poseNode.isNone()) + { + // RTAB-Map calibration format + bool hasLocalTransform = !fs["local_transform"].isNone(); + fs.release(); + model.load(filePath, _rectifyImages); + if(!hasLocalTransform) + { + if(!_configLocalTransformWarned) + { + UWARN("Loaded calibration file doesn't have local_transform field, " + "the global local_transform parameter is used by default (%s).", + this->getLocalTransform().prettyPrint().c_str()); + _configLocalTransformWarned = true; + } + model.setLocalTransform(this->getLocalTransform()); + } + } + else + { + // 3DScannerApp(iOS) format + cv::FileNode intrinsicsNode = fs["intrinsics"]; + if(intrinsicsNode.isNone() || intrinsicsNode.size()!=9) + { + UERROR("Failed reading \"intrinsics\" parameter (file=%s)", filePath.c_str()); + return CameraModel(); + } + model = CameraModel( + (double)intrinsicsNode[0], //fx + (double)intrinsicsNode[4], //fy + (double)intrinsicsNode[2], //cx + (double)intrinsicsNode[5], //cy + CameraModel::opticalRotation()); + } + if(!model.isValidForProjection()) + { + UERROR("Camera model loaded from \"%s\" is not valid for projection.", filePath.c_str()); + return CameraModel(); + } + if(_rectifyImages && !model.isValidForRectification()) + { + UERROR("Parameter \"rectifyImages\" is set, but camera model loaded from \"%s\" is not valid for rectification.", + filePath.c_str()); + return CameraModel(); + } + return model; +} + std::string CameraImages::getSerial() const { return _model.name(); @@ -728,7 +874,13 @@ SensorData CameraImages::captureImage(SensorCaptureInfo * info) cv::Mat covariance; Transform groundTruthPose; cv::Mat depthFromScan; - CameraModel model = _model; + std::vector models; // one entry per camera (single entry in single-camera mode) + if(!_multiCameraCalib) + { + // Single-camera mode keeps exactly one model in the vector; in multi-camera + // mode the per-frame vector is taken from _multiModels below. + models.push_back(_model); + } UDEBUG(""); if(_dir || _scanDir) { @@ -784,7 +936,8 @@ SensorData CameraImages::captureImage(SensorCaptureInfo * info) { UERROR("groundTruth cannot be used when startAt < 0"); } - if(_models.size() && !model.isValidForProjection()) + if(_modelFileNames.size() || !_multiModels.empty() || + (_multiCameraCalib && _hasConfigForEachFrame)) { UERROR("models cannot be used when startAt < 0"); } @@ -826,12 +979,24 @@ SensorData CameraImages::captureImage(SensorCaptureInfo * info) groundTruthPose = groundTruth_.front(); groundTruth_.pop_front(); } - if(_models.size() && !model.isValidForProjection()) + if(_modelFileNames.size()) { - UASSERT_MSG(stampsSize==0 || stampsSize == _models.size(), - uFormat("Stamps=%ld models=%ld", _stamps.size(), _models.size()).c_str()); - model = _models.front(); - _models.pop_front(); + UASSERT_MSG(stampsSize==0 || stampsSize == _modelFileNames.size(), + uFormat("Stamps=%ld models=%ld", _stamps.size(), _modelFileNames.size()).c_str()); + // per-frame calibration loaded on demand (not kept in memory) + models.back() = loadConfigModel(_modelFileNames.front()); + _modelFileNames.pop_front(); + } + if(_multiCameraCalib && _hasConfigForEachFrame) + { + // Per-frame calibration loaded on demand (not kept in memory), + // keyed by the current image's base name. + models = loadMultiCameraModels(imageFileName.substr(0, imageFileName.find_last_of('.'))); + } + else if(!_multiModels.empty()) + { + // calibration is shared across all frames + models = _multiModels; } while(_count++ < _startAt) @@ -875,14 +1040,27 @@ SensorData CameraImages::captureImage(SensorCaptureInfo * info) groundTruthPose = groundTruth_.front(); groundTruth_.pop_front(); } - if(_models.size() && !model.isValidForProjection()) + if(_modelFileNames.size()) { - UASSERT_MSG(stampsSize==0 || stampsSize == _models.size(), - uFormat("Stamps=%ld models=%ld", _stamps.size(), _models.size()).c_str()); - model = _models.front(); - _models.pop_front(); + UASSERT_MSG(stampsSize==0 || stampsSize == _modelFileNames.size(), + uFormat("Stamps=%ld models=%ld", _stamps.size(), _modelFileNames.size()).c_str()); + // per-frame calibration loaded on demand (not kept in memory) + models.back() = loadConfigModel(_modelFileNames.front()); + _modelFileNames.pop_front(); + } + if(_multiCameraCalib && _hasConfigForEachFrame) + { + // Per-frame calibration loaded on demand (not kept in memory), + // keyed by the current image's base name. + models = loadMultiCameraModels(imageFileName.substr(0, imageFileName.find_last_of('.'))); + } + else if(!_multiModels.empty()) + { + // calibration is shared across all frames + models = _multiModels; } } + _lastImageFileName = imageFileName; // expose the current frame name to subclasses } } @@ -951,9 +1129,67 @@ SensorData CameraImages::captureImage(SensorCaptureInfo * info) } } - if(!img.empty() && model.isValidForRectification() && _rectifyImages) + if(!img.empty() && !models.empty()) { - img = model.rectifyImage(img); + // The image is a horizontal stack of N sub-images (N==1 in + // single-camera mode). Each sub-camera's width comes from its + // calibrated image size; if a model has no size (==0), assume a + // uniform split (stackedWidth / N) and set it. If requested, each + // sub-image is rectified independently with its model and written + // into a new stacked image of the same layout. + cv::Mat rectified; + int offset = 0; + bool rectifyAborted = false; + for(size_t i=0; i img.cols) + { + UERROR("Multi-camera: sum of sub-image widths (%d) exceeds the stacked " + "image width (%d) at camera %d.", offset+subWidth, img.cols, (int)i); + rectified = cv::Mat(); + rectifyAborted = true; + break; + } + if(_rectifyImages) + { + if(!models[i].isValidForRectification()) + { + // In multi-camera mode a valid calibration is expected for each + // sub-camera. In single-camera mode this is a passthrough (e.g. the + // base reader of a stereo/RGBD subclass that rectifies itself, or + // images that are already rectified): skip silently to keep the + // backward-compatible behavior. + if(_multiCameraCalib) + { + UERROR("Parameter \"rectifyImages\" is set, but camera model %d is not valid for rectification.", (int)i); + } + rectified = cv::Mat(); + rectifyAborted = true; + break; + } + if(rectified.empty()) + { + rectified = cv::Mat::zeros(img.rows, img.cols, img.type()); + } + cv::Rect roi(offset, 0, subWidth, img.rows); + models[i].rectifyImage(img(roi)).copyTo(rectified(roi)); + } + offset += subWidth; + } + if(!rectifyAborted && offset != img.cols) + { + UWARN("Multi-camera: sum of sub-image widths (%d) does not match the " + "stacked image width (%d).", offset, img.cols); + } + if(!rectified.empty()) + { + img = rectified; + } } } @@ -966,7 +1202,7 @@ SensorData CameraImages::captureImage(SensorCaptureInfo * info) if(_depthFromScan && !img.empty()) { UDEBUG("Computing depth from scan..."); - if(!model.isValidForProjection()) + if(models.empty() || !models.front().isValidForProjection()) { UWARN("Depth from laser scan: Camera model should be valid."); } @@ -977,10 +1213,23 @@ SensorData CameraImages::captureImage(SensorCaptureInfo * info) else { pcl::PointCloud::Ptr cloud = util3d::laserScanToPointCloud(scan, scan.localTransform()); - depthFromScan = util3d::projectCloudToCamera(img.size(), model.K(), cloud, model.localTransform()); - if(_depthFromScanFillHoles!=0) + // There is no multi-camera version of projectCloudToCamera(): project + // the scan into each (sub-)camera and stack the registered depth images + // horizontally, matching the RGB layout (single iteration in single-camera + // mode). Sub-image widths come from each model's (already set) image size. + // Holes are filled per sub-image so they don't bleed across cameras. + depthFromScan = cv::Mat::zeros(img.rows, img.cols, CV_32FC1); + int offset = 0; + for(size_t i=0; i0, _depthFromScanFillHolesFromBorder); + int subWidth = models[i].imageWidth(); + cv::Mat subDepth = util3d::projectCloudToCamera(cv::Size(subWidth, img.rows), models[i].K(), cloud, models[i].localTransform()); + if(_depthFromScanFillHoles!=0) + { + util3d::fillProjectedCloudHoles(subDepth, _depthFromScanFillHoles>0, _depthFromScanFillHolesFromBorder); + } + subDepth.copyTo(depthFromScan(cv::Rect(offset, 0, subWidth, img.rows))); + offset += subWidth; } } } @@ -992,15 +1241,10 @@ SensorData CameraImages::captureImage(SensorCaptureInfo * info) UWARN("Directory is not set, camera must be initialized."); } - if(model.imageHeight() == 0 || model.imageWidth() == 0) - { - model.setImageSize(img.size()); - } - SensorData data; if(!img.empty() || !scan.empty()) { - data = SensorData(scan, _isDepth?cv::Mat():img, _isDepth?img:depthFromScan, model, this->getNextSeqID(), stamp); + data = SensorData(scan, _isDepth?cv::Mat():img, _isDepth?img:depthFromScan, models, this->getNextSeqID(), stamp); data.setGroundTruth(groundTruthPose); } diff --git a/corelib/src/camera/CameraStereoImages.cpp b/corelib/src/camera/CameraStereoImages.cpp index c50a3b89..67f7bb7b 100644 --- a/corelib/src/camera/CameraStereoImages.cpp +++ b/corelib/src/camera/CameraStereoImages.cpp @@ -27,6 +27,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include +#include +#include #include namespace rtabmap @@ -87,10 +89,12 @@ bool CameraStereoImages::init(const std::string & calibrationFolder, const std:: { UINFO("Calibration folder: \"%s\", name=\"%s\"", calibrationFolder.c_str(), cameraName.c_str()); + multiStereoModels_.clear(); + // look for calibration files - if(!calibrationFolder.empty() && !cameraName.empty()) + if(!_multiCameraCalib && !calibrationFolder.empty() && !cameraName.empty()) { - if(!stereoModel_.load(calibrationFolder, cameraName, false) && !stereoModel_.isValidForProjection()) + if(!stereoModel_.load(calibrationFolder, cameraName, false, this->isImagesRectified()) && !stereoModel_.isValidForProjection()) { UWARN("Missing calibration files for camera \"%s\" in \"%s\" folder, you should calibrate the camera!", cameraName.c_str(), calibrationFolder.c_str()); @@ -105,16 +109,32 @@ bool CameraStereoImages::init(const std::string & calibrationFolder, const std:: } } - stereoModel_.setLocalTransform(this->getLocalTransform()); - stereoModel_.setName(cameraName); - if(this->isImagesRectified() && !stereoModel_.isValidForRectification()) + if(!_multiCameraCalib) { - UWARN("Parameter \"rectifyImages\" is set, but no stereo model is loaded or valid for rectification. This can be ignored if input images are already rectified."); + stereoModel_.setLocalTransform(this->getLocalTransform()); + stereoModel_.setName(cameraName); + if(this->isImagesRectified() && !stereoModel_.isValidForRectification()) + { + UWARN("Parameter \"rectifyImages\" is set, but no stereo model is loaded or valid for rectification. This can be ignored if input images are already rectified."); + } } //desactivate before init as we will do it in this class instead for convenience bool rectify = this->isImagesRectified(); this->setImagesRectified(false); + // The base reader is used here only to enumerate/read the left images; in + // multi-camera mode this class loads the calibration and splits the images + // itself, so prevent the base from doing its own multi-camera handling and + // per-frame config loading (which would look for config files inside the image + // directory). Both flags are restored below: _multiCameraCalib still drives the + // per-frame vs. shared calibration loading in this class. + bool configForEachFrame = this->isConfigForEachFrame(); + bool multiCameraCalib = _multiCameraCalib; + if(multiCameraCalib) + { + this->setConfigForEachFrame(false); + } + _multiCameraCalib = false; bool success = false; if(CameraImages::init()) @@ -144,13 +164,141 @@ bool CameraStereoImages::init(const std::string & calibrationFolder, const std:: success = true; } } + + // restore the flags: _multiCameraCalib drives the per-frame vs. shared calibration loading below + this->setConfigForEachFrame(configForEachFrame); + _multiCameraCalib = multiCameraCalib; + + if(success && _multiCameraCalib) + { + // Multi-camera stereo mode: each left/right image is a horizontal stack of N + // sub-camera images. Load one StereoCameraModel per sub-camera from calibration + // files named "__left.yaml" and "__right.yaml" + // (index starting at 0). + if(calibrationFolder.empty()) + { + UERROR("Multi-camera calibration is enabled but no calibration folder was provided."); + success = false; + } + else + { + const std::vector imageFiles = this->filenames(); + std::string firstBase = imageFiles.front().substr(0, imageFiles.front().find_last_of('.')); + + // In config-for-each-frame mode the prefix is each image's base name (one + // calibration set per frame). Otherwise a single calibration set is shared by all + // frames, using cameraName as the prefix when provided. cameraName may point to a + // specific sub-camera calibration "_" (e.g. when a single calibration + // file is selected); in that case strip the trailing "_" to recover the rig + // prefix shared by all sub-cameras. Fall back to the first image's base name if empty. + std::string leftSuffix = "_0_" + stereoModel_.getLeftSuffix() + ".yaml"; + std::string sharedBase = cameraName; + if(!sharedBase.empty() && !UFile::exists(calibrationFolder + "/" + sharedBase + leftSuffix)) + { + std::size_t us = sharedBase.find_last_of('_'); + if(us != std::string::npos && us+1 < sharedBase.size() && + sharedBase.find_first_not_of("0123456789", us+1) == std::string::npos && + UFile::exists(calibrationFolder + "/" + sharedBase.substr(0, us) + leftSuffix)) + { + sharedBase = sharedBase.substr(0, us); + } + } + if(sharedBase.empty()) + { + sharedBase = firstBase; + } + + // auto-detect the number of cameras + int numCameras = 0; + std::string detectBase = this->isConfigForEachFrame() ? firstBase : sharedBase; + while(UFile::exists(calibrationFolder + "/" + detectBase + "_" + uNumber2Str(numCameras) + "_" + stereoModel_.getLeftSuffix() + ".yaml")) + { + ++numCameras; + } + + if(numCameras == 0) + { + UERROR("Multi-camera calibration is enabled but no calibration file matching " + "\"%s/%s__%s.yaml\" was found.", + calibrationFolder.c_str(), detectBase.c_str(), stereoModel_.getLeftSuffix().c_str()); + success = false; + } + else + { + UINFO("Multi-camera stereo mode: %d sub-cameras detected from \"%s\" (%s).", numCameras, calibrationFolder.c_str(), + this->isConfigForEachFrame()?"one calibration per frame, loaded on demand": + uFormat("calibration \"%s_\" reused for all frames", sharedBase.c_str()).c_str()); + _calibrationFolder = calibrationFolder; + _multiCameraCount = numCameras; + if(this->isConfigForEachFrame()) + { + // Validate the first frame now; the per-frame sub-camera stereo models are + // loaded on demand in captureImage() (keyed by each image's base name) so we + // don't keep every frame's models - and their rectification maps - in memory. + if(loadStereoCameraModels(firstBase, rectify).empty()) + { + success = false; + } + } + else + { + // A single calibration set is shared by all frames: load it once. + std::vector models = loadStereoCameraModels(sharedBase, rectify); + if(models.empty()) + { + success = false; + } + else + { + multiStereoModels_ = models; + } + } + } + } + } + this->setImagesRectified(rectify); // reset the flag return success; } bool CameraStereoImages::isCalibrated() const { - return stereoModel_.isValidForProjection(); + return stereoModel_.isValidForProjection() || + (!multiStereoModels_.empty() && multiStereoModels_.front().isValidForProjection()) || + (_multiCameraCalib && this->isConfigForEachFrame() && _multiCameraCount > 0); // per-frame models loaded on demand +} + +std::vector CameraStereoImages::loadStereoCameraModels(const std::string & baseName, bool rectify) const +{ + std::vector models(_multiCameraCount); + for(int i=0; i<_multiCameraCount; ++i) + { + std::string name = baseName + "_" + uNumber2Str(i); + if(!models[i].load(_calibrationFolder, name, true /*ignoreStereoTransform*/, rectify) || !models[i].isValidForProjection()) + { + UERROR("Failed to load a valid stereo calibration \"%s/%s_{%s,%s}.yaml\" for multi-camera frame base \"%s\".", + _calibrationFolder.c_str(), name.c_str(), + models[i].getLeftSuffix().c_str(), models[i].getRightSuffix().c_str(), baseName.c_str()); + return std::vector(); + } + if(models[i].localTransform().isNull()) + { + // In multi-camera mode the rig extrinsics are required: each + // sub-camera calibration must provide a "local_transform". + UERROR("Stereo calibration \"%s/%s_%s.yaml\" has no \"local_transform\"; it is " + "required in multi-camera mode (rig extrinsics).", + _calibrationFolder.c_str(), name.c_str(), models[i].getLeftSuffix().c_str()); + return std::vector(); + } + if(rectify && !models[i].isValidForRectification()) + { + UERROR("Parameter \"rectifyImages\" is set, but stereo calibration \"%s/%s_{%s,%s}.yaml\" is not valid for rectification.", + _calibrationFolder.c_str(), name.c_str(), + models[i].getLeftSuffix().c_str(), models[i].getRightSuffix().c_str()); + return std::vector(); + } + } + return models; } std::string CameraStereoImages::getSerial() const @@ -163,6 +311,11 @@ SensorData CameraStereoImages::captureImage(SensorCaptureInfo * info) SensorData data; SensorData left, right; + // Disable the base reader's own multi-camera handling while reading the stacked + // left/right images: this class splits and rectifies them itself below. Restored + // before the multi-camera branch, which relies on the real flag value. + bool multiCameraCalib = _multiCameraCalib; + _multiCameraCalib = false; left = CameraImages::captureImage(info); if(!left.imageRaw().empty()) { @@ -175,18 +328,86 @@ SensorData CameraStereoImages::captureImage(SensorCaptureInfo * info) { right = this->takeImage(info); } + } + _multiCameraCalib = multiCameraCalib; - if(!right.imageRaw().empty()) + if(!left.imageRaw().empty() && !right.imageRaw().empty()) + { + // Rectification + cv::Mat leftImage = left.imageRaw(); + cv::Mat rightImage = right.imageRaw(); + if(rightImage.type() != CV_8UC1 && rightGrayScale_) { - // Rectification - cv::Mat leftImage = left.imageRaw(); - cv::Mat rightImage = right.imageRaw(); - if(rightImage.type() != CV_8UC1 && rightGrayScale_) + cv::Mat tmp; + cv::cvtColor(rightImage, tmp, CV_BGR2GRAY); + rightImage = tmp; + } + + if(multiCameraCalib) + { + // Multi-camera stereo mode: left and right images are horizontal stacks + // of N sub-images (one stereo pair per sub-camera). Each pair is rectified + // independently with its own model and written back into a stacked image of + // the same layout. Sub-images are split using a uniform width (cols / N), + // matching the convention used downstream to de-stack the images. + std::vector models; + if(this->isConfigForEachFrame()) { - cv::Mat tmp; - cv::cvtColor(rightImage, tmp, CV_BGR2GRAY); - rightImage = tmp; + // Per-frame calibration loaded on demand (not kept in memory), + // keyed by the current image's base name. + std::string base = this->lastImageFileName(); + base = base.substr(0, base.find_last_of('.')); + models = loadStereoCameraModels(base, this->isImagesRectified()); } + else + { + // a single calibration set is shared by all frames + UASSERT(!multiStereoModels_.empty()); + models = multiStereoModels_; + } + if(models.empty()) + { + return data; + } + int n = (int)models.size(); + + if(leftImage.cols % n != 0 || rightImage.cols % n != 0) + { + UERROR("Multi-camera stereo: stacked image width (left=%d, right=%d) is not " + "divisible by the number of cameras (%d).", leftImage.cols, rightImage.cols, n); + return data; + } + int subWidthLeft = leftImage.cols/n; + int subWidthRight = rightImage.cols/n; + + if(this->isImagesRectified()) + { + cv::Mat leftRect(leftImage.rows, leftImage.cols, leftImage.type()); + cv::Mat rightRect(rightImage.rows, rightImage.cols, rightImage.type()); + for(int i=0; iisImagesRectified() && stereoModel_.isValidForRectification()) { leftImage = stereoModel_.left().rectifyImage(leftImage); diff --git a/guilib/src/DatabaseViewer.cpp b/guilib/src/DatabaseViewer.cpp index 8951266a..63afea48 100644 --- a/guilib/src/DatabaseViewer.cpp +++ b/guilib/src/DatabaseViewer.cpp @@ -1665,12 +1665,12 @@ void DatabaseViewer::extractImages() } StereoCameraModel model( cameraName, - data.imageRaw().size(), + data.stereoCameraModels()[i].left().imageSize(), data.stereoCameraModels()[i].left().K_raw(), data.stereoCameraModels()[i].left().D_raw(), data.stereoCameraModels()[i].left().R(), data.stereoCameraModels()[i].left().P(), - data.rightRaw().size(), + data.stereoCameraModels()[i].right().imageSize(), data.stereoCameraModels()[i].right().K_raw(), data.stereoCameraModels()[i].right().D_raw(), data.stereoCameraModels()[i].right().R(), @@ -1730,7 +1730,7 @@ void DatabaseViewer::extractImages() cameraName+="_"+uNumber2Str((int)i); } CameraModel model(cameraName, - data.imageRaw().size(), + data.cameraModels()[i].imageSize(), data.cameraModels()[i].K_raw(), data.cameraModels()[i].D_raw(), data.cameraModels()[i].R(), diff --git a/guilib/src/PreferencesDialog.cpp b/guilib/src/PreferencesDialog.cpp index 2ccdd09e..3a824ed4 100644 --- a/guilib/src/PreferencesDialog.cpp +++ b/guilib/src/PreferencesDialog.cpp @@ -843,6 +843,7 @@ PreferencesDialog::PreferencesDialog(QWidget * parent) : connect(_ui->lineEdit_cameraRGBDImages_path_rgb, SIGNAL(textChanged(const QString &)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->lineEdit_cameraRGBDImages_path_depth, SIGNAL(textChanged(const QString &)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->checkBox_cameraImages_configForEachFrame, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); + connect(_ui->checkBox_cameraImages_multiCameraCalibration, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->checkBox_cameraImages_timestamps, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->checkBox_cameraImages_syncTimeStamps, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->doubleSpinBox_cameraRGBDImages_scale, SIGNAL(valueChanged(double)), this, SLOT(makeObsoleteSourcePanel())); @@ -2389,6 +2390,7 @@ void PreferencesDialog::resetSettings(QGroupBox * groupBox) _ui->lineEdit_depthai_blob_path->clear(); _ui->checkBox_cameraImages_configForEachFrame->setChecked(false); + _ui->checkBox_cameraImages_multiCameraCalibration->setChecked(false); _ui->checkBox_cameraImages_timestamps->setChecked(false); _ui->checkBox_cameraImages_syncTimeStamps->setChecked(true); _ui->lineEdit_cameraImages_timestamps->setText(""); @@ -2924,6 +2926,7 @@ void PreferencesDialog::readCameraSettings(const QString & filePath) _ui->comboBox_cameraImages_bayerMode->setCurrentIndex(settings.value("bayerMode",_ui->comboBox_cameraImages_bayerMode->currentIndex()).toInt()); _ui->checkBox_cameraImages_configForEachFrame->setChecked(settings.value("config_each_frame",_ui->checkBox_cameraImages_configForEachFrame->isChecked()).toBool()); + _ui->checkBox_cameraImages_multiCameraCalibration->setChecked(settings.value("multi_camera_calibration",_ui->checkBox_cameraImages_multiCameraCalibration->isChecked()).toBool()); _ui->checkBox_cameraImages_timestamps->setChecked(settings.value("filenames_as_stamps",_ui->checkBox_cameraImages_timestamps->isChecked()).toBool()); _ui->checkBox_cameraImages_syncTimeStamps->setChecked(settings.value("sync_stamps",_ui->checkBox_cameraImages_syncTimeStamps->isChecked()).toBool()); _ui->lineEdit_cameraImages_timestamps->setText(settings.value("stamps", _ui->lineEdit_cameraImages_timestamps->text()).toString()); @@ -3546,6 +3549,7 @@ void PreferencesDialog::writeCameraSettings(const QString & filePath) const settings.setValue("maxFrames", _ui->source_images_spinBox_maxFrames->value()); settings.setValue("bayerMode", _ui->comboBox_cameraImages_bayerMode->currentIndex()); settings.setValue("config_each_frame", _ui->checkBox_cameraImages_configForEachFrame->isChecked()); + settings.setValue("multi_camera_calibration", _ui->checkBox_cameraImages_multiCameraCalibration->isChecked()); settings.setValue("filenames_as_stamps", _ui->checkBox_cameraImages_timestamps->isChecked()); settings.setValue("sync_stamps", _ui->checkBox_cameraImages_syncTimeStamps->isChecked()); settings.setValue("stamps", _ui->lineEdit_cameraImages_timestamps->text()); @@ -4622,7 +4626,12 @@ void PreferencesDialog::selectCalibrationPath() { dir = getWorkingDirectory()+"/camera_info/"+dir; } - QString path = QFileDialog::getOpenFileName(this, tr("Select file"), dir, tr("Calibration file (*.yaml)")); +#if CV_MAJOR_VERSION < 3 || (CV_MAJOR_VERSION == 3 && CV_MAJOR_VERSION < 2) + QString path = QFileDialog::getOpenFileName(this, tr("Select file"), dir, tr("Calibration file (*.yaml *.xml)")); +#else + QString path = QFileDialog::getOpenFileName(this, tr("Select file"), dir, tr("Calibration file (*.yaml *.xml *.json)")); +#endif + if(path.size()) { _ui->lineEdit_calibrationFile->setText(path); @@ -7069,6 +7078,7 @@ Camera * PreferencesDialog::createCamera( _ui->lineEdit_cameraImages_timestamps->text().toStdString(), _ui->checkBox_cameraImages_syncTimeStamps->isChecked()); ((CameraRGBDImages*)camera)->setConfigForEachFrame(_ui->checkBox_cameraImages_configForEachFrame->isChecked()); + ((CameraRGBDImages*)camera)->setMultiCameraCalibration(_ui->checkBox_cameraImages_multiCameraCalibration->isChecked()); } else if(driver == kSrcDC1394) { @@ -7115,6 +7125,7 @@ Camera * PreferencesDialog::createCamera( _ui->lineEdit_cameraImages_timestamps->text().toStdString(), _ui->checkBox_cameraImages_syncTimeStamps->isChecked()); ((CameraStereoImages*)camera)->setConfigForEachFrame(_ui->checkBox_cameraImages_configForEachFrame->isChecked()); + ((CameraStereoImages*)camera)->setMultiCameraCalibration(_ui->checkBox_cameraImages_multiCameraCalibration->isChecked()); ((CameraStereoImages*)camera)->setRightGrayScale(_ui->checkBox_stereo_rightGrayScale->isChecked()); } else if (driver == kSrcStereoUsb) @@ -7319,7 +7330,8 @@ Camera * PreferencesDialog::createCamera( _ui->checkBox_cameraImages_timestamps->isChecked(), _ui->lineEdit_cameraImages_timestamps->text().toStdString(), _ui->checkBox_cameraImages_syncTimeStamps->isChecked()); - ((CameraRGBDImages*)camera)->setConfigForEachFrame(_ui->checkBox_cameraImages_configForEachFrame->isChecked()); + ((CameraImages*)camera)->setConfigForEachFrame(_ui->checkBox_cameraImages_configForEachFrame->isChecked()); + ((CameraImages*)camera)->setMultiCameraCalibration(_ui->checkBox_cameraImages_multiCameraCalibration->isChecked()); } else if(driver == kSrcDatabase) { diff --git a/guilib/src/ui/preferencesDialog.ui b/guilib/src/ui/preferencesDialog.ui index 64eff092..c2fef56f 100644 --- a/guilib/src/ui/preferencesDialog.ui +++ b/guilib/src/ui/preferencesDialog.ui @@ -7,7 +7,7 @@ 0 0 980 - 925 + 1246 @@ -63,9 +63,9 @@ 0 - -648 + -1365 684 - 5218 + 5343 @@ -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 - 3 + 0 @@ -3590,7 +3590,7 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - 9 + 7 @@ -7556,279 +7556,6 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - - - - - - - - - - s - - - 4 - - - 9.990000000000000 - - - 0.010000000000000 - - - 0.020000000000000 - - - - - - - - - - - - - - ... - - - - - - - Path to file containing optional IMU data (*.csv [EuRoC format]). Mouse over the box to show formats. - - - 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 - - - - - - - - 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 - - - - - - - Synchronize capture rate with timestamps. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - Local transform from /base_link to /laser_link. Mouse over the box to show formats. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - 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. - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - - - - - - - - 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 - - - @@ -7861,47 +7588,41 @@ 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 /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> + - 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 + 0 0 0 0 0 0 - - + + + + <html><head/><body><p>KITTI: 130 000 points</p></body></html> + + + 99999999 + + + + + ... - - + + - - - - 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> @@ -7911,10 +7632,10 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - + + - Ground truth file. Select the correct format below. + Local transform from /base_link to /imu_link. Mouse over the box to show formats. true @@ -7924,28 +7645,53 @@ 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>Load config file for each frame. Config files are read from the calibration folder and 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. </p></body></html> + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + - - + + - ... + Synchronize capture rate with timestamps. + + + 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> @@ -8020,72 +7766,6 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki - - - - 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 - - - - - - - ... - - - - - - - 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 - - - - - - - - - - - - - - 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 - - - - - - - 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 - - - @@ -8099,7 +7779,347 @@ 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 + + + + + + + ... + + + + + + + Naming <prefix>_<index>.yaml, or <prefix>_<index>_left.yaml and <prefix>_<index>_right.yaml for stereo, index starting at 0 + + + <html><head/><body><p>Multi-camera mode: each image is a horizontal stack of N sub-camera images (for stereo, both left and right images are stacked the same way). One calibration file per sub-camera (see naming format on tooltip) must be in the calibration folder. A local_transform (rig extrinsics) is required in each file.</p></body></html> + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + ... + + + + + + + Odometry file. Select the correct format below. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + ... + + + + + + + EuRoC: 200 Hz -> 250 Hz + + + 99999999 + + + + + + + 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 + + + + + + + <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> + + + + + + + + + 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 + + + + + + + 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 + + + + + + + + + + + + + + s + + + 4 + + + 9.990000000000000 + + + 0.010000000000000 + + + 0.020000000000000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + + + + + + + + + + + + ... + + + + + + + Ground truth file. Select the correct format below. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Path to file containing optional IMU data (*.csv [EuRoC format]). 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></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 + + + + + + + <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 + + + + + + + + + + + + + + + Maximum laser scan points. + + + 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. @@ -8112,13 +8132,16 @@ 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></body></html> - + + - 0 0 0 0 0 0 + 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 diff --git a/package.xml b/package.xml index 4b178ba1..328f494b 100644 --- a/package.xml +++ b/package.xml @@ -1,7 +1,7 @@ rtabmap - 0.23.7 + 0.23.8 RTAB-Map's standalone library. RTAB-Map is a RGB-D SLAM approach with real-time constraints. Mathieu Labbe Mathieu Labbe From 9d30b174e67a86ee7c033370ffdace6a8980d730 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Fri, 26 Jun 2026 09:29:10 -0700 Subject: [PATCH 02/51] Fixed rtabmap-report build without Qt --- tools/Report/CMakeLists.txt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tools/Report/CMakeLists.txt b/tools/Report/CMakeLists.txt index 790a6a61..70b86374 100644 --- a/tools/Report/CMakeLists.txt +++ b/tools/Report/CMakeLists.txt @@ -9,10 +9,13 @@ ADD_EXECUTABLE(report main.cpp) TARGET_LINK_LIBRARIES(report ${LIBRARIES}) -SET_TARGET_PROPERTIES(report PROPERTIES +SET_TARGET_PROPERTIES(report PROPERTIES OUTPUT_NAME ${PROJECT_PREFIX}-report - COMPILE_DEFINITIONS "WITH_QT" ) + +IF(WITH_QT AND (QT4_FOUND OR Qt5_FOUND OR Qt6_FOUND)) + SET_TARGET_PROPERTIES(report PROPERTIES COMPILE_DEFINITIONS "WITH_QT") +ENDIF(WITH_QT AND (QT4_FOUND OR Qt5_FOUND OR Qt6_FOUND)) INSTALL(TARGETS report RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" COMPONENT runtime From cb34c4bd37413fb54a9376b4b6a2ec7d60f54565 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Wed, 1 Jul 2026 18:11:28 -0700 Subject: [PATCH 03/51] MacOS and Windows binaries update (0.23.8) (#1726) * Windows CI: more sensor drivers + Windows/macOS bundle scripts * added opengv to mac build * windows bundle fixes * zed extra neural ddl in separate package * OpenGV macos eigen version error * opengv macos build issues --- .../install-macos-source-deps/action.yml | 225 +++++++++++++ .../patches/depthai-2.32.0-hunter-macos.patch | 41 +++ .../patches/gtsam-4.2.1-eigen5.patch | 83 +++++ .../patches/libnabo-1.1.2.patch | 87 +++++ .../patches/libpointmatcher-1.4.4-boost.patch | 32 ++ .../patches/opengv-91f4b19c-macos.patch | 309 ++++++++++++++++++ ...orbbecsdk-2.8.7-cmake-config-install.patch | 29 ++ .../install-windows-cuda-deps/action.yml | 4 +- .../actions/install-windows-deps/action.yml | 2 + .github/workflows/cmake-macos.yml | 46 +-- .github/workflows/cmake-windows.yml | 19 +- .gitignore | 6 + CMakeLists.txt | 8 +- app/src/CMakeLists.txt | 115 ++++++- app/src/Info.plist.in | 40 +++ bundle_windows_deps.bat | 217 +++++++++++- bundle_windows_deps_cuda.bat | 124 ++++++- cmake_modules/FindEigen3.cmake | 8 +- corelib/src/CMakeLists.txt | 1 - corelib/src/camera/CameraDepthAI.cpp | 17 +- corelib/src/camera/CameraOrbbecSDK.cpp | 53 ++- corelib/src/camera/CameraRealSense2.cpp | 12 + patches/cccorelib_4095bea.patch | 21 ++ patches/libfreenect2_v0.2.1.patch | 102 ++++++ patches/orbbecsdk_2.8.7.patch | 29 ++ tools/CameraRGBD/main.cpp | 26 +- vcpkg.json | 12 + 27 files changed, 1587 insertions(+), 81 deletions(-) create mode 100644 .github/actions/install-macos-source-deps/action.yml create mode 100644 .github/actions/install-macos-source-deps/patches/depthai-2.32.0-hunter-macos.patch create mode 100644 .github/actions/install-macos-source-deps/patches/gtsam-4.2.1-eigen5.patch create mode 100644 .github/actions/install-macos-source-deps/patches/libnabo-1.1.2.patch create mode 100644 .github/actions/install-macos-source-deps/patches/libpointmatcher-1.4.4-boost.patch create mode 100644 .github/actions/install-macos-source-deps/patches/opengv-91f4b19c-macos.patch create mode 100644 .github/actions/install-macos-source-deps/patches/orbbecsdk-2.8.7-cmake-config-install.patch create mode 100644 app/src/Info.plist.in create mode 100644 patches/cccorelib_4095bea.patch create mode 100644 patches/libfreenect2_v0.2.1.patch create mode 100644 patches/orbbecsdk_2.8.7.patch diff --git a/.github/actions/install-macos-source-deps/action.yml b/.github/actions/install-macos-source-deps/action.yml new file mode 100644 index 00000000..21dbd3d4 --- /dev/null +++ b/.github/actions/install-macos-source-deps/action.yml @@ -0,0 +1,225 @@ +name: 'Install macOS Source Dependencies' +description: 'Builds (and per-dependency caches) the source-only deps not available from Homebrew (GTSAM, libnabo+libpointmatcher, OrbbecSDK, depthai) and installs them into /usr/local.' +inputs: + os: + description: 'Runner os label (e.g. matrix.os), used to namespace the per-dependency caches. Required because intel/arm produce different binaries with identical Homebrew version strings.' + required: true + build_type: + description: 'CMake build type.' + required: false + default: 'Release' +runs: + using: "composite" + steps: + - name: Resolve dependency versions + id: depver + shell: bash + # The source deps link against these Homebrew libraries and are compiled + # with Xcode's toolchain, so both are folded into the cache keys: a + # Homebrew bump (e.g. a new Eigen) or an Xcode update forces a rebuild. + run: | + BREW=$(brew list --versions eigen boost yaml-cpp | sort) + XCODE=$(xcodebuild -version 2>/dev/null || clang --version | head -1) + HASH=$(printf '%s\n%s\n' "$BREW" "$XCODE" | shasum | cut -d' ' -f1) + echo "hash=$HASH" >> "$GITHUB_OUTPUT" + + # ----------------------------- GTSAM ----------------------------- + - name: Cache GTSAM + id: cache-gtsam + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/deps-stage/gtsam + key: gtsam-4.2.1-${{ inputs.os }}-${{ hashFiles('.github/actions/install-macos-source-deps/patches/gtsam-4.2.1-eigen5.patch') }}-${{ steps.depver.outputs.hash }} + + - name: Build GTSAM + if: steps.cache-gtsam.outputs.cache-hit != 'true' + shell: bash + run: | + set -e + NPROC=$(sysctl -n hw.logicalcpu) + PATCHES="${{ github.action_path }}/patches" + SRC="${{ runner.temp }}/src-deps/gtsam" + STAGE="${{ runner.temp }}/deps-stage/gtsam" + rm -rf "$SRC"; mkdir -p "$SRC" "$STAGE" + git clone --depth 1 --branch 4.2.1 https://github.com/borglab/gtsam.git "$SRC/gtsam" + cd "$SRC/gtsam" + git apply "$PATCHES/gtsam-4.2.1-eigen5.patch" + cmake -B build \ + -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} \ + -DGTSAM_BUILD_WITH_MARCH_NATIVE=OFF \ + -DGTSAM_WITH_TBB=OFF \ + -DGTSAM_BUILD_EXAMPLES_ALWAYS=OFF \ + -DGTSAM_BUILD_TESTS=OFF \ + -DGTSAM_BUILD_UNSTABLE=OFF \ + -DGTSAM_USE_SYSTEM_EIGEN=ON \ + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 + cmake --build build -j$NPROC + DESTDIR="$STAGE" cmake --install build + + # ----------------- libnabo + libpointmatcher -------------------- + # Built together: libpointmatcher depends on libnabo, so keeping them in one + # unit keeps the build order internal (lpm finds libnabo in the unit's stage). + - name: Cache pointmatcher + id: cache-pointmatcher + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/deps-stage/pointmatcher + key: pointmatcher-nabo1.1.2-lpm1.4.4-${{ inputs.os }}-${{ hashFiles('.github/actions/install-macos-source-deps/patches/libnabo-1.1.2.patch', '.github/actions/install-macos-source-deps/patches/libpointmatcher-1.4.4-boost.patch') }}-${{ steps.depver.outputs.hash }} + + - name: Build pointmatcher + if: steps.cache-pointmatcher.outputs.cache-hit != 'true' + shell: bash + run: | + set -e + NPROC=$(sysctl -n hw.logicalcpu) + PATCHES="${{ github.action_path }}/patches" + SRC="${{ runner.temp }}/src-deps/pointmatcher" + STAGE="${{ runner.temp }}/deps-stage/pointmatcher" + rm -rf "$SRC"; mkdir -p "$SRC" "$STAGE" + # libnabo (required by libpointmatcher) + git clone --depth 1 --branch 1.1.2 https://github.com/ethz-asl/libnabo.git "$SRC/libnabo" + cd "$SRC/libnabo" + git apply "$PATCHES/libnabo-1.1.2.patch" + cmake -B build -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} + cmake --build build -j$NPROC + DESTDIR="$STAGE" cmake --install build + # libpointmatcher (finds libnabo from the unit's staged prefix) + git clone --depth 1 --branch 1.4.4 https://github.com/ethz-asl/libpointmatcher.git "$SRC/libpointmatcher" + cd "$SRC/libpointmatcher" + git apply "$PATCHES/libpointmatcher-1.4.4-boost.patch" + cmake -B build -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DCMAKE_PREFIX_PATH="$STAGE/usr/local" -DBUILD_EVALUATIONS=OFF -DBUILD_EXAMPLES=OFF + cmake --build build -j$NPROC + DESTDIR="$STAGE" cmake --install build + + # ----------------------------- OrbbecSDK ------------------------ + - name: Cache OrbbecSDK + id: cache-orbbec + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/deps-stage/orbbec + key: orbbec-2.8.7-usrlocal-${{ inputs.os }}-${{ hashFiles('.github/actions/install-macos-source-deps/patches/orbbecsdk-2.8.7-cmake-config-install.patch') }}-${{ steps.depver.outputs.hash }} + + - name: Build OrbbecSDK + if: steps.cache-orbbec.outputs.cache-hit != 'true' + shell: bash + run: | + set -e + NPROC=$(sysctl -n hw.logicalcpu) + PATCHES="${{ github.action_path }}/patches" + SRC="${{ runner.temp }}/src-deps/orbbec" + STAGE="${{ runner.temp }}/deps-stage/orbbec" + rm -rf "$SRC"; mkdir -p "$SRC" "$STAGE" + git clone --depth 1 --branch v2.8.7 https://github.com/orbbec/OrbbecSDK_v2.git "$SRC/OrbbecSDK_v2" + cd "$SRC/OrbbecSDK_v2" + git apply "$PATCHES/orbbecsdk-2.8.7-cmake-config-install.patch" + # OrbbecSDK defaults its install prefix to /opt/OrbbecSDK; force /usr/local + # so it stages under usr/local like the other deps and is installed/found. + cmake -B build -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DCMAKE_INSTALL_PREFIX=/usr/local -DOB_BUILD_DOCS=OFF -DOB_BUILD_EXAMPLES=OFF -DOB_BUILD_TOOLS=OFF -DOB_INSTALL_EXAMPLES_SOURCE=OFF + cmake --build build -j$NPROC + DESTDIR="$STAGE" cmake --install build + + # ----------------------------- depthai -------------------------- + - name: Cache depthai + id: cache-depthai + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/deps-stage/depthai + key: depthai-2.32.0-usrlocal-${{ inputs.os }}-${{ hashFiles('.github/actions/install-macos-source-deps/patches/depthai-2.32.0-hunter-macos.patch') }}-${{ steps.depver.outputs.hash }} + + - name: Build depthai + if: steps.cache-depthai.outputs.cache-hit != 'true' + shell: bash + run: | + set -e + NPROC=$(sysctl -n hw.logicalcpu) + PATCHES="${{ github.action_path }}/patches" + SRC="${{ runner.temp }}/src-deps/depthai" + STAGE="${{ runner.temp }}/deps-stage/depthai" + rm -rf "$SRC"; mkdir -p "$SRC" "$STAGE" + # depthai 2.32.0 (Hunter-based; v2 does NOT use vcpkg/CMake presets) + git clone --depth 1 --branch v2.32.0 https://github.com/luxonis/depthai-core.git "$SRC/depthai-core" + cd "$SRC/depthai-core" + git submodule update --init --recursive + # Route zlib to Homebrew: Hunter's pinned zlib fork has a classic + # TARGET_OS_MAC fdopen block that won't compile against the modern macOS SDK. + git apply "$PATCHES/depthai-2.32.0-hunter-macos.patch" + # v2 and its old Hunter deps declare cmake_minimum_required < 3.5, which the + # runner's CMake 4.x rejects, and CMAKE_POLICY_VERSION_MINIMUM does not + # propagate into Hunter's sub-builds. Use a pinned CMake 3.x just for depthai + # (Hunter reuses the same cmake binary for its dependency sub-builds). + python3 -m venv "$SRC/cmake3-venv" + "$SRC/cmake3-venv/bin/pip" install -q "cmake==3.31.6" + CMAKE3="$SRC/cmake3-venv/bin/cmake" + # DEPTHAI_ENABLE_CURL=OFF: Hunter's CURL drags in the broken zlib and also + # fails to configure on this toolchain; rtabmap doesn't need depthai's CURL. + # depthai-core defaults its install prefix to /install; force + # /usr/local so it stages under usr/local like the other deps. + "$CMAKE3" -B build \ + -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} \ + -DCMAKE_INSTALL_PREFIX=/usr/local \ + -DBUILD_SHARED_LIBS=ON \ + -DDEPTHAI_ENABLE_CURL=OFF \ + -DDEPTHAI_BUILD_TESTS=OFF \ + -DDEPTHAI_BUILD_EXAMPLES=OFF \ + -DCMAKE_PREFIX_PATH="$(brew --prefix zlib)" + "$CMAKE3" --build build -j$NPROC + DESTDIR="$STAGE" "$CMAKE3" --install build + + # ----------------------------- opengv --------------------------- + - name: Cache opengv + id: cache-opengv + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/deps-stage/opengv + key: opengv-91f4b19c-usrlocal-${{ inputs.os }}-${{ hashFiles('.github/actions/install-macos-source-deps/patches/opengv-91f4b19c-macos.patch') }}-${{ steps.depver.outputs.hash }} + + - name: Build opengv + if: steps.cache-opengv.outputs.cache-hit != 'true' + shell: bash + run: | + set -e + NPROC=$(sysctl -n hw.logicalcpu) + PATCHES="${{ github.action_path }}/patches" + SRC="${{ runner.temp }}/src-deps/opengv" + STAGE="${{ runner.temp }}/deps-stage/opengv" + rm -rf "$SRC"; mkdir -p "$SRC" "$STAGE" + # opengv pins a commit (not a tag), so clone then checkout. + git clone https://github.com/laurentkneip/opengv.git "$SRC/opengv" + cd "$SRC/opengv" + git checkout 91f4b19c73450833a40e463ad3648aae80b3a7f3 + # Disable the -march=native/armv7-a/armv8-a flags so it builds portably on + # both Intel and Apple Silicon runners. + git apply "$PATCHES/opengv-91f4b19c-macos.patch" + # CMAKE_POLICY_VERSION_MINIMUM=3.5: opengv declares an ancient + # cmake_minimum_required that modern CMake rejects. EIGEN_INCLUDE_DIR points + # opengv's bundled FindEigen at Homebrew's eigen (covers /opt/homebrew on arm64). + cmake -B build \ + -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} \ + -DCMAKE_INSTALL_PREFIX=/usr/local \ + -DBUILD_TESTS=OFF \ + -DBUILD_SHARED_LIBS=ON \ + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ + -DEIGEN_INCLUDE_DIR="$(brew --prefix eigen)/include/eigen3" + cmake --build build -j$NPROC + DESTDIR="$STAGE" cmake --install build + + # ------------------- install all units into /usr/local ---------- + - name: Install source dependencies into /usr/local + shell: bash + # Copy each (cached or freshly built) staged tree into /usr/local so + # rtabmap's find_package() and the bundle fixup (which search /usr/local) + # pick them up. Runs on both cache hit and miss. + run: | + set -e + STAGE="${{ runner.temp }}/deps-stage" + found=0 + for unit in gtsam pointmatcher orbbec depthai opengv; do + if [ -d "$STAGE/$unit/usr/local" ]; then + sudo cp -a "$STAGE/$unit/usr/local/." /usr/local/ + found=1 + fi + done + if [ "$found" != "1" ]; then + echo "No staged source dependencies found under $STAGE" >&2 + exit 1 + fi diff --git a/.github/actions/install-macos-source-deps/patches/depthai-2.32.0-hunter-macos.patch b/.github/actions/install-macos-source-deps/patches/depthai-2.32.0-hunter-macos.patch new file mode 100644 index 00000000..fdeda53b --- /dev/null +++ b/.github/actions/install-macos-source-deps/patches/depthai-2.32.0-hunter-macos.patch @@ -0,0 +1,41 @@ +diff --git a/cmake/depthaiDependencies.cmake b/cmake/depthaiDependencies.cmake +index 6b0b44f98..672c26402 100644 +--- a/cmake/depthaiDependencies.cmake ++++ b/cmake/depthaiDependencies.cmake +@@ -17,7 +17,9 @@ else() + hunter_add_package(FP16) + hunter_add_package(libarchive-luxonis) + hunter_add_package(spdlog) +- hunter_add_package(ZLIB) ++ # ZLIB: use the system/Homebrew copy instead of Hunter's (the old pinned ++ # zlib fork fails to build against the modern macOS SDK). Found below via ++ # the standard FindZLIB module, which provides the same ZLIB::ZLIB target. + if(DEPTHAI_ENABLE_BACKWARD) + hunter_add_package(Backward) + endif() +@@ -45,8 +47,23 @@ if(NOT CONFIG_MODE OR (CONFIG_MODE AND NOT DEPTHAI_SHARED_LIBS)) + # libarchive for firmware packages + find_package(archive_static ${_QUIET} CONFIG REQUIRED) + find_package(lzma ${_QUIET} CONFIG REQUIRED) +- # ZLIB for compressing Apps +- find_package(ZLIB CONFIG REQUIRED) ++ # ZLIB for compressing Apps. ++ # Use the system/Homebrew zlib directly instead of Hunter's: Hunter's pinned ++ # zlib fork fails to build against the modern macOS SDK, and Hunter also ++ # intercepts find_package(ZLIB) via its own FindZLIB on the module path, so ++ # we locate it manually and expose the ZLIB::zlib target depthai expects. ++ find_path(SYSTEM_ZLIB_INCLUDE_DIR NAMES zlib.h) ++ find_library(SYSTEM_ZLIB_LIBRARY NAMES z) ++ if(NOT SYSTEM_ZLIB_INCLUDE_DIR OR NOT SYSTEM_ZLIB_LIBRARY) ++ message(FATAL_ERROR "System zlib not found (looked for zlib.h and libz). Install via: brew install zlib") ++ endif() ++ if(NOT TARGET ZLIB::zlib) ++ add_library(ZLIB::zlib UNKNOWN IMPORTED) ++ set_target_properties(ZLIB::zlib PROPERTIES ++ IMPORTED_LOCATION "${SYSTEM_ZLIB_LIBRARY}" ++ INTERFACE_INCLUDE_DIRECTORIES "${SYSTEM_ZLIB_INCLUDE_DIR}" ++ ) ++ endif() + + # spdlog for library and device logging + find_package(spdlog ${_QUIET} CONFIG REQUIRED) diff --git a/.github/actions/install-macos-source-deps/patches/gtsam-4.2.1-eigen5.patch b/.github/actions/install-macos-source-deps/patches/gtsam-4.2.1-eigen5.patch new file mode 100644 index 00000000..505517b9 --- /dev/null +++ b/.github/actions/install-macos-source-deps/patches/gtsam-4.2.1-eigen5.patch @@ -0,0 +1,83 @@ +diff --git a/cmake/HandleEigen.cmake b/cmake/HandleEigen.cmake +index b3b4f66b6..651cd1009 100644 +--- a/cmake/HandleEigen.cmake ++++ b/cmake/HandleEigen.cmake +@@ -59,7 +59,12 @@ else() + endif() + + # Detect Eigen version: +-set(EIGEN_VER_H "${GTSAM_EIGEN_INCLUDE_FOR_BUILD}/Eigen/src/Core/util/Macros.h") ++# Since Eigen 5.0.0 the version macros moved from Eigen/src/Core/util/Macros.h ++# to a dedicated Eigen/Version header, so check the new location first. ++set(EIGEN_VER_H "${GTSAM_EIGEN_INCLUDE_FOR_BUILD}/Eigen/Version") ++if (NOT EXISTS ${EIGEN_VER_H}) ++ set(EIGEN_VER_H "${GTSAM_EIGEN_INCLUDE_FOR_BUILD}/Eigen/src/Core/util/Macros.h") ++endif() + if (EXISTS ${EIGEN_VER_H}) + file(READ "${EIGEN_VER_H}" STR_EIGEN_VERSION) + +diff --git a/gtsam/base/Matrix.h b/gtsam/base/Matrix.h +index cfedf6d8c..9c9771407 100644 +--- a/gtsam/base/Matrix.h ++++ b/gtsam/base/Matrix.h +@@ -54,7 +54,7 @@ using Matrix7##N = Eigen::Matrix; \ + using Matrix8##N = Eigen::Matrix; \ + using Matrix9##N = Eigen::Matrix; \ + static const Eigen::MatrixBase::IdentityReturnType I_##N##x##N = Matrix##N::Identity(); \ +-static const Eigen::MatrixBase::ConstantReturnType Z_##N##x##N = Matrix##N::Zero(); ++static const decltype(Matrix##N::Zero()) Z_##N##x##N = Matrix##N::Zero(); + + GTSAM_MAKE_MATRIX_DEFS(1) + GTSAM_MAKE_MATRIX_DEFS(2) +diff --git a/gtsam/base/Vector.h b/gtsam/base/Vector.h +index f7923ff88..5410393f6 100644 +--- a/gtsam/base/Vector.h ++++ b/gtsam/base/Vector.h +@@ -27,6 +27,7 @@ + + #include + #include ++#include + #include + #include + +@@ -42,14 +43,14 @@ typedef Eigen::Matrix Vector1; + typedef Eigen::Vector2d Vector2; + typedef Eigen::Vector3d Vector3; + +-static const Eigen::MatrixBase::ConstantReturnType Z_2x1 = Vector2::Zero(); +-static const Eigen::MatrixBase::ConstantReturnType Z_3x1 = Vector3::Zero(); ++static const decltype(Vector2::Zero()) Z_2x1 = Vector2::Zero(); ++static const decltype(Vector3::Zero()) Z_3x1 = Vector3::Zero(); + + // Create handy typedefs and constants for vectors with N>3 + // VectorN and Z_Nx1, for N=1..9 + #define GTSAM_MAKE_VECTOR_DEFS(N) \ + using Vector##N = Eigen::Matrix; \ +- static const Eigen::MatrixBase::ConstantReturnType Z_##N##x1 = Vector##N::Zero(); ++ static const decltype(Vector##N::Zero()) Z_##N##x1 = Vector##N::Zero(); + + GTSAM_MAKE_VECTOR_DEFS(4) + GTSAM_MAKE_VECTOR_DEFS(5) +diff --git a/gtsam/linear/iterative.h b/gtsam/linear/iterative.h +index 22f65b8de..20a0c936e 100644 +--- a/gtsam/linear/iterative.h ++++ b/gtsam/linear/iterative.h +@@ -61,7 +61,7 @@ namespace gtsam { + + /** Apply operator A'*e */ + Vector operator^(const Vector& e) const { +- return A_ ^ e; ++ return gtsam::operator^(A_, e); + } + + /** +@@ -71,7 +71,7 @@ namespace gtsam { + + /** gradient of objective function 0.5*|Ax-b_|^2 at x = A_'*(Ax-b_) */ + Vector gradient(const Vector& x) const { +- return A() ^ (A() * x - b()); ++ return gtsam::operator^(A(), Vector(A() * x - b())); + } + + /** Apply operator A */ diff --git a/.github/actions/install-macos-source-deps/patches/libnabo-1.1.2.patch b/.github/actions/install-macos-source-deps/patches/libnabo-1.1.2.patch new file mode 100644 index 00000000..a9e462f9 --- /dev/null +++ b/.github/actions/install-macos-source-deps/patches/libnabo-1.1.2.patch @@ -0,0 +1,87 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index a5f0c44..69340e4 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -78,18 +78,18 @@ elseif () + endif () + endif () + +-# enable C++11 support. ++# enable C++14 support (required by recent Eigen versions). + if (CMAKE_VERSION VERSION_LESS "3.1") + if (MSVC) + message(FATAL_ERROR "CMake version 3.1 or later is required to compile ${PROJECT_NAME} with Microsoft Visual C++") + endif () + if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") +- set (CMAKE_CXX_FLAGS "-std=c++0x ${CMAKE_CXX_FLAGS}") ++ set (CMAKE_CXX_FLAGS "-std=c++14 ${CMAKE_CXX_FLAGS}") + else () +- set (CMAKE_CXX_FLAGS "-std=c++11 ${CMAKE_CXX_FLAGS}") ++ set (CMAKE_CXX_FLAGS "-std=c++14 ${CMAKE_CXX_FLAGS}") + endif () + else () +- set (CMAKE_CXX_STANDARD 11) ++ set (CMAKE_CXX_STANDARD 14) + endif () + + #======================== External Dependencies =============================== +diff --git a/experimental/kdtree_cpu.cpp b/experimental/kdtree_cpu.cpp +index 302b672..d63fcd3 100644 +--- a/experimental/kdtree_cpu.cpp ++++ b/experimental/kdtree_cpu.cpp +@@ -31,6 +31,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + #include "nabo_experimental.h" + #include "../nabo/index_heap.h" ++#include + #include + #include + #include +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/nabo/kdtree_opencl.cpp b/nabo/kdtree_opencl.cpp +index 5a9fee2..fb1345f 100644 +--- a/nabo/kdtree_opencl.cpp ++++ b/nabo/kdtree_opencl.cpp +@@ -33,6 +33,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + #include "nabo_private.h" + #include "index_heap.h" ++#include + #include + #include + #include +diff --git a/tests/knnshow.cpp b/tests/knnshow.cpp +index 6f4d3fc..c612378 100644 +--- a/tests/knnshow.cpp ++++ b/tests/knnshow.cpp +@@ -30,6 +30,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + #include "nabo/nabo.h" ++#include + #include + #include + #include +diff --git a/tests/knnvalidate.cpp b/tests/knnvalidate.cpp +index 2430249..2a7dcc4 100644 +--- a/tests/knnvalidate.cpp ++++ b/tests/knnvalidate.cpp +@@ -32,6 +32,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + #include "nabo/nabo.h" + #include "helpers.h" + //#include "experimental/nabo_experimental.h" ++#include + #include + #include + #include diff --git a/.github/actions/install-macos-source-deps/patches/libpointmatcher-1.4.4-boost.patch b/.github/actions/install-macos-source-deps/patches/libpointmatcher-1.4.4-boost.patch new file mode 100644 index 00000000..b1030f30 --- /dev/null +++ b/.github/actions/install-macos-source-deps/patches/libpointmatcher-1.4.4-boost.patch @@ -0,0 +1,32 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 9dabfd0..7b73418 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -128,9 +128,9 @@ endif() + #-------------------- + # DEPENDENCY: boost + #-------------------- +-find_package(Boost REQUIRED COMPONENTS thread system program_options date_time) ++find_package(Boost REQUIRED COMPONENTS thread program_options date_time) + if (Boost_MINOR_VERSION GREATER 47) +- find_package(Boost REQUIRED COMPONENTS thread system program_options date_time chrono) ++ find_package(Boost REQUIRED COMPONENTS thread program_options date_time chrono) + endif () + + #-------------------- +diff --git a/libpointmatcherConfig.cmake.in b/libpointmatcherConfig.cmake.in +index 6de2c85..7c887fd 100644 +--- a/libpointmatcherConfig.cmake.in ++++ b/libpointmatcherConfig.cmake.in +@@ -7,9 +7,9 @@ + include(CMakeFindDependencyMacro) + find_dependency(libnabo REQUIRED) + find_dependency(yaml-cpp REQUIRED) +-find_package(Boost COMPONENTS thread system program_options date_time REQUIRED) ++find_package(Boost COMPONENTS thread program_options date_time REQUIRED) + if (Boost_MINOR_VERSION GREATER 47) +- find_package(Boost COMPONENTS thread system program_options date_time chrono REQUIRED) ++ find_package(Boost COMPONENTS thread program_options date_time chrono REQUIRED) + endif () + include(${CMAKE_CURRENT_LIST_DIR}/libpointmatcher-config.cmake) + diff --git a/.github/actions/install-macos-source-deps/patches/opengv-91f4b19c-macos.patch b/.github/actions/install-macos-source-deps/patches/opengv-91f4b19c-macos.patch new file mode 100644 index 00000000..c503c37a --- /dev/null +++ b/.github/actions/install-macos-source-deps/patches/opengv-91f4b19c-macos.patch @@ -0,0 +1,309 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 9660f55..0186da4 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -31,12 +31,12 @@ IF(MSVC) + add_definitions(-D_USE_MATH_DEFINES) + ELSE() + IF (CMAKE_SYSTEM_PROCESSOR MATCHES "(arm64)|(ARM64)|(aarch64)|(AARCH64)") +- add_definitions (-march=armv8-a) ++ #add_definitions (-march=armv8-a) + ELSEIF (CMAKE_SYSTEM_PROCESSOR MATCHES + "(arm)|(ARM)|(armhf)|(ARMHF)|(armel)|(ARMEL)") +- add_definitions (-march=armv7-a) ++ #add_definitions (-march=armv7-a) + ELSE () +- 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 ++ #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 ( + -O3 +@@ -181,7 +181,7 @@ add_library( random_generators test/random_generators.cpp test/random_generators + set_target_properties( opengv random_generators PROPERTIES + SOVERSION ${PROJECT_VERSION} + VERSION ${PROJECT_VERSION} +- CXX_STANDARD 11 ++ CXX_STANDARD 14 + CXX_STANDARD_REQUIRED ON + DEBUG_POSTFIX d ) + +@@ -329,7 +329,7 @@ IF (BUILD_TESTS) + test_Sturm + + PROPERTIES +- CXX_STANDARD 11 ++ CXX_STANDARD 14 + CXX_STANDARD_REQUIRED ON + DEBUG_POSTFIX d ) + +diff --git a/modules/FindEigen.cmake b/modules/FindEigen.cmake +index 126f4a0..39b5524 100644 +--- a/modules/FindEigen.cmake ++++ b/modules/FindEigen.cmake +@@ -30,7 +30,13 @@ if(NOT Eigen_FIND_VERSION) + endif(NOT Eigen_FIND_VERSION) + + macro(_eigen3_check_version) +- file(READ "${EIGEN_INCLUDE_DIR}/Eigen/src/Core/util/Macros.h" _eigen3_version_header) ++ # Since Eigen 5.0.0 the version macros moved from Eigen/src/Core/util/Macros.h ++ # to a dedicated Eigen/Version header, so check the new location first. ++ if(EXISTS "${EIGEN_INCLUDE_DIR}/Eigen/Version") ++ file(READ "${EIGEN_INCLUDE_DIR}/Eigen/Version" _eigen3_version_header) ++ else() ++ file(READ "${EIGEN_INCLUDE_DIR}/Eigen/src/Core/util/Macros.h" _eigen3_version_header) ++ endif() + + string(REGEX MATCH "define[ \t]+EIGEN_WORLD_VERSION[ \t]+([0-9]+)" _eigen3_world_version_match "${_eigen3_version_header}") + set(EIGEN_WORLD_VERSION "${CMAKE_MATCH_1}") +diff --git a/src/absolute_pose/CentralAbsoluteAdapter.cpp b/src/absolute_pose/CentralAbsoluteAdapter.cpp +index 684fa7e..a4ff2bd 100644 +--- a/src/absolute_pose/CentralAbsoluteAdapter.cpp ++++ b/src/absolute_pose/CentralAbsoluteAdapter.cpp +@@ -29,6 +29,7 @@ + ******************************************************************************/ + + ++#include + #include + + +diff --git a/src/absolute_pose/MACentralAbsolute.cpp b/src/absolute_pose/MACentralAbsolute.cpp +index 6edbabc..1687d45 100644 +--- a/src/absolute_pose/MACentralAbsolute.cpp ++++ b/src/absolute_pose/MACentralAbsolute.cpp +@@ -29,6 +29,7 @@ + ******************************************************************************/ + + ++#include + #include + + +diff --git a/src/absolute_pose/MANoncentralAbsolute.cpp b/src/absolute_pose/MANoncentralAbsolute.cpp +index d9b5b09..a7ae689 100644 +--- a/src/absolute_pose/MANoncentralAbsolute.cpp ++++ b/src/absolute_pose/MANoncentralAbsolute.cpp +@@ -29,6 +29,7 @@ + ******************************************************************************/ + + ++#include + #include + + opengv::absolute_pose::MANoncentralAbsolute::MANoncentralAbsolute( +diff --git a/src/absolute_pose/NoncentralAbsoluteAdapter.cpp b/src/absolute_pose/NoncentralAbsoluteAdapter.cpp +index 30176aa..6a0e405 100644 +--- a/src/absolute_pose/NoncentralAbsoluteAdapter.cpp ++++ b/src/absolute_pose/NoncentralAbsoluteAdapter.cpp +@@ -29,6 +29,7 @@ + ******************************************************************************/ + + ++#include + #include + + opengv::absolute_pose::NoncentralAbsoluteAdapter::NoncentralAbsoluteAdapter( +diff --git a/src/absolute_pose/NoncentralAbsoluteMultiAdapter.cpp b/src/absolute_pose/NoncentralAbsoluteMultiAdapter.cpp +index 88c237a..58a82f4 100644 +--- a/src/absolute_pose/NoncentralAbsoluteMultiAdapter.cpp ++++ b/src/absolute_pose/NoncentralAbsoluteMultiAdapter.cpp +@@ -29,6 +29,7 @@ + ******************************************************************************/ + + ++#include + #include + + opengv::absolute_pose::NoncentralAbsoluteMultiAdapter::NoncentralAbsoluteMultiAdapter( +diff --git a/src/absolute_pose/methods.cpp b/src/absolute_pose/methods.cpp +index b1f0889..19c4eaf 100644 +--- a/src/absolute_pose/methods.cpp ++++ b/src/absolute_pose/methods.cpp +@@ -29,6 +29,7 @@ + ******************************************************************************/ + + ++#include + #include + #include + +diff --git a/src/absolute_pose/modules/main.cpp b/src/absolute_pose/modules/main.cpp +index ed0c271..99f7f35 100644 +--- a/src/absolute_pose/modules/main.cpp ++++ b/src/absolute_pose/modules/main.cpp +@@ -29,6 +29,7 @@ + ******************************************************************************/ + + ++#include + #include + #include + +diff --git a/src/math/arun.cpp b/src/math/arun.cpp +index a0d6296..f5c0ac3 100644 +--- a/src/math/arun.cpp ++++ b/src/math/arun.cpp +@@ -29,6 +29,7 @@ + ******************************************************************************/ + + ++#include + #include + + opengv::rotation_t +diff --git a/src/point_cloud/MAPointCloud.cpp b/src/point_cloud/MAPointCloud.cpp +index 81fd5dd..cdccae3 100644 +--- a/src/point_cloud/MAPointCloud.cpp ++++ b/src/point_cloud/MAPointCloud.cpp +@@ -29,6 +29,7 @@ + ******************************************************************************/ + + ++#include + #include + + opengv::point_cloud::MAPointCloud::MAPointCloud( +diff --git a/src/point_cloud/PointCloudAdapter.cpp b/src/point_cloud/PointCloudAdapter.cpp +index f9faaeb..28fbb7e 100644 +--- a/src/point_cloud/PointCloudAdapter.cpp ++++ b/src/point_cloud/PointCloudAdapter.cpp +@@ -29,6 +29,7 @@ + ******************************************************************************/ + + ++#include + #include + + opengv::point_cloud::PointCloudAdapter::PointCloudAdapter( +diff --git a/src/point_cloud/methods.cpp b/src/point_cloud/methods.cpp +index 5409eeb..7a26e6a 100644 +--- a/src/point_cloud/methods.cpp ++++ b/src/point_cloud/methods.cpp +@@ -29,6 +29,7 @@ + ******************************************************************************/ + + ++#include + #include + #include + +diff --git a/src/relative_pose/CentralRelativeAdapter.cpp b/src/relative_pose/CentralRelativeAdapter.cpp +index 38e9a62..5018bd8 100644 +--- a/src/relative_pose/CentralRelativeAdapter.cpp ++++ b/src/relative_pose/CentralRelativeAdapter.cpp +@@ -29,6 +29,7 @@ + ******************************************************************************/ + + ++#include + #include + + opengv::relative_pose::CentralRelativeAdapter::CentralRelativeAdapter( +diff --git a/src/relative_pose/CentralRelativeMultiAdapter.cpp b/src/relative_pose/CentralRelativeMultiAdapter.cpp +index 2ab7476..49a3a07 100644 +--- a/src/relative_pose/CentralRelativeMultiAdapter.cpp ++++ b/src/relative_pose/CentralRelativeMultiAdapter.cpp +@@ -29,6 +29,7 @@ + ******************************************************************************/ + + ++#include + #include + + opengv::relative_pose::CentralRelativeMultiAdapter::CentralRelativeMultiAdapter( +diff --git a/src/relative_pose/CentralRelativeWeightingAdapter.cpp b/src/relative_pose/CentralRelativeWeightingAdapter.cpp +index a6ab478..7684526 100644 +--- a/src/relative_pose/CentralRelativeWeightingAdapter.cpp ++++ b/src/relative_pose/CentralRelativeWeightingAdapter.cpp +@@ -29,6 +29,7 @@ + ******************************************************************************/ + + ++#include + #include + + opengv::relative_pose::CentralRelativeWeightingAdapter::CentralRelativeWeightingAdapter( +diff --git a/src/relative_pose/MACentralRelative.cpp b/src/relative_pose/MACentralRelative.cpp +index ec2959f..75e76d5 100644 +--- a/src/relative_pose/MACentralRelative.cpp ++++ b/src/relative_pose/MACentralRelative.cpp +@@ -29,6 +29,7 @@ + ******************************************************************************/ + + ++#include + #include + + opengv::relative_pose::MACentralRelative::MACentralRelative( +diff --git a/src/relative_pose/MANoncentralRelative.cpp b/src/relative_pose/MANoncentralRelative.cpp +index cea9c14..8566aeb 100644 +--- a/src/relative_pose/MANoncentralRelative.cpp ++++ b/src/relative_pose/MANoncentralRelative.cpp +@@ -29,6 +29,7 @@ + ******************************************************************************/ + + ++#include + #include + + opengv::relative_pose::MANoncentralRelative::MANoncentralRelative( +diff --git a/src/relative_pose/MANoncentralRelativeMulti.cpp b/src/relative_pose/MANoncentralRelativeMulti.cpp +index 49f8ecf..8b62e07 100644 +--- a/src/relative_pose/MANoncentralRelativeMulti.cpp ++++ b/src/relative_pose/MANoncentralRelativeMulti.cpp +@@ -29,6 +29,7 @@ + ******************************************************************************/ + + ++#include + #include + + opengv::relative_pose::MANoncentralRelativeMulti::MANoncentralRelativeMulti( +diff --git a/src/relative_pose/NoncentralRelativeAdapter.cpp b/src/relative_pose/NoncentralRelativeAdapter.cpp +index 552f180..775d520 100644 +--- a/src/relative_pose/NoncentralRelativeAdapter.cpp ++++ b/src/relative_pose/NoncentralRelativeAdapter.cpp +@@ -29,6 +29,7 @@ + ******************************************************************************/ + + ++#include + #include + + opengv::relative_pose::NoncentralRelativeAdapter::NoncentralRelativeAdapter( +diff --git a/src/relative_pose/NoncentralRelativeMultiAdapter.cpp b/src/relative_pose/NoncentralRelativeMultiAdapter.cpp +index f41edbe..733023c 100644 +--- a/src/relative_pose/NoncentralRelativeMultiAdapter.cpp ++++ b/src/relative_pose/NoncentralRelativeMultiAdapter.cpp +@@ -29,6 +29,7 @@ + ******************************************************************************/ + + ++#include + #include + + opengv::relative_pose::NoncentralRelativeMultiAdapter::NoncentralRelativeMultiAdapter( +diff --git a/src/relative_pose/methods.cpp b/src/relative_pose/methods.cpp +index 0027dae..7678567 100644 +--- a/src/relative_pose/methods.cpp ++++ b/src/relative_pose/methods.cpp +@@ -29,6 +29,7 @@ + ******************************************************************************/ + + ++#include + #include + #include + +diff --git a/src/relative_pose/modules/fivept_nister/modules.cpp b/src/relative_pose/modules/fivept_nister/modules.cpp +index 4b134c5..258d416 100644 +--- a/src/relative_pose/modules/fivept_nister/modules.cpp ++++ b/src/relative_pose/modules/fivept_nister/modules.cpp +@@ -29,6 +29,7 @@ + ******************************************************************************/ + + ++#include + #include + #include + #include diff --git a/.github/actions/install-macos-source-deps/patches/orbbecsdk-2.8.7-cmake-config-install.patch b/.github/actions/install-macos-source-deps/patches/orbbecsdk-2.8.7-cmake-config-install.patch new file mode 100644 index 00000000..9bd50c4a --- /dev/null +++ b/.github/actions/install-macos-source-deps/patches/orbbecsdk-2.8.7-cmake-config-install.patch @@ -0,0 +1,29 @@ +diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt +index 261f2cda..644e068f 100644 +--- a/src/CMakeLists.txt ++++ b/src/CMakeLists.txt +@@ -92,17 +92,20 @@ if(OB_IS_MAIN_PROJECT) + ) + + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/generated/Export.h" DESTINATION include/libobsensor/h) +- install(EXPORT ${OB_SDK_LIB_NAME}Config NAMESPACE ob:: DESTINATION lib) ++ install(EXPORT ${OB_SDK_LIB_NAME}Config NAMESPACE ob:: DESTINATION lib/cmake/${OB_SDK_LIB_NAME}) + + include(CMakePackageConfigHelpers) ++ # Name the version file ConfigVersion.cmake (not Version.cmake) and ++ # install it next to the config in lib/cmake/, so find_package(OrbbecSDK) ++ # discovers it in config mode and reads its version. + write_basic_package_version_file( +- "${CMAKE_CURRENT_BINARY_DIR}/${OB_SDK_LIB_NAME}Version.cmake" ++ "${CMAKE_CURRENT_BINARY_DIR}/${OB_SDK_LIB_NAME}ConfigVersion.cmake" + VERSION "${PROJECT_VERSION}" + COMPATIBILITY SameMajorVersion + ) + install( +- FILES "${CMAKE_CURRENT_BINARY_DIR}/${OB_SDK_LIB_NAME}Version.cmake" +- DESTINATION lib ++ FILES "${CMAKE_CURRENT_BINARY_DIR}/${OB_SDK_LIB_NAME}ConfigVersion.cmake" ++ DESTINATION lib/cmake/${OB_SDK_LIB_NAME} + ) + if(MSVC) + install(FILES $ DESTINATION bin OPTIONAL) diff --git a/.github/actions/install-windows-cuda-deps/action.yml b/.github/actions/install-windows-cuda-deps/action.yml index 0027ee58..bd4bbfcd 100644 --- a/.github/actions/install-windows-cuda-deps/action.yml +++ b/.github/actions/install-windows-cuda-deps/action.yml @@ -46,7 +46,9 @@ runs: 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\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + # K4A runtime DLLs (k4a.dll, k4arecord.dll) live in the SDK bin; needed on PATH so fixup_bundle resolves them at package time. + echo "$vcpkg_path\installed\x64-windows-release\sdk\windows-desktop\amd64\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 diff --git a/.github/actions/install-windows-deps/action.yml b/.github/actions/install-windows-deps/action.yml index 00983b29..4659b17b 100644 --- a/.github/actions/install-windows-deps/action.yml +++ b/.github/actions/install-windows-deps/action.yml @@ -34,4 +34,6 @@ runs: $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 + # K4A runtime DLLs (k4a.dll, k4arecord.dll) live in the SDK bin; needed on PATH so fixup_bundle resolves them at package time. + echo "$vcpkg_path\installed\x64-windows-release\sdk\windows-desktop\amd64\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/cmake-macos.yml b/.github/workflows/cmake-macos.yml index a8d37a3e..ce467a77 100644 --- a/.github/workflows/cmake-macos.yml +++ b/.github/workflows/cmake-macos.yml @@ -26,20 +26,16 @@ jobs: include: - build_name: macos-sequoia-intel os: macos-15-intel - extra_deps: "" - extra_cmake_def: '-DBUILD_AS_BUNDLE=ON' + extra_cmake_def: '-DBUILD_AS_BUNDLE=ON -DWITH_ORBBEC_SDK=ON -DWITH_DEPTHAI=ON' - build_name: macos-sequoia-apple-silicon os: macos-15 - extra_deps: "" - extra_cmake_def: '-DBUILD_AS_BUNDLE=ON' + extra_cmake_def: '-DBUILD_AS_BUNDLE=ON -DWITH_ORBBEC_SDK=ON -DWITH_DEPTHAI=ON' - build_name: macos-tahoe-intel os: macos-26-intel - extra_deps: "" - extra_cmake_def: '-DBUILD_AS_BUNDLE=ON' + extra_cmake_def: '-DBUILD_AS_BUNDLE=ON -DWITH_ORBBEC_SDK=ON -DWITH_DEPTHAI=ON' - build_name: macos-tahoe-apple-silicon os: macos-26 - extra_deps: "" - extra_cmake_def: '-DBUILD_AS_BUNDLE=ON' + extra_cmake_def: '-DBUILD_AS_BUNDLE=ON -DWITH_ORBBEC_SDK=ON -DWITH_DEPTHAI=ON' steps: - uses: actions/checkout@v4 @@ -47,7 +43,15 @@ jobs: - name: Install Brew Dependencies run: | # Update brew and install from Brewfile if present, or specific packages - brew install pcl opencv octomap g2o pdal + brew install pcl opencv octomap g2o pdal yaml-cpp librealsense libfreenect libusb zlib + + - name: Install Source Dependencies + # Build (and per-dependency cache) the source-only deps not available from + # Homebrew, then install them into /usr/local. See the composite action. + uses: ./.github/actions/install-macos-source-deps + with: + os: ${{ matrix.os }} + build_type: ${{ env.BUILD_TYPE }} - name: Configure CMake run: | @@ -61,19 +65,19 @@ jobs: run: | ./rtabmap-console --version -# - name: Build MacOS Package -# run: | -# cmake --build ${{ github.workspace }}/build --config ${{ env.BUILD_TYPE }} --target package + - 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: Upload RTABMap Artifacts (DMG) + uses: actions/upload-artifact@v4 + with: + name: RTABMap-Binaries-${{ matrix.build_name }}-nonsigned-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 diff --git a/.github/workflows/cmake-windows.yml b/.github/workflows/cmake-windows.yml index 9af50aa3..5c812843 100644 --- a/.github/workflows/cmake-windows.yml +++ b/.github/workflows/cmake-windows.yml @@ -27,11 +27,11 @@ jobs: - build_name: windows-2022 os: windows-2022 extra_deps: "" - extra_cmake_def: '-DBUILD_AS_BUNDLE=ON -DWITH_PYTHON=ON -DWITH_TORCH=OFF' + extra_cmake_def: '-DWITH_TORCH=OFF -DWITH_ZED=OFF' - build_name: windows-2022-cuda os: windows-2022 extra_deps: "" - extra_cmake_def: '-DBUILD_AS_BUNDLE=ON -DWITH_PYTHON=ON -DWITH_TORCH=ON' + extra_cmake_def: '-DWITH_TORCH=ON -DWITH_ZED=ON -DWITH_CUDASIFT=ON' steps: - uses: actions/checkout@v4 @@ -46,9 +46,24 @@ jobs: - name: Configure CMake run: | + # K4A, K4W2 and ZED are located via these env vars (FindK4A.cmake / FindKinectSDK2.cmake + # and the install rules), pointing at the export where the bundle staged their SDK files. + $env:K4A_ROOT_DIR = "${{env.VCPKG_EXPORT_PATH}}/installed/x64-windows-release" + $env:KINECTSDK20_DIR = "${{env.VCPKG_EXPORT_PATH}}/installed/x64-windows-release" + $env:ZED_SDK_ROOT_DIR = "${{env.VCPKG_EXPORT_PATH}}/installed/x64-windows-release" cmake ` -B ${{github.workspace}}/build ` -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} ` + -DBUILD_AS_BUNDLE=ON ` + -DWITH_PYTHON=ON ` + -DWITH_CERES=ON ` + -DWITH_ORBBEC_SDK=ON ` + -DWITH_FREENECT2=ON ` + -DWITH_K4W2=ON ` + -DWITH_K4A=ON ` + -DWITH_DEPTHAI=ON ` + -DWITH_REALSENSE2=ON ` + -DWITH_CCCORELIB=ON ` ${{ matrix.extra_cmake_def }} ` -DVCPKG_MANIFEST_INSTALL=OFF ` -DVCPKG_TARGET_TRIPLET=x64-windows-release ` diff --git a/.gitignore b/.gitignore index 5b6152a9..1fb83a34 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,9 @@ compile_flags.txt tags build_* *.bak + +# Windows bundle: vcpkg tree, build output, and deps built from source (bundle_windows_deps.bat) +vcpkg/ +vcpkg_installed/ +vcpkg_binaries/ +vcpkg_deps_from_source/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 47fac7b6..25adaccc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -208,7 +208,7 @@ option(WITH_REALSENSE "Include RealSense support" ON) option(WITH_REALSENSE_SLAM "Include RealSenseSlam 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_DEPTHAI "Include depthai-core v2 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) @@ -711,7 +711,9 @@ IF(WITH_MYNTEYE) ENDIF(WITH_MYNTEYE) IF(WITH_DEPTHAI) - FIND_PACKAGE(depthai 2.24) + # Force DepthAI major version 2 (>=2.24, <3.0): the v3 API is not yet + # supported (see CameraDepthAI). + FIND_PACKAGE(depthai 2.24...<3.0) IF(depthai_FOUND) MESSAGE(STATUS "Found depthai-core (targets)") ENDIF(depthai_FOUND) @@ -1877,7 +1879,7 @@ MESSAGE(STATUS " With DepthAI ${depthai_VERSION} = YES (License: MIT)") ELSEIF(NOT WITH_DEPTHAI) MESSAGE(STATUS " With DepthAI = NO (WITH_DEPTHAI=OFF)") ELSE() -MESSAGE(STATUS " With DepthAI = NO (depthai-core not found)") +MESSAGE(STATUS " With DepthAI = NO (depthai-core v2 not found)") ENDIF() IF(xvsdk_FOUND) diff --git a/app/src/CMakeLists.txt b/app/src/CMakeLists.txt index a58eeb3e..68fb707b 100644 --- a/app/src/CMakeLists.txt +++ b/app/src/CMakeLists.txt @@ -24,17 +24,25 @@ IF(WIN32) ELSE( MINGW ) SET(SRC_FILES ${SRC_FILES} ${PROJECT_NAME}.rc) ENDIF( MINGW ) -ENDIF(WIN32) - -IF(BUILD_AS_BUNDLE) - ADD_DEFINITIONS("-DBUILD_AS_BUNDLE") +ENDIF(WIN32) + +IF(BUILD_AS_BUNDLE) + ADD_DEFINITIONS("-DBUILD_AS_BUNDLE") ENDIF() # Add binary IF(APPLE AND BUILD_AS_BUNDLE) ADD_EXECUTABLE(rtabmap_app MACOSX_BUNDLE ${SRC_FILES}) + # Custom Info.plist providing NSCameraUsageDescription, required by macOS to + # grant the app access to USB/UVC cameras (e.g. Orbbec, RealSense). A bundle + # identifier is also set: macOS ties the camera (TCC) permission to it, so it + # must be non-empty for the permission to be granted/persisted. + SET_TARGET_PROPERTIES(rtabmap_app PROPERTIES + MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.in + MACOSX_BUNDLE_GUI_IDENTIFIER "com.introlab.rtabmap" + MACOSX_BUNDLE_BUNDLE_NAME "${CMAKE_BUNDLE_NAME}") ELSEIF(WIN32 AND BUILD_AS_BUNDLE) - ADD_EXECUTABLE(rtabmap_app ${SRC_FILES}) + ADD_EXECUTABLE(rtabmap_app WIN32 ${SRC_FILES}) ELSE() ADD_EXECUTABLE(rtabmap_app ${SRC_FILES}) ENDIF() @@ -70,13 +78,13 @@ IF(BUILD_AS_BUNDLE AND (APPLE OR WIN32)) SET(thirdparty_dest_dir bin) IF(APPLE) - SET(plugin_dest_dir MacOS/plugins) - IF(Qt6_FOUND) - SET(plugin_dest_dir PlugIns) + SET(plugin_dest_dir MacOS/plugins) + IF(Qt6_FOUND) + SET(plugin_dest_dir PlugIns) ENDIF() SET(qtconf_dest_dir Resources) SET(thirdparty_dest_dir MacOS) - SET(APPS "\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/MacOS/${CMAKE_BUNDLE_NAME}") + SET(APPS "\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/MacOS/${CMAKE_BUNDLE_NAME}") ENDIF(APPLE) IF(OpenNI2_FOUND) @@ -103,12 +111,26 @@ IF(BUILD_AS_BUNDLE AND (APPLE OR WIN32)) ENDIF(OpenNI2_FOUND) IF(k4a_FOUND) - # Install needed depthengine_2_0.dll + # Install needed depthengine_2_0.dll. Its location varies by K4A SDK version: + # v1.4.1 ships it under sdk/windows-desktop/amd64/release/bin, or fallback in tools/. IF(WIN32) file(TO_CMAKE_PATH "$ENV{K4A_ROOT_DIR}" ENV_K4A_ROOT_DIR) - INSTALL(FILES "${ENV_K4A_ROOT_DIR}/tools/depthengine_2_0.dll" - DESTINATION ${thirdparty_dest_dir} - COMPONENT runtime) + set(DEPTHENGINE_DLL "") + foreach(_depthengine_dir + "${ENV_K4A_ROOT_DIR}/sdk/windows-desktop/amd64/release/bin" + "${ENV_K4A_ROOT_DIR}/tools") + if(EXISTS "${_depthengine_dir}/depthengine_2_0.dll") + set(DEPTHENGINE_DLL "${_depthengine_dir}/depthengine_2_0.dll") + break() + endif() + endforeach() + IF(DEPTHENGINE_DLL) + INSTALL(FILES "${DEPTHENGINE_DLL}" + DESTINATION ${thirdparty_dest_dir} + COMPONENT runtime) + ELSE() + MESSAGE(WARNING "depthengine_2_0.dll not found under $ENV{K4A_ROOT_DIR} (sdk/windows-desktop/amd64/release/bin or tools); Kinect for Azure depth may not work at runtime.") + ENDIF() ENDIF(WIN32) ENDIF(k4a_FOUND) @@ -241,14 +263,49 @@ IF(BUILD_AS_BUNDLE AND (APPLE OR WIN32)) ENDIF(WIN32) ENDIF(Python3_FOUND) - IF(Qt6_FOUND) + IF(Qt6_FOUND AND APPLE) + # On macOS we do NOT use qt_deploy_runtime_dependencies (macdeployqt): + # macdeployqt cannot deploy VTK 9.6+, whose libraries reference each other + # via @rpath relative to "@loader_path/../lib". Instead we install the Qt plugins + # manually and let fixup_bundle() below deploy all libraries. + IF(TARGET Qt6::QCocoaIntegrationPlugin) + get_target_property(_qt_plugin_loc Qt6::QCocoaIntegrationPlugin LOCATION) + get_filename_component(_qt_plugins_dir "${_qt_plugin_loc}" DIRECTORY) # .../plugins/platforms + get_filename_component(_qt_plugins_dir "${_qt_plugins_dir}" DIRECTORY) # .../plugins + FOREACH(_qt_plugin_type platforms styles imageformats iconengines) + IF(EXISTS "${_qt_plugins_dir}/${_qt_plugin_type}") + # Homebrew Qt plugins are symlinks into the Cellar; resolve them to the + # real files so they aren't installed as dangling symlinks (which would + # break fixup_bundle and codesign). + FILE(GLOB _qt_plugin_files "${_qt_plugins_dir}/${_qt_plugin_type}/*${CMAKE_SHARED_LIBRARY_SUFFIX}") + SET(_qt_plugin_real "") + FOREACH(_qt_plugin_file ${_qt_plugin_files}) + GET_FILENAME_COMPONENT(_qt_plugin_file "${_qt_plugin_file}" REALPATH) + LIST(APPEND _qt_plugin_real "${_qt_plugin_file}") + ENDFOREACH() + IF(_qt_plugin_real) + INSTALL(FILES ${_qt_plugin_real} + DESTINATION ${plugin_dest_dir}/${_qt_plugin_type} + COMPONENT runtime) + ENDIF() + ENDIF() + ENDFOREACH() + ELSE() + MESSAGE(WARNING "Qt6::QCocoaIntegrationPlugin target not found: Qt plugins may be missing from the bundle") + ENDIF() + # qt.conf so Qt finds the bundled plugins (path is relative to Contents/) + SET(QT_CONF_FILE [Paths]\nPlugins=${plugin_dest_dir}) + INSTALL(CODE " + file(WRITE \"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${qtconf_dest_dir}/qt.conf\" \"${QT_CONF_FILE}\") + " COMPONENT runtime) + ELSEIF(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 set(deploy_script "${CMAKE_CURRENT_BINARY_DIR}/deploy_app$.cmake") file(GENERATE OUTPUT ${deploy_script} CONTENT " # Including the file pointed to by QT_DEPLOY_SUPPORT ensures the generated # deployment script has access to qt_deploy_runtime_dependencies() - include(\"${QT_DEPLOY_SUPPORT}\") + include(\"${QT_DEPLOY_SUPPORT}\") qt_deploy_runtime_dependencies( EXECUTABLE \"${APPS}\" PLUGINS_DIR ${plugin_dest_dir} @@ -328,10 +385,13 @@ IF(BUILD_AS_BUNDLE AND (APPLE OR WIN32)) INSTALL(CODE " file(WRITE \"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${qtconf_dest_dir}/qt.conf\" \"${QT_CONF_FILE}\") " COMPONENT runtime) - ENDIF() + ENDIF() # directories to look for dependencies - SET(DIRS "${QT_LIBRARY_DIRS}" "\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/lib") + # CMAKE_LIBRARY_OUTPUT_DIRECTORY is where rtabmap's own libraries (e.g. + # librtabmap_core) are built; fixup_bundle needs it to resolve the app's + # @rpath references to them (macdeployqt used to handle this via rpath). + SET(DIRS "${QT_LIBRARY_DIRS}" "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}" "\$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) @@ -363,6 +423,25 @@ IF(BUILD_AS_BUNDLE AND (APPLE OR WIN32)) include(\"BundleUtilities\") fixup_bundle(\"${APPS}\" \"\${ALL_LIBS}\" \"${DIRS}\") " COMPONENT runtime) - + + IF(APPLE) + # Re-sign ad-hoc AFTER fixup_bundle has finished all its install_name_tool + # edits. + install(CODE " + set(_contents \"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}\") + get_filename_component(_app \"\${_contents}\" DIRECTORY) + file(GLOB_RECURSE _nested \"\${_contents}/*${CMAKE_SHARED_LIBRARY_SUFFIX}\") + foreach(_f \${_nested}) + execute_process(COMMAND codesign --force --sign - --timestamp=none \"\${_f}\") + endforeach() + file(GLOB _frameworks \"\${_contents}/Frameworks/*.framework\") + foreach(_f \${_frameworks}) + execute_process(COMMAND codesign --force --sign - --timestamp=none \"\${_f}\") + endforeach() + message(STATUS \"Ad-hoc re-signing bundle: \${_app}\") + execute_process(COMMAND codesign --force --sign - --timestamp=none \"\${_app}\") + " COMPONENT runtime) + ENDIF(APPLE) + ENDIF(BUILD_AS_BUNDLE AND (APPLE OR WIN32)) diff --git a/app/src/Info.plist.in b/app/src/Info.plist.in new file mode 100644 index 00000000..e9d4ca3c --- /dev/null +++ b/app/src/Info.plist.in @@ -0,0 +1,40 @@ + + + + + NSPrincipalClass + NSApplication + NSHighResolutionCapable + True + 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 + + NSHumanReadableCopyright + ${MACOSX_BUNDLE_COPYRIGHT} + NSCameraUsageDescription + RTAB-Map needs access to the camera to capture images from connected RGB-D/stereo cameras (e.g. Orbbec, RealSense). + + diff --git a/bundle_windows_deps.bat b/bundle_windows_deps.bat index 7f255557..9de688ab 100644 --- a/bundle_windows_deps.bat +++ b/bundle_windows_deps.bat @@ -4,6 +4,8 @@ setlocal enabledelayedexpansion :: --- CONFIGURATION --- set "VCPKG_ROOT=%~dp0vcpkg" set "EXPORT_DIR=%~dp0vcpkg_binaries" +:: All deps downloaded/built from source (git clones + the pinned CMake) live here. +set "SRC_DIR=%~dp0vcpkg_deps_from_source" set "TRIPLET=x64-windows-release" set "SEVENZIP_EXE=C:\Program Files\7-Zip\7z.exe" @@ -43,7 +45,8 @@ echo [+] Installing dependencies via vcpkg manifest... --x-feature=openni2 ^ --x-feature=gtsam-deps ^ --x-feature=python ^ - --x-feature=libpointmatcher-deps || exit /b !errorlevel! + --x-feature=libpointmatcher-deps ^ + --x-feature=libfreenect2-deps || exit /b !errorlevel! :: 3. Export echo [+] Exporting built binaries to raw folder... @@ -52,13 +55,7 @@ for /f "usebackq tokens=*" %%i in (`"%VS_LOCATOR%" -latest -property catalog_pro 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! +"%VCPKG_ROOT%\vcpkg.exe" export --raw --output-dir="%EXPORT_DIR%" --output=%TARGET_NAME% --triplet=%TRIPLET% || exit /b !errorlevel! set "FINAL_EXPORT_PATH=%TARGET_FULL_PATH%" echo [+] Add numpy... @@ -68,6 +65,72 @@ echo [+] Add numpy... %FINAL_EXPORT_PATH%/installed/%TRIPLET%/tools/python3/python.exe -m pip install "numpy<2" || exit /b !errorlevel! :: 4. Other dependencies not in vcpkg +:: Everything cloned/downloaded below goes into %SRC_DIR% to keep the repo root clean. +if not exist "%SRC_DIR%" mkdir "%SRC_DIR%" +cd /d "%SRC_DIR%" + +:: Kinect for Windows SDK v2 (k4w2): rtabmap's FindKinectSDK2.cmake expects the +:: Microsoft SDK layout (KINECTSDK20_DIR\inc and \Lib\x64), but vcpkg's kinectsdk2 +:: port installs vcpkg-style (include\ and lib\). Mirror the bundled headers/lib into +:: the expected layout so it is found with KINECTSDK20_DIR pointing at the export. +if "%KINECTSDK20_DIR%"=="" ( + echo Error: KINECTSDK20_DIR environment variable is not set! Install Kinect for Windows SDK v2: https://www.microsoft.com/en-us/download/details.aspx?id=44561 + pause + exit /b 1 +) +echo [+] Arranging Kinect SDK v2 layout for FindKinectSDK2... +set "K4W2_ROOT=%FINAL_EXPORT_PATH%\installed\%TRIPLET%" +robocopy "%K4W2_ROOT%\include" "%K4W2_ROOT%\inc" Kinect*.h Nui*.h /XO >nul +robocopy "%K4W2_ROOT%\lib" "%K4W2_ROOT%\Lib\x64" Kinect20*.lib /XO >nul +robocopy "%KINECTSDK20_DIR%\bin" "%K4W2_ROOT%\bin" Kinect20.dll /XO >nul +:: Robocopy exit codes under 8 mean successful copies/no changes; 8+ is a failure. +if !errorlevel! GEQ 8 ( + echo Error: Robocopy failed with exit code !errorlevel! + pause + exit /b !errorlevel! +) + +:: orbbec SDK2 +echo [+] Building OrbbecSDK_v2... +if not exist OrbbecSDK_v2 ( + echo [+] Downloading and applying patch... + git clone https://github.com/orbbec/OrbbecSDK_v2.git + cd OrbbecSDK_v2 + git checkout v2.8.7 + :: Install the CMake package config under lib/cmake/ so find_package(OrbbecSDK) works + git apply "%~dp0patches\orbbecsdk_2.8.7.patch" + cd .. +) +cd OrbbecSDK_v2 +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 ^ + -DOB_BUILD_DOCS=OFF ^ + -DOB_BUILD_EXAMPLES=OFF ^ + -DOB_BUILD_TOOLS=OFF ^ + -DOB_INSTALL_EXAMPLES_SOURCE=OFF || exit /b !errorlevel! +cmake --build build --config Release --target install || exit /b !errorlevel! +cd .. + +if "%K4A_ROOT_DIR%"=="" ( + echo Error: K4A_ROOT_DIR environment variable is not set! Install K4A: https://github.com/microsoft/Azure-Kinect-Sensor-SDK/blob/develop/docs/usage.md + pause + exit /b 1 +) +echo Copying Kinect For Azure components... +robocopy "%K4A_ROOT_DIR%\sdk" "%FINAL_EXPORT_PATH%\installed\%TRIPLET%\sdk" /E /XO +:: Robocopy exit codes under 8 mean successful copies/no changes. +:: 8 or higher means there was a failure. +if !errorlevel! GEQ 8 ( + echo Error: Robocopy failed with exit code !errorlevel! + pause + exit /b !errorlevel! +) + :: libnabo echo [+] Building libnabo... if not exist libnabo ( @@ -76,7 +139,7 @@ if not exist libnabo ( cd libnabo :: Jan 27, 2022 git checkout c925c47 - git apply ../patches/libnabo_c925c47.patch + git apply "%~dp0patches\libnabo_c925c47.patch" cd .. ) cd libnabo @@ -103,7 +166,7 @@ if not exist libpointmatcher ( cd libpointmatcher :: Mar 17, 2023 git checkout 7dc58e5 - git apply ../patches/pointmatcher_7dc58e5.patch + git apply "%~dp0patches\pointmatcher_7dc58e5.patch" cd .. ) cd libpointmatcher @@ -136,7 +199,7 @@ if not exist gtsam ( :: June 18, 2025 git checkout 4.3a0-ros git cherry-pick 18af4e6 - git apply ../patches/gtsam_4_3a0-ros.patch + git apply "%~dp0patches\gtsam_4_3a0-ros.patch" cd .. ) cd gtsam @@ -167,7 +230,7 @@ if not exist opengv ( cd opengv :: Aug 6, 2020 git checkout 91f4b19c73450833a40e463ad3648aae80b3a7f3 - git apply ../patches/opengv_91f4b19c.patch + git apply "%~dp0patches\opengv_91f4b19c.patch" cd .. ) cd opengv @@ -182,6 +245,117 @@ cmake -S . -B build -GNinja ^ cmake --build build --config Release --target install || exit /b !errorlevel! cd .. +:: libfreenect2 +echo [+] Building libfreenect2... +if not exist libfreenect2 ( + echo [+] Downloading and applying patch... + git clone https://github.com/OpenKinect/libfreenect2.git + cd libfreenect2 + :: Aug 6, 2021 + git checkout v0.2.1 + :: Patch makes freenect2Config.cmake relocatable so the prebuilt bundle works + :: after being moved/unzipped, drops cudaDeviceProp::clockRate/computeMode + :: removed in CUDA 13 from the CUDA build, and renames a CL_ICDL_VERSION local + :: that collides with the OpenCL 3.0 macro. + git apply "%~dp0patches\libfreenect2_v0.2.1.patch" + cd .. +) +cd libfreenect2 +:: libfreenect2's FindLibUSB.cmake locates libusb via pkg-config. It declares an +:: ancient cmake_minimum_required (2.8.12), which makes FindPkgConfig default +:: PKG_CONFIG_USE_CMAKE_PREFIX_PATH=OFF, so it ignores the vcpkg CMAKE_PREFIX_PATH. +:: Force it ON so the bundle's lib/pkgconfig (libusb-1.0.pc, libturbojpeg.pc) is +:: searched, use the vcpkg-bundled pkgconf, and also set PKG_CONFIG_PATH as a belt. +set "PKG_CONFIG_PATH=%FINAL_EXPORT_PATH%\installed\%TRIPLET%\lib\pkgconfig" +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" ^ + -DPKG_CONFIG_EXECUTABLE="%FINAL_EXPORT_PATH%\installed\%TRIPLET%\tools\pkgconf\pkgconf.exe" ^ + -DPKG_CONFIG_USE_CMAKE_PREFIX_PATH=ON ^ + -DCMAKE_BUILD_TYPE=Release ^ + -DENABLE_CUDA=OFF ^ + -DENABLE_OPENCL=OFF ^ + -DBUILD_EXAMPLES=OFF ^ + -DCMAKE_INSTALL_PREFIX="%FINAL_EXPORT_PATH%\installed\%TRIPLET%" ^ + -DBUILD_SHARED_LIBS=ON || exit /b !errorlevel! +cmake --build build --config Release --target install || exit /b !errorlevel! +cd .. + +:: depthai +echo [+] Building depthai... +if not exist depthai-core ( + echo [+] Downloading... + git clone https://github.com/luxonis/depthai-core.git + cd depthai-core + :: depthai 2.32.0 (Hunter-based; v2 does NOT use vcpkg/CMake presets) + git checkout v2.32.0 + git submodule update --init --recursive + cd .. +) +cd depthai-core +:: v2 and its old Hunter sub-deps declare cmake_minimum_required < 3.5, which +:: CMake 4.x rejects, and CMAKE_POLICY_VERSION_MINIMUM does not propagate into +:: Hunter's sub-builds. Use a pinned CMake 3.x just for depthai (Hunter reuses the +:: same cmake binary for its dependency sub-builds). +set "CMAKE3_VER=3.31.12" +set "CMAKE3_NAME=cmake-%CMAKE3_VER%-windows-x86_64" +set "CMAKE3_DIR=%SRC_DIR%\%CMAKE3_NAME%" +set "CMAKE3=%CMAKE3_DIR%\bin\cmake.exe" +if not exist "%CMAKE3_DIR%" ( + echo [+] Downloading CMake %CMAKE3_VER% ... + curl -L -o "%SRC_DIR%\%CMAKE3_NAME%.zip" "https://github.com/Kitware/CMake/releases/download/v%CMAKE3_VER%/%CMAKE3_NAME%.zip" || exit /b !errorlevel! + "%SEVENZIP_EXE%" x "%SRC_DIR%\%CMAKE3_NAME%.zip" -o"%SRC_DIR%" -y || exit /b !errorlevel! +) +:: v2 is fully Hunter-managed (no vcpkg toolchain). depthaiDependencies.cmake +:: unconditionally does find_package(OpenCV 4 QUIET CONFIG) (not gated by +:: DEPTHAI_OPENCV_SUPPORT); if it finds the bundle's OpenCV, OpenCVModules does +:: find_dependency(TIFF) which collides with Hunter's config-less FindTIFF. We don't +:: need depthai's OpenCV at all (CameraDepthAI converts ImgFrame->cv::Mat itself), so +:: hard-disable the lookup. DEPTHAI_ENABLE_CURL=OFF avoids Hunter's old zlib/curl. +"%CMAKE3%" -S . -B build -GNinja ^ + -DCMAKE_INSTALL_PREFIX="%FINAL_EXPORT_PATH%\installed\%TRIPLET%" ^ + -DCMAKE_BUILD_TYPE=Release ^ + -DBUILD_SHARED_LIBS=ON ^ + -DDEPTHAI_OPENCV_SUPPORT=OFF ^ + -DCMAKE_DISABLE_FIND_PACKAGE_OpenCV=ON ^ + -DDEPTHAI_ENABLE_CURL=OFF ^ + -DDEPTHAI_BUILD_EXAMPLES=OFF ^ + -DDEPTHAI_BUILD_TESTS=OFF || exit /b !errorlevel! +"%CMAKE3%" --build build --config Release --target install || exit /b !errorlevel! +cd .. + +:: CCCoreLib +echo [+] Building CCCoreLib... +if not exist CCCoreLib ( + echo [+] Downloading... + git clone https://github.com/CloudCompare/CCCoreLib.git + cd CCCoreLib + :: June 20, 2026 + git checkout 4095bea6552096cb528c3b8b5cb9505df2aa6002 + git submodule update --init --recursive + :: nanoflann is an internal dep (only used in src/Kriging.cpp, no public header), + :: but CCCoreLib links it PUBLIC, leaking nanoflann::nanoflann into the exported + :: interface and breaking find_package(CCCoreLib). Link it PRIVATE instead. + git apply "%~dp0patches\cccorelib_4095bea.patch" + cd .. +) +cd CCCoreLib +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 ^ + -DCCCORELIB_SHARED=ON ^ + -DCCCORELIB_USE_CGAL=OFF ^ + -DCCCORELIB_USE_TBB=ON ^ + -DCCCORELIB_USE_QT_CONCURRENT=OFF || 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 @@ -205,13 +379,30 @@ if !errorlevel! EQU 0 ( :: Example building rtabmap afterwards goto :EndComment +:: The vcpkg export folder should not be in the rtabmap source directory +:: (otherwise we get some cmake errors about that) set VCPKG_UNZIPPED_EXPORT_PATH=%USERPROFILE%\Downloads\vcpkg-export-########-x64-vs2022 + set PATH=%VCPKG_UNZIPPED_EXPORT_PATH%\installed\x64-windows-release\bin;%PATH% +set PATH=%VCPKG_UNZIPPED_EXPORT_PATH%\installed\x64-windows-release\sdk\windows-desktop\amd64\release\bin;%PATH% +set PATH=%VCPKG_UNZIPPED_EXPORT_PATH%\installed\x64-windows-release\tools\python3\Lib\site-packages\numpy.libs;%PATH% +set K4A_ROOT_DIR=%VCPKG_UNZIPPED_EXPORT_PATH%\installed\x64-windows-release +set KINECTSDK20_DIR=%VCPKG_UNZIPPED_EXPORT_PATH%\installed\x64-windows-release cmake -B build -GNinja ^ -DCMAKE_BUILD_TYPE=Release ^ - -DBUILD_AS_BUNDLE=ON -DWITH_PYTHON=ON ^ + -DBUILD_AS_BUNDLE=ON ^ + -DWITH_PYTHON=ON ^ -DWITH_ZED=OFF ^ + -DWITH_CERES=ON ^ + -DWITH_ORBBEC_SDK=ON ^ + -DWITH_FREENECT2=ON ^ + -DWITH_K4W2=ON ^ + -DWITH_K4A=ON ^ + -DWITH_DEPTHAI=ON ^ + -DWITH_REALSENSE2=ON ^ + -DWITH_CCCORELIB=ON ^ + -DWITH_PDAL=OFF ^ -DVCPKG_MANIFEST_INSTALL=OFF ^ -DVCPKG_TARGET_TRIPLET=x64-windows-release ^ -DVCPKG_INSTALLED_DIR="%VCPKG_UNZIPPED_EXPORT_PATH%/installed" ^ diff --git a/bundle_windows_deps_cuda.bat b/bundle_windows_deps_cuda.bat index 3ba4d2fc..b80488d1 100644 --- a/bundle_windows_deps_cuda.bat +++ b/bundle_windows_deps_cuda.bat @@ -34,6 +34,8 @@ echo Installed CUDA Toolkit: %CUDA_VER% :: --- CONFIGURATION --- set "VCPKG_ROOT=%~dp0vcpkg" set "EXPORT_DIR=%~dp0vcpkg_binaries" +:: All deps downloaded/built from source (pytorch, torchvision, opencv, libfreenect2, ...) live here. +set "SRC_DIR=%~dp0vcpkg_deps_from_source" set "TRIPLET=x64-windows-release" set "SEVENZIP_EXE=C:\Program Files\7-Zip\7z.exe" @@ -70,11 +72,52 @@ if not exist "%FINAL_EXPORT_PATH%" ( xcopy "%CUDA_PATH%\bin\x64\cudnn*.dll" "%FINAL_EXPORT_PATH%\installed\%TRIPLET%\bin\" /Y ) +:: ZED: copy the main library + the zed-config.cmake so find_package(ZED) resolves +:: it from the export (the config derives paths from ZED_SDK_ROOT_DIR/CMAKE_PREFIX_PATH). +if "%ZED_SDK_ROOT_DIR%"=="" ( + echo Error: ZED_SDK_ROOT_DIR environment variable is not set! + pause + exit /b 1 +) +echo Copying ZED main library only... +:: Only the core runtime goes into the bundle; the neural-depth/TensorRT DLLs are +:: shipped separately (see the zed-neural-extra archive below). +robocopy "%ZED_SDK_ROOT_DIR%\bin" "%FINAL_EXPORT_PATH%\installed\%TRIPLET%\bin" sl_zed64.dll zlibwapi.dll /XO +robocopy "%ZED_SDK_ROOT_DIR%\lib" "%FINAL_EXPORT_PATH%\installed\%TRIPLET%\lib" /E /XO +robocopy "%ZED_SDK_ROOT_DIR%\include" "%FINAL_EXPORT_PATH%\installed\%TRIPLET%\include" /E /XO +:: Install ZED's CMake config at the export prefix root so find_package(ZED 2) (config +:: mode, via the vcpkg toolchain CMAKE_PREFIX_PATH) finds it. +robocopy "%ZED_SDK_ROOT_DIR%" "%FINAL_EXPORT_PATH%\installed\%TRIPLET%" zed-config.cmake zed-config-version.cmake /XO +:: Robocopy exit codes under 8 mean successful copies/no changes. +:: 8 or higher means there was a failure. +if !errorlevel! GEQ 8 ( + echo Error: Robocopy failed with exit code !errorlevel! + pause + exit /b !errorlevel! +) +:: Archive the remaining ZED runtime DLLs (neural-depth / TensorRT extras) as a +:: separate optional download, kept out of the main bundle to keep it small. Built +:: only once: skipped if the archive already exists. Staged with robocopy /XF +:: (excludes by name; avoids 7z's "!" exclude switch that delayed expansion mangles). +set "ZED_EXTRA_7Z=%EXPORT_DIR%\%TARGET_NAME%-zed-neural-extra.7z" +set "ZED_EXTRA_DIR=%SRC_DIR%\zed-neural-extra" +if not exist "%ZED_EXTRA_7Z%" ( + echo [+] Creating ZED neural extra archive... + if exist "%ZED_EXTRA_DIR%" rd /s /q "%ZED_EXTRA_DIR%" + robocopy "%ZED_SDK_ROOT_DIR%\bin" "%ZED_EXTRA_DIR%" *.dll /XF sl_zed64.dll zlibwapi.dll >nul + "%SEVENZIP_EXE%" a -t7z -mx9 "%ZED_EXTRA_7Z%" "%ZED_EXTRA_DIR%\*" || exit /b !errorlevel! +) + :: 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 +:: All deps cloned/built below go into %SRC_DIR% (incl. libfreenect2 built by +:: bundle_windows_deps.bat) to keep the repo root clean. +if not exist "%SRC_DIR%" mkdir "%SRC_DIR%" +cd /d "%SRC_DIR%" + :: pytorch, build with local cuda libraries to avoid duplicating them when we install rtabmap echo [+] Building pytorch with cuda support... if not exist pytorch ( @@ -124,6 +167,7 @@ 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 ( @@ -138,7 +182,7 @@ if not exist opencv ( if not exist opencv_contrib ( echo [+] Downloading opencv_contrib... git clone https://github.com/opencv/opencv_contrib.git || exit /b !errorlevel! - cd opencv + cd opencv_contrib :: 4.13.0 minimum required to be compatible with cuda 13 :: Dec 31, 2025 git checkout 4.13.0 @@ -174,6 +218,56 @@ robocopy "%FINAL_EXPORT_PATH%\installed\%TRIPLET%\bin\Lib" "%FINAL_EXPORT_PATH%\ cd .. +:: freenect2 with cuda support +:: libfreenect2's CUDA processors include , which modern CUDA toolkits +:: no longer ship (it moved to the NVIDIA/cuda-samples repo). Fetch it and point +:: NVCUDASAMPLES_ROOT at it; libfreenect2 adds %NVCUDASAMPLES_ROOT%\common\inc to nvcc. +set "CUDA_SAMPLES_DIR=%SRC_DIR%\cuda-samples" +if not exist "%CUDA_SAMPLES_DIR%\common\inc\helper_math.h" ( + mkdir "%CUDA_SAMPLES_DIR%\common\inc" + curl -L -o "%CUDA_SAMPLES_DIR%\common\inc\helper_math.h" "https://raw.githubusercontent.com/NVIDIA/cuda-samples/v12.5/Common/helper_math.h" || exit /b !errorlevel! +) +set "NVCUDASAMPLES_ROOT=%CUDA_SAMPLES_DIR%" +cd libfreenect2 +cmake -S . -B build_cuda -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" ^ + -DPKG_CONFIG_EXECUTABLE="%FINAL_EXPORT_PATH%\installed\%TRIPLET%\tools\pkgconf\pkgconf.exe" ^ + -DPKG_CONFIG_USE_CMAKE_PREFIX_PATH=ON ^ + -DCMAKE_INSTALL_PREFIX="%FINAL_EXPORT_PATH%\installed\%TRIPLET%" ^ + -DCMAKE_BUILD_TYPE=Release ^ + -DENABLE_CUDA=ON ^ + -DENABLE_OPENCL=ON ^ + -DENABLE_OPENGL=ON ^ + -DBUILD_EXAMPLES=OFF ^ + -DBUILD_SHARED_LIBS=ON || exit /b !errorlevel! +cmake --build build_cuda --config Release --target install || exit /b !errorlevel! +cd .. + +:: CudaSift (CUDA SIFT/SURF GPU features, matlabbe fork; needs CUDA) +echo [+] Building CudaSift... +if not exist CudaSift ( + echo [+] Downloading... + git clone https://github.com/matlabbe/CudaSift.git + cd CudaSift + git checkout f764e14ae59ee78ff5b282d38790301d80faadc3 + cd .. +) +cd CudaSift +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 ^ + -DVERBOSE=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 @@ -195,8 +289,9 @@ if !errorlevel! EQU 0 ( :: Example building rtabmap with opencv cuda and libtorch afterwards goto :EndComment -:: Set path of unzipped deps -set VCPKG_UNZIPPED_EXPORT_PATH= +:: The vcpkg export folder should not be in the rtabmap source directory +:: (otherwise we get some cmake errors about that) +set VCPKG_UNZIPPED_EXPORT_PATH=%USERPROFILE%\Downloads\vcpkg-export-########-x64-vs2022 set TRIPLET=x64-windows-release set PATH=%VCPKG_UNZIPPED_EXPORT_PATH%\installed\%TRIPLET%\bin;%PATH% @@ -206,15 +301,10 @@ 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 +set PATH=%VCPKG_UNZIPPED_EXPORT_PATH%\installed\x64-windows-release\sdk\windows-desktop\amd64\release\bin;%PATH% +set K4A_ROOT_DIR=%VCPKG_UNZIPPED_EXPORT_PATH%\installed\%TRIPLET% +set KINECTSDK20_DIR=%VCPKG_UNZIPPED_EXPORT_PATH%\installed\%TRIPLET% +set ZED_SDK_ROOT_DIR=%VCPKG_UNZIPPED_EXPORT_PATH%\installed\%TRIPLET% cmake -B build_cuda -GNinja ^ -DCMAKE_BUILD_TYPE=Release ^ @@ -222,6 +312,16 @@ cmake -B build_cuda -GNinja ^ -DWITH_PYTHON=ON ^ -DWITH_TORCH=ON ^ -DWITH_ZED=ON ^ + -DWITH_CUDASIFT=ON ^ + -DWITH_CERES=ON ^ + -DWITH_ORBBEC_SDK=ON ^ + -DWITH_FREENECT2=ON ^ + -DWITH_K4W2=ON ^ + -DWITH_K4A=ON ^ + -DWITH_DEPTHAI=ON ^ + -DWITH_REALSENSE2=ON ^ + -DWITH_CCCORELIB=ON ^ + -DWITH_PDAL=OFF ^ -DVCPKG_MANIFEST_INSTALL=OFF ^ -DVCPKG_TARGET_TRIPLET=%TRIPLET% ^ -DVCPKG_INSTALLED_DIR="%VCPKG_UNZIPPED_EXPORT_PATH%/installed" ^ diff --git a/cmake_modules/FindEigen3.cmake b/cmake_modules/FindEigen3.cmake index 460de2ae..013755fb 100644 --- a/cmake_modules/FindEigen3.cmake +++ b/cmake_modules/FindEigen3.cmake @@ -40,7 +40,13 @@ if(NOT Eigen3_FIND_VERSION) endif() macro(_eigen3_check_version) - file(READ "${EIGEN3_INCLUDE_DIR}/Eigen/src/Core/util/Macros.h" _eigen3_version_header) + # Since Eigen 5.0.0 the version macros moved from Eigen/src/Core/util/Macros.h + # to a dedicated Eigen/Version header, so check the new location first. + if(EXISTS "${EIGEN3_INCLUDE_DIR}/Eigen/Version") + file(READ "${EIGEN3_INCLUDE_DIR}/Eigen/Version" _eigen3_version_header) + else() + file(READ "${EIGEN3_INCLUDE_DIR}/Eigen/src/Core/util/Macros.h" _eigen3_version_header) + endif() string(REGEX MATCH "define[ \t]+EIGEN_WORLD_VERSION[ \t]+([0-9]+)" _eigen3_world_version_match "${_eigen3_version_header}") set(EIGEN3_WORLD_VERSION "${CMAKE_MATCH_1}") diff --git a/corelib/src/CMakeLists.txt b/corelib/src/CMakeLists.txt index e6bf74c9..6969c5cc 100644 --- a/corelib/src/CMakeLists.txt +++ b/corelib/src/CMakeLists.txt @@ -392,7 +392,6 @@ IF(depthai_FOUND) SET(PUBLIC_LIBRARIES ${PUBLIC_LIBRARIES} depthai::core - depthai::opencv ) ENDIF(depthai_FOUND) diff --git a/corelib/src/camera/CameraDepthAI.cpp b/corelib/src/camera/CameraDepthAI.cpp index beaca874..4dbbafa6 100644 --- a/corelib/src/camera/CameraDepthAI.cpp +++ b/corelib/src/camera/CameraDepthAI.cpp @@ -729,6 +729,21 @@ std::string CameraDepthAI::getSerial() const return ""; } +#ifdef RTABMAP_DEPTHAI +namespace { +// Convert the depthai depth ImgFrame to cv::Mat (similar to dai::ImgFrame::getCvFrame()) +cv::Mat depthImgFrameToCvMat(const std::shared_ptr & frame) +{ + if(frame->getType() != dai::ImgFrame::Type::RAW16) + { + UERROR("Expected RAW16 depth ImgFrame but got type %d", (int)frame->getType()); + return cv::Mat(); + } + return cv::Mat((int)frame->getHeight(), (int)frame->getWidth(), CV_16UC1, frame->getData().data()).clone(); +} +} +#endif + SensorData CameraDepthAI::captureImage(SensorCaptureInfo * info) { SensorData data; @@ -740,7 +755,7 @@ SensorData CameraDepthAI::captureImage(SensorCaptureInfo * info) double stamp = std::chrono::duration(depthOrRight->getTimestampDevice(dai::CameraExposureOffset::MIDDLE).time_since_epoch()).count(); if(outputMode_) - data = SensorData(cv::imdecode(rgbOrLeft->getData(), cv::IMREAD_ANYCOLOR), depthOrRight->getCvFrame(), stereoModel_.left(), this->getNextSeqID(), stamp); + data = SensorData(cv::imdecode(rgbOrLeft->getData(), cv::IMREAD_ANYCOLOR), depthImgFrameToCvMat(depthOrRight), stereoModel_.left(), this->getNextSeqID(), stamp); else data = SensorData(cv::imdecode(rgbOrLeft->getData(), cv::IMREAD_GRAYSCALE), cv::imdecode(depthOrRight->getData(), cv::IMREAD_GRAYSCALE), stereoModel_, this->getNextSeqID(), stamp); diff --git a/corelib/src/camera/CameraOrbbecSDK.cpp b/corelib/src/camera/CameraOrbbecSDK.cpp index 30d11277..d91ebeda 100644 --- a/corelib/src/camera/CameraOrbbecSDK.cpp +++ b/corelib/src/camera/CameraOrbbecSDK.cpp @@ -220,6 +220,12 @@ bool CameraOrbbecSDK::init(const std::string & calibrationFolder, const std::str #ifdef RTABMAP_ORBBEC_SDK this->close(); + // The Orbbec SDK reports failures by throwing ob::Error (e.g. uvc_open + // access denied on macOS without camera permission, or unsupported config). + // Catch them here so init() logs the error and returns false instead of + // letting the exception propagate and abort the whole process. + try { + std::shared_ptr device; ob::Context context; @@ -276,16 +282,20 @@ bool CameraOrbbecSDK::init(const std::string & calibrationFolder, const std::str } if(device.get() == nullptr) { +#ifdef __APPLE__ + const std::string connectHint = "camera is correctly connected. On macOS the Orbbec " + "camera can only be accessed with root privileges, so run the application with sudo."; +#else + const std::string connectHint = "camera is correctly connected and the udev rules are installed."; +#endif if(deviceId_.empty()) { - UERROR( "Could not find any orbbec compatible devices! Verify that the " - "camera is correctly connected and the udev rules are installed."); + UERROR("Could not find any orbbec compatible devices! Verify that the %s", connectHint.c_str()); } 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."); + UERROR("Could not find an orbbec device with ID \"%s\"! Verify that the %s " + "Unset the ID to choose the first camera found.", deviceId_.c_str(), connectHint.c_str()); } - + return false; } @@ -546,6 +556,22 @@ bool CameraOrbbecSDK::init(const std::string & calibrationFolder, const std::str return false; } + } + catch(const ob::Error & e) + { +#ifdef __APPLE__ + UERROR("Failed to initialize Orbbec camera: %s. On macOS the Orbbec SDK accesses " + "the camera through libusb, which must take the USB interface from the system " + "UVC driver; this requires root privileges. If this is a USB access error " + "(e.g. \"uvc_open failed ... Return Code: -3\"), run the application with sudo.", + e.what()); +#else + UERROR("Failed to initialize Orbbec camera: %s", e.what()); +#endif + this->close(); + return false; + } + return true; #else UERROR("CameraOrbbecSDK: RTAB-Map is not built with Orbbec SDK support!"); @@ -598,13 +624,19 @@ SensorData CameraOrbbecSDK::captureImage(SensorCaptureInfo * info) UERROR("Camera is not initialized!"); return data; } + + // The streaming calls below can throw ob::Error (transient frame/USB + // errors, filter or profile failures). Catch them so a bad frame is logged + // and skipped (empty SensorData) instead of aborting the whole process. + try { + 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()); + UWARN("Received %u frames, expecting 2!", frameset->getCount()); return data; } @@ -733,6 +765,13 @@ SensorData CameraOrbbecSDK::captureImage(SensorCaptureInfo * info) } } + } + catch(const ob::Error & e) + { + UERROR("Error capturing Orbbec camera frame: %s", e.what()); + return data; + } + #else UERROR("CameraOrbbecSDK: RTAB-Map is not built with Orbbec SDK support!"); #endif diff --git a/corelib/src/camera/CameraRealSense2.cpp b/corelib/src/camera/CameraRealSense2.cpp index fb15b0c3..a82b514f 100644 --- a/corelib/src/camera/CameraRealSense2.cpp +++ b/corelib/src/camera/CameraRealSense2.cpp @@ -508,7 +508,14 @@ bool CameraRealSense2::init(const std::string & calibrationFolder, const std::st rs2::device_list list = ctx_.query_devices(); if (0 == list.size()) { +#ifdef __APPLE__ + UERROR("No RealSense2 devices were found! On macOS the RealSense SDK accesses " + "the camera through libusb, which must take the USB interface from the " + "system driver; this requires root privileges. If a device is connected " + "but not detected, run the application with sudo."); +#else UERROR("No RealSense2 devices were found!"); +#endif return false; } @@ -546,7 +553,12 @@ bool CameraRealSense2::init(const std::string & calibrationFolder, const std::st } catch(const rs2::error & error) { +#ifdef __APPLE__ + UWARN("%s. Is the camera already used with another app? On macOS, accessing a " + "RealSense camera requires root privileges, so try running with sudo.", error.what()); +#else UWARN("%s. Is the camera already used with another app?", error.what()); +#endif } if (!found) diff --git a/patches/cccorelib_4095bea.patch b/patches/cccorelib_4095bea.patch new file mode 100644 index 00000000..5af89a76 --- /dev/null +++ b/patches/cccorelib_4095bea.patch @@ -0,0 +1,21 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index e671b87..2e2538e 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -121,7 +121,7 @@ set( NANOFLANN_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE ) + set( NANOFLANN_BUILD_TESTS OFF CACHE BOOL "" FORCE ) + add_subdirectory( extern/nanoflann EXCLUDE_FROM_ALL ) + target_link_libraries( CCCoreLib +- PUBLIC ++ PRIVATE + nanoflann::nanoflann + ) + +@@ -236,6 +236,7 @@ install( + CCCoreLib + EXPORT + CCCoreLib-targets ++ RUNTIME DESTINATION bin + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib + INCLUDES DESTINATION include diff --git a/patches/libfreenect2_v0.2.1.patch b/patches/libfreenect2_v0.2.1.patch new file mode 100644 index 00000000..52bc1096 --- /dev/null +++ b/patches/libfreenect2_v0.2.1.patch @@ -0,0 +1,102 @@ +diff --git a/freenect2.cmake.in b/freenect2.cmake.in +index 05df1af..2195bb4 100644 +--- a/freenect2.cmake.in ++++ b/freenect2.cmake.in +@@ -1,10 +1,15 @@ ++# Resolve paths relative to this config file (/lib/cmake/freenect2/) so the ++# install tree stays relocatable (e.g. when shipped/unzipped as a prebuilt bundle). ++get_filename_component(_freenect2_prefix "${CMAKE_CURRENT_LIST_DIR}" DIRECTORY) ++get_filename_component(_freenect2_prefix "${_freenect2_prefix}" DIRECTORY) ++get_filename_component(_freenect2_prefix "${_freenect2_prefix}" DIRECTORY) + FIND_LIBRARY(freenect2_LIBRARY freenect2 +- PATHS @CMAKE_INSTALL_PREFIX@/lib ++ PATHS "${_freenect2_prefix}/lib" + NO_DEFAULT_PATH + ) + SET(freenect2_LIBRARIES ${freenect2_LIBRARY} @LIBFREENECT2_THREADING_LIBRARIES@) + FIND_PATH(freenect2_INCLUDE_DIR @PROJECT_NAME@/libfreenect2.hpp +- PATHS @CMAKE_INSTALL_PREFIX@/include ++ PATHS "${_freenect2_prefix}/include" + NO_DEFAULT_PATH + ) + SET(freenect2_INCLUDE_DIRS ${freenect2_INCLUDE_DIR}) +diff --git a/src/cuda_depth_packet_processor.cu b/src/cuda_depth_packet_processor.cu +index 7ca5974..95c8e11 100644 +--- a/src/cuda_depth_packet_processor.cu ++++ b/src/cuda_depth_packet_processor.cu +@@ -779,12 +779,7 @@ public: + + cudaDeviceProp prop; + CHECK_CUDA(cudaGetDeviceProperties(&prop, i)); +- LOG_INFO << "device " << i << ": " << prop.name << " @ " << (prop.clockRate / 1000) << "MHz Memory " << (prop.totalGlobalMem >> 20) << "MB"; +- +- if (prop.computeMode == cudaComputeModeProhibited) { +- LOG_INFO << " Compute Mode Prohibited"; +- continue; +- } ++ LOG_INFO << "device " << i << ": " << prop.name << " Memory " << (prop.totalGlobalMem >> 20) << "MB"; + + if (prop.major < 1) { + LOG_INFO << " does not support CUDA"; +diff --git a/src/cuda_kde_depth_packet_processor.cu b/src/cuda_kde_depth_packet_processor.cu +index 0a16f55..bca00cc 100644 +--- a/src/cuda_kde_depth_packet_processor.cu ++++ b/src/cuda_kde_depth_packet_processor.cu +@@ -1192,12 +1192,7 @@ public: + + cudaDeviceProp prop; + CHECK_CUDA(cudaGetDeviceProperties(&prop, i)); +- LOG_INFO << "device " << i << ": " << prop.name << " @ " << (prop.clockRate / 1000) << "MHz Memory " << (prop.totalGlobalMem >> 20) << "MB"; +- +- if (prop.computeMode == cudaComputeModeProhibited) { +- LOG_INFO << " Compute Mode Prohibited"; +- continue; +- } ++ LOG_INFO << "device " << i << ": " << prop.name << " Memory " << (prop.totalGlobalMem >> 20) << "MB"; + + if (prop.major < 1) { + LOG_INFO << " does not support CUDA"; +diff --git a/src/opencl_depth_packet_processor.cpp b/src/opencl_depth_packet_processor.cpp +index d8db14d..51d0883 100644 +--- a/src/opencl_depth_packet_processor.cpp ++++ b/src/opencl_depth_packet_processor.cpp +@@ -251,7 +251,7 @@ public: + newIrFrame(); + newDepthFrame(); + +- const int CL_ICDL_VERSION = 2; ++ const int clIcdlVersion = 2; + typedef cl_int (*icdloader_func)(int, size_t, void*, size_t*); + #ifdef _MSC_VER + #pragma warning(push) +@@ -269,7 +269,7 @@ public: + if (clGetICDLoaderInfoOCLICD != NULL) + { + char buf[16]; +- if (clGetICDLoaderInfoOCLICD(CL_ICDL_VERSION, sizeof(buf), buf, NULL) == CL_SUCCESS) ++ if (clGetICDLoaderInfoOCLICD(clIcdlVersion, sizeof(buf), buf, NULL) == CL_SUCCESS) + { + if (strcmp(buf, "2.2.4") < 0 && strlen(buf) <= 5) + LOG_WARNING << "Your ocl-icd has deadlock bugs. Update to 2.2.4+ is recommended."; +diff --git a/src/opencl_kde_depth_packet_processor.cpp b/src/opencl_kde_depth_packet_processor.cpp +index 98dbdff..97fbc6d 100644 +--- a/src/opencl_kde_depth_packet_processor.cpp ++++ b/src/opencl_kde_depth_packet_processor.cpp +@@ -259,7 +259,7 @@ public: + newIrFrame(); + newDepthFrame(); + +- const int CL_ICDL_VERSION = 2; ++ const int clIcdlVersion = 2; + typedef cl_int (*icdloader_func)(int, size_t, void*, size_t*); + #ifdef _MSC_VER + #pragma warning(push) +@@ -277,7 +277,7 @@ public: + if (clGetICDLoaderInfoOCLICD != NULL) + { + char buf[16]; +- if (clGetICDLoaderInfoOCLICD(CL_ICDL_VERSION, sizeof(buf), buf, NULL) == CL_SUCCESS) ++ if (clGetICDLoaderInfoOCLICD(clIcdlVersion, sizeof(buf), buf, NULL) == CL_SUCCESS) + { + if (strcmp(buf, "2.2.4") < 0) + LOG_WARNING << "Your ocl-icd has deadlock bugs. Update to 2.2.4+ is recommended."; diff --git a/patches/orbbecsdk_2.8.7.patch b/patches/orbbecsdk_2.8.7.patch new file mode 100644 index 00000000..9bd50c4a --- /dev/null +++ b/patches/orbbecsdk_2.8.7.patch @@ -0,0 +1,29 @@ +diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt +index 261f2cda..644e068f 100644 +--- a/src/CMakeLists.txt ++++ b/src/CMakeLists.txt +@@ -92,17 +92,20 @@ if(OB_IS_MAIN_PROJECT) + ) + + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/generated/Export.h" DESTINATION include/libobsensor/h) +- install(EXPORT ${OB_SDK_LIB_NAME}Config NAMESPACE ob:: DESTINATION lib) ++ install(EXPORT ${OB_SDK_LIB_NAME}Config NAMESPACE ob:: DESTINATION lib/cmake/${OB_SDK_LIB_NAME}) + + include(CMakePackageConfigHelpers) ++ # Name the version file ConfigVersion.cmake (not Version.cmake) and ++ # install it next to the config in lib/cmake/, so find_package(OrbbecSDK) ++ # discovers it in config mode and reads its version. + write_basic_package_version_file( +- "${CMAKE_CURRENT_BINARY_DIR}/${OB_SDK_LIB_NAME}Version.cmake" ++ "${CMAKE_CURRENT_BINARY_DIR}/${OB_SDK_LIB_NAME}ConfigVersion.cmake" + VERSION "${PROJECT_VERSION}" + COMPATIBILITY SameMajorVersion + ) + install( +- FILES "${CMAKE_CURRENT_BINARY_DIR}/${OB_SDK_LIB_NAME}Version.cmake" +- DESTINATION lib ++ FILES "${CMAKE_CURRENT_BINARY_DIR}/${OB_SDK_LIB_NAME}ConfigVersion.cmake" ++ DESTINATION lib/cmake/${OB_SDK_LIB_NAME} + ) + if(MSVC) + install(FILES $ DESTINATION bin OPTIONAL) diff --git a/tools/CameraRGBD/main.cpp b/tools/CameraRGBD/main.cpp index b8342e6e..adfa5e9c 100644 --- a/tools/CameraRGBD/main.cpp +++ b/tools/CameraRGBD/main.cpp @@ -48,6 +48,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include +#include +#if VTK_MAJOR_VERSION > 9 || (VTK_MAJOR_VERSION==9 && VTK_MINOR_VERSION >= 1) +#include +#endif void showUsage(const char * executableName) { @@ -168,7 +173,6 @@ int main(int argc, char * argv[]) } if (strcmp(argv[i], "-pcl") == 0) { - ++i; usePCLViz = true; continue; } @@ -370,6 +374,18 @@ int main(int argc, char * argv[]) exit(1); } +#ifdef __APPLE__ + if(usePCLViz) + { + // pcl::visualization::CloudViewer creates its render window on its own + // background thread, but macOS only allows NSWindow creation on the main + // thread, so it aborts. Fall back to the default (Qt) viewer. + printf("-pcl is not supported on macOS (pcl::visualization::CloudViewer creates its " + "window on a background thread, which macOS forbids). Using the default viewer instead.\n"); + usePCLViz = false; + } +#endif + if(usePCLViz) { rtabmap::SensorData data = camera->takeData(); @@ -485,6 +501,14 @@ int main(int argc, char * argv[]) } else // Use our own visualizer { +#if VTK_MAJOR_VERSION > 9 || (VTK_MAJOR_VERSION==9 && VTK_MINOR_VERSION >= 1) + // Needed to ensure the appropriate OpenGL context is created for VTK + // rendering (QVTKOpenGLNativeWidget). Without it the embedded VTK widget + // gets an incompatible context and VTK calls a null GL function pointer + // (crash in vtkOpenGLVertexArrayObject::Bind on first paint). Must be set + // before QApplication is constructed. + QSurfaceFormat::setDefaultFormat(QVTKRenderWidget::defaultFormat()); +#endif QApplication app(argc, argv); rtabmap::CameraViewer cameraViewer; diff --git a/vcpkg.json b/vcpkg.json index bca2fac7..f2de8a53 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -113,6 +113,18 @@ "kinectsdk2" ] }, + "libfreenect2-deps": { + "description": "Build libfreenect2's dependencies", + "dependencies": [ + "libusb", + "libjpeg-turbo", + "glfw3", + { + "name": "pkgconf", + "host": true + } + ] + }, "libpointmatcher-deps": { "description": "Build libpointmatcher's dependencies", "dependencies": [ From d069becc724905d6465a8758c40dae88e13bb9e8 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sat, 4 Jul 2026 18:16:37 -0700 Subject: [PATCH 04/51] Windows CI: make vcpkg download compatible with draft release (#1727) * Windows CI: make vcpkg download compatible with draft release * forcing nvdia driver dll to be ignored in fixup_bundle * disabling opencv cudec and python * disabling rtabmap-console --version on cuda build * Added assert when bad imu data is provided. Added checks when zed sdk is returning nan imu data. * Updated zed sdk5 resolution and quality * Updated zed sdk5 resolution and quality * Fixing parameters reloaded when testing camera. Also fixed app freezing when closing Preferences dialog after using test camera dialog * cleanup and debug logs * Added dialogs when starting/stopping sensors/detection. Fixed nullptr bug on MainWindow::handleEvents * renamed * found a way to expose ZED model downloads * Updated default config path on Windows * Fixed extra endline in file logging (windows) * Fixed camera local transform when used with lidar and odomSensor * CI(macos): build g2o from source with OpenMP (libomp include/link fix) * Mac: added ceres dep, fixed omp not found by cmake * Mac: bundle orbbec extensions * stripping rpath of orbbec's extensions * cleanup * Realsense2 error as error (not warning) * Fixed config ownership changed to root when sarting in sudo. LidarVLP16: fixed crash on Mac by reimplementing our own socket threading * LidarVLP16: own socket implementation only for Mac (linux and windows use original PCL's reader) --- .../install-macos-source-deps/action.yml | 43 ++- .../install-windows-cuda-deps/action.yml | 25 +- .../actions/install-windows-deps/action.yml | 25 +- .github/workflows/cmake-macos.yml | 23 +- .github/workflows/cmake-windows.yml | 13 +- app/src/CMakeLists.txt | 42 ++- app/src/main.cpp | 10 + bundle_windows_deps_cuda.bat | 9 +- corelib/include/rtabmap/core/SensorData.h | 2 +- .../rtabmap/core/camera/CameraStereoZed.h | 15 +- .../include/rtabmap/core/lidar/LidarVLP16.h | 46 +++ corelib/src/Parameters.cpp | 7 +- corelib/src/SensorCaptureThread.cpp | 41 ++- corelib/src/SensorData.cpp | 16 ++ corelib/src/camera/CameraRealSense2.cpp | 11 +- corelib/src/camera/CameraStereoZed.cpp | 179 ++++++++++-- corelib/src/lidar/LidarVLP16.cpp | 169 ++++++++++- .../include/rtabmap/gui/PreferencesDialog.h | 9 + guilib/src/DatabaseViewer.cpp | 28 +- guilib/src/MainWindow.cpp | 53 +++- guilib/src/PreferencesDialog.cpp | 271 ++++++++++++++++-- guilib/src/ui/preferencesDialog.ui | 63 ++-- tools/CameraRGBD/main.cpp | 5 +- tools/DatabaseViewer/main.cpp | 6 + tools/LidarViewer/main.cpp | 2 +- tools/OdometryViewer/main.cpp | 3 + utilite/src/ULogger.cpp | 7 +- 27 files changed, 982 insertions(+), 141 deletions(-) diff --git a/.github/actions/install-macos-source-deps/action.yml b/.github/actions/install-macos-source-deps/action.yml index 21dbd3d4..855d646e 100644 --- a/.github/actions/install-macos-source-deps/action.yml +++ b/.github/actions/install-macos-source-deps/action.yml @@ -203,6 +203,47 @@ runs: cmake --build build -j$NPROC DESTDIR="$STAGE" cmake --install build + # ----------------------------- g2o ------------------------------ + - name: Cache g2o + id: cache-g2o + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/deps-stage/g2o + key: g2o-67cbe15c-${{ inputs.os }}-${{ steps.depver.outputs.hash }} + + - name: Build g2o + if: steps.cache-g2o.outputs.cache-hit != 'true' + shell: bash + run: | + set -e + NPROC=$(sysctl -n hw.logicalcpu) + SRC="${{ runner.temp }}/src-deps/g2o" + STAGE="${{ runner.temp }}/deps-stage/g2o" + rm -rf "$SRC"; mkdir -p "$SRC" "$STAGE" + # g2o pins a commit (not a tag), so clone then checkout. + git clone https://github.com/RainerKuemmerle/g2o.git "$SRC/g2o" + cd "$SRC/g2o" + git checkout 67cbe15c998737ac6705d3cd18201a72be0d073d + # CMAKE_INSTALL_PREFIX=/usr/local so it stages under usr/local like the + # other deps. Homebrew's libomp is keg-only and g2o does not propagate + # OpenMP's include/lib dirs, so with Apple clang isn't found and + # libomp isn't linked; add its include (-I) and lib (-L -lomp) explicitly. + LIBOMP=$(brew --prefix libomp) + cmake -B build \ + -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} \ + -DCMAKE_INSTALL_PREFIX=/usr/local \ + -DCMAKE_PREFIX_PATH="$LIBOMP" \ + -DCMAKE_C_FLAGS="-I$LIBOMP/include" \ + -DCMAKE_CXX_FLAGS="-I$LIBOMP/include" \ + -DCMAKE_EXE_LINKER_FLAGS="-L$LIBOMP/lib -lomp" \ + -DCMAKE_SHARED_LINKER_FLAGS="-L$LIBOMP/lib -lomp" \ + -DG2O_BUILD_APPS=OFF \ + -DG2O_BUILD_EXAMPLES=OFF \ + -DG2O_USE_OPENMP=ON \ + -DG2O_USE_OPENGL=OFF + cmake --build build -j$NPROC + DESTDIR="$STAGE" cmake --install build + # ------------------- install all units into /usr/local ---------- - name: Install source dependencies into /usr/local shell: bash @@ -213,7 +254,7 @@ runs: set -e STAGE="${{ runner.temp }}/deps-stage" found=0 - for unit in gtsam pointmatcher orbbec depthai opengv; do + for unit in gtsam pointmatcher orbbec depthai opengv g2o; do if [ -d "$STAGE/$unit/usr/local" ]; then sudo cp -a "$STAGE/$unit/usr/local/." /usr/local/ found=1 diff --git a/.github/actions/install-windows-cuda-deps/action.yml b/.github/actions/install-windows-cuda-deps/action.yml index bd4bbfcd..0664fe70 100644 --- a/.github/actions/install-windows-cuda-deps/action.yml +++ b/.github/actions/install-windows-cuda-deps/action.yml @@ -26,7 +26,7 @@ runs: uses: actions/cache@v4 with: path: ${{ runner.workspace }}/vcpkg_installed - key: ${{ runner.os }}-vcpkg-export-66c0373d-x64-vs2022-cuda130_v1 + key: ${{ runner.os }}-vcpkg-export-66c0373d-x64-vs2022-cuda130_v4 - name: Download and Install vcpkg if: steps.cache-vcpkg.outputs.cache-hit != 'true' @@ -35,10 +35,25 @@ runs: $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 + # The 7z is built locally with bundle_windows_deps_cuda.bat and uploaded to a + # GitHub release. Resolve it by filename via the API (authenticated with the + # built-in token) so it works even from a *draft* release and regardless of + # the (changing) asset id. Requires the workflow to run in-repo (github.token + # can't read introlab drafts from a fork PR). + $repo = "introlab/rtabmap" + $releaseTag = "0.23.8" + $assetName = "vcpkg-export-66c0373d-x64-vs2022-cuda130.7z" + $apiHeaders = @{ Authorization = "Bearer ${{ github.token }}"; "User-Agent" = "rtabmap-ci"; Accept = "application/vnd.github+json" } + # Select the release by tag_name (populated on drafts too, and unique), then + # take the asset from THAT release so concurrent releases can't be confused. + $rel = (Invoke-RestMethod -Uri "https://api.github.com/repos/$repo/releases?per_page=100" -Headers $apiHeaders) | + Where-Object { $_.tag_name -eq $releaseTag } | Select-Object -First 1 + if (-not $rel) { throw "Release tagged $releaseTag not found (including drafts)" } + $asset = $rel.assets | Where-Object { $_.name -eq $assetName } | Select-Object -First 1 + if (-not $asset) { throw "Asset $assetName not found in release $releaseTag" } + Write-Host "Downloading $assetName (asset id $($asset.id)) ..." + $dlHeaders = @{ Authorization = "Bearer ${{ github.token }}"; "User-Agent" = "rtabmap-ci"; Accept = "application/octet-stream" } + Invoke-WebRequest -Uri $asset.url -Headers $dlHeaders -OutFile $archivePath & 7z x $archivePath "-o$install_dir" -y - name: Add vcpkg to PATH and env variable diff --git a/.github/actions/install-windows-deps/action.yml b/.github/actions/install-windows-deps/action.yml index 4659b17b..c9837afa 100644 --- a/.github/actions/install-windows-deps/action.yml +++ b/.github/actions/install-windows-deps/action.yml @@ -13,7 +13,7 @@ runs: uses: actions/cache@v4 with: path: ${{ runner.workspace }}/vcpkg_installed - key: ${{ runner.os }}-vcpkg-export-66c0373d-x64-vs2022-v4 + key: ${{ runner.os }}-vcpkg-export-66c0373d-x64-vs2022-v6 - name: Download and Install vcpkg if: steps.cache-vcpkg.outputs.cache-hit != 'true' @@ -22,10 +22,25 @@ runs: $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 + # The 7z is built locally with bundle_windows_deps.bat and uploaded to a + # GitHub release. Resolve it by filename via the API (authenticated with the + # built-in token) so it works even from a *draft* release and regardless of + # the (changing) asset id. Requires the workflow to run in-repo (github.token + # can't read introlab drafts from a fork PR). + $repo = "introlab/rtabmap" + $releaseTag = "0.23.8" + $assetName = "vcpkg-export-66c0373d-x64-vs2022.7z" + $apiHeaders = @{ Authorization = "Bearer ${{ github.token }}"; "User-Agent" = "rtabmap-ci"; Accept = "application/vnd.github+json" } + # Select the release by tag_name (populated on drafts too, and unique), then + # take the asset from THAT release so concurrent releases can't be confused. + $rel = (Invoke-RestMethod -Uri "https://api.github.com/repos/$repo/releases?per_page=100" -Headers $apiHeaders) | + Where-Object { $_.tag_name -eq $releaseTag } | Select-Object -First 1 + if (-not $rel) { throw "Release tagged $releaseTag not found (including drafts)" } + $asset = $rel.assets | Where-Object { $_.name -eq $assetName } | Select-Object -First 1 + if (-not $asset) { throw "Asset $assetName not found in release $releaseTag" } + Write-Host "Downloading $assetName (asset id $($asset.id)) ..." + $dlHeaders = @{ Authorization = "Bearer ${{ github.token }}"; "User-Agent" = "rtabmap-ci"; Accept = "application/octet-stream" } + Invoke-WebRequest -Uri $asset.url -Headers $dlHeaders -OutFile $archivePath & 7z x $archivePath "-o$install_dir" -y - name: Add vcpkg to PATH and env variable diff --git a/.github/workflows/cmake-macos.yml b/.github/workflows/cmake-macos.yml index ce467a77..a22e3152 100644 --- a/.github/workflows/cmake-macos.yml +++ b/.github/workflows/cmake-macos.yml @@ -26,24 +26,19 @@ jobs: include: - build_name: macos-sequoia-intel os: macos-15-intel - extra_cmake_def: '-DBUILD_AS_BUNDLE=ON -DWITH_ORBBEC_SDK=ON -DWITH_DEPTHAI=ON' - build_name: macos-sequoia-apple-silicon os: macos-15 - extra_cmake_def: '-DBUILD_AS_BUNDLE=ON -DWITH_ORBBEC_SDK=ON -DWITH_DEPTHAI=ON' - build_name: macos-tahoe-intel os: macos-26-intel - extra_cmake_def: '-DBUILD_AS_BUNDLE=ON -DWITH_ORBBEC_SDK=ON -DWITH_DEPTHAI=ON' - build_name: macos-tahoe-apple-silicon os: macos-26 - extra_cmake_def: '-DBUILD_AS_BUNDLE=ON -DWITH_ORBBEC_SDK=ON -DWITH_DEPTHAI=ON' - steps: - uses: actions/checkout@v4 - name: Install Brew Dependencies run: | # Update brew and install from Brewfile if present, or specific packages - brew install pcl opencv octomap g2o pdal yaml-cpp librealsense libfreenect libusb zlib + brew install pcl opencv octomap pdal yaml-cpp librealsense libfreenect libusb zlib libomp suite-sparse ceres-solver - name: Install Source Dependencies # Build (and per-dependency cache) the source-only deps not available from @@ -55,7 +50,21 @@ jobs: - name: Configure CMake run: | - cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} ${{ matrix.extra_cmake_def }} + # Apple clang has no built-in OpenMP; point find_package(OpenMP) at + # Homebrew's keg-only libomp so PCL/g2o/rtabmap enable OpenMP instead of + # repeatedly logging "Could NOT find OpenMP". + LIBOMP=$(brew --prefix libomp) + cmake -B ${{github.workspace}}/build \ + -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} \ + -DBUILD_AS_BUNDLE=ON \ + -DWITH_ORBBEC_SDK=ON \ + -DWITH_DEPTHAI=ON \ + -DWITH_CERES=ON \ + -DOpenMP_C_FLAGS="-Xclang -fopenmp -I$LIBOMP/include" \ + -DOpenMP_C_LIB_NAMES=omp \ + -DOpenMP_CXX_FLAGS="-Xclang -fopenmp -I$LIBOMP/include" \ + -DOpenMP_CXX_LIB_NAMES=omp \ + -DOpenMP_omp_LIBRARY="$LIBOMP/lib/libomp.dylib" - name: Build run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} diff --git a/.github/workflows/cmake-windows.yml b/.github/workflows/cmake-windows.yml index 5c812843..87513c9b 100644 --- a/.github/workflows/cmake-windows.yml +++ b/.github/workflows/cmake-windows.yml @@ -74,6 +74,14 @@ jobs: - name: Build run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} + - name: Info + # Skipped for CUDA: the binary links ZED (sl_zed64.dll -> nvcuvid/nvEncodeAPI64), + # which need the NVIDIA driver; the GPU-less runner can't load the exe. + if: matrix.build_name != 'windows-2022-cuda' + working-directory: ${{github.workspace}}/build/bin + run: | + ./rtabmap-console --version + - name: Build Windows Package shell: pwsh run: | @@ -88,11 +96,6 @@ jobs: 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 (ZIP) uses: actions/upload-artifact@v4 with: diff --git a/app/src/CMakeLists.txt b/app/src/CMakeLists.txt index 68fb707b..462e4c87 100644 --- a/app/src/CMakeLists.txt +++ b/app/src/CMakeLists.txt @@ -151,14 +151,30 @@ IF(BUILD_AS_BUNDLE AND (APPLE OR WIN32)) 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}") + MESSAGE(STATUS "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) + ELSEIF(APPLE) + # OrbbecSDK loads its extensions (frame processor, filters like + # FrameUnpacker) via dlopen relative to libOrbbecSDK. fixup_bundle puts + # libOrbbecSDK in Contents/Frameworks, so the extensions must sit next to + # it in Frameworks/extensions (they reference @rpath/libOrbbecSDK, which + # resolves via the app executable's @executable_path/../Frameworks rpath). + # Without this the camera is detected but frame processing fails. + get_filename_component(OrbbecSDK_LIB_DIR "${OrbbecSDK_DIR}/../.." ABSOLUTE) + INSTALL(DIRECTORY "${OrbbecSDK_LIB_DIR}/extensions" + DESTINATION Frameworks + COMPONENT runtime) + IF(EXISTS "${OrbbecSDK_LIB_DIR}/OrbbecSDKConfig.xml") + INSTALL(FILES "${OrbbecSDK_LIB_DIR}/OrbbecSDKConfig.xml" + DESTINATION Frameworks + COMPONENT runtime) + ENDIF() + ENDIF(WIN32) ENDIF(OrbbecSDK_FOUND) IF(Torch_FOUND) @@ -405,7 +421,17 @@ IF(BUILD_AS_BUNDLE AND (APPLE OR WIN32)) install(CODE " # Glob Qt Plugins file(GLOB_RECURSE ALL_LIBS \"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${plugin_dest_dir}/*${CMAKE_SHARED_LIBRARY_SUFFIX}\") - + + # OrbbecSDK loads its filter/frame-processor extensions via dlopen, and they + # link @rpath/libOrbbecSDK. Feed them to fixup_bundle so their reference is + # rewritten to the embedded libOrbbecSDK + file(GLOB ORBBEC_EXT_LIBS + \"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/Frameworks/extensions/filters/*${CMAKE_SHARED_LIBRARY_SUFFIX}\" + \"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/Frameworks/extensions/frameprocessor/*${CMAKE_SHARED_LIBRARY_SUFFIX}\") + if(ORBBEC_EXT_LIBS) + list(APPEND ALL_LIBS \${ORBBEC_EXT_LIBS}) + endif() + if(NOT \"${python_pyd_dir}\" STREQUAL \"\") file(GLOB_RECURSE PYD_FILES \"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${python_pyd_dir}/*.pyd\") if(PYD_FILES) @@ -421,6 +447,16 @@ IF(BUILD_AS_BUNDLE AND (APPLE OR WIN32)) set(BU_CHMOD_BUNDLE_ITEMS ON) include(\"BundleUtilities\") + # nvcuvid.dll (NVDEC) and nvEncodeAPI64.dll (NVENC), and the CUDA driver API + # nvcuda.dll, ship with the NVIDIA GPU driver (System32), not the CUDA toolkit, + # so they are absent on driver-less CI runners and must never be bundled (they + # are resolved at runtime from the end user's driver). Mark them 'system' so + # fixup_bundle skips them instead of failing to resolve them. + function(gp_resolved_file_type_override resolved_file type_var) + if(resolved_file MATCHES \"(nvcuvid|nvEncodeAPI64|nvcuda)\") + set(\${type_var} \"system\" PARENT_SCOPE) + endif() + endfunction() fixup_bundle(\"${APPS}\" \"\${ALL_LIBS}\" \"${DIRS}\") " COMPONENT runtime) diff --git a/app/src/main.cpp b/app/src/main.cpp index 914a3496..ae433431 100644 --- a/app/src/main.cpp +++ b/app/src/main.cpp @@ -52,6 +52,10 @@ int main(int argc, char* argv[]) ULogger::setLevel(ULogger::kWarning); #ifdef WIN32 + // STA (single-threaded apartment) is required for the native file dialogs / File Explorer + // (see commit d75cc04, "Fixed File Explorer hanging (Qt 5.12)"). Do NOT switch to MTA + // (CoInitializeEx COINIT_MULTITHREADED): it deadlocks the + // native file dialogs. CoInitialize(nullptr); #endif @@ -60,6 +64,12 @@ int main(int argc, char* argv[]) QSurfaceFormat::setDefaultFormat(QVTKRenderWidget::defaultFormat()); #endif + // Recommended by VTK when using QVTKOpenGLNativeWidget (a QOpenGLWidget): let all VTK + // render widgets share a single OpenGL context, which is needed for correct rendering + // when render widgets live in / move across multiple top-level windows. Must be set + // before QApplication is constructed. + QApplication::setAttribute(Qt::AA_ShareOpenGLContexts); + /* Create tasks */ QApplication * app = new QApplication(argc, argv); app->setStyleSheet("QMessageBox { messagebox-text-interaction-flags: 5; }"); // selectable message box diff --git a/bundle_windows_deps_cuda.bat b/bundle_windows_deps_cuda.bat index b80488d1..8d9bf052 100644 --- a/bundle_windows_deps_cuda.bat +++ b/bundle_windows_deps_cuda.bat @@ -205,16 +205,15 @@ cmake -S . -B build -GNinja ^ -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_cudacodec=OFF ^ + -DBUILD_opencv_python3=OFF ^ + -DBUILD_opencv_python_bindings_generator=OFF ^ + -DBUILD_opencv_python_tests=OFF ^ -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 .. diff --git a/corelib/include/rtabmap/core/SensorData.h b/corelib/include/rtabmap/core/SensorData.h index ea5a5dee..cad4f6cc 100644 --- a/corelib/include/rtabmap/core/SensorData.h +++ b/corelib/include/rtabmap/core/SensorData.h @@ -344,7 +344,7 @@ public: void setGPS(const GPS & gps) {gps_ = gps;} const GPS & gps() const {return gps_;} - void setIMU(const IMU & imu) {imu_ = imu; } + void setIMU(const IMU & imu); const IMU & imu() const {return imu_;} void setEnvSensors(const EnvSensors & sensors) {_envSensors = sensors;} diff --git a/corelib/include/rtabmap/core/camera/CameraStereoZed.h b/corelib/include/rtabmap/core/camera/CameraStereoZed.h index b5e9d50e..f0f8cc72 100644 --- a/corelib/include/rtabmap/core/camera/CameraStereoZed.h +++ b/corelib/include/rtabmap/core/camera/CameraStereoZed.h @@ -49,8 +49,8 @@ public: public: CameraStereoZed( int deviceId, - int resolution = -1, // -1 = AUTO, 0=HD4K 1=HD2K 2=HD1080 3=HD1200 4=HD720 5=SVGA 6=VGA - int quality = 1, // 0=NONE, 1=PERFORMANCE, 2=QUALITY + int resolution = -1, // -1 = AUTO, 0=HD4K 1=QHDPLUS 2=HD2K 3=HD1536 4=HD1080 5=HD1200 6=HD720 7=SVGA 8=VGA 9=XVGA 10=TXVGA + int quality = 1, // 0=NONE, 1=PERFORMANCE, 2=QUALITY, 3=ULTRA, 4=NEURAL_LIGHT, 5=NEURAL, 6=NEURAL_ULTRA int sensingMode = 0,// 0=STANDARD, 1=FILL int confidenceThr = 100, bool computeOdometry = false, @@ -61,7 +61,7 @@ public: int texturenessConfidenceThr = 90); // introduced with ZED SDK 3 CameraStereoZed( const std::string & svoFilePath, - int quality = 1, // 0=NONE, 1=PERFORMANCE, 2=QUALITY, 3=NEURAL + int quality = 1, // 0=NONE, 1=PERFORMANCE, 2=QUALITY, 3=ULTRA, 4=NEURAL_LIGHT, 5=NEURAL, 6=NEURAL_ULTRA int sensingMode = 0,// 0=STANDARD, 1=FILL int confidenceThr = 100, bool computeOdometry = false, @@ -81,6 +81,15 @@ public: void postInterIMUPublic(const IMU & imu, double stamp); void setRightGrayScale(bool enabled = true); + /** + * If the given ZED depth mode (quality: same encoding as the constructor) is a NEURAL mode + * whose AI model isn't yet downloaded/optimized for the GPU, returns a human-readable warning + * that the first start will be slower (the ZED SDK downloads/optimizes the model during + * init()). Returns an empty string when the model is ready, the mode isn't neural, or the + * SDK/build doesn't support the check. + */ + static std::string getNeuralModelWarning(int quality); + protected: virtual SensorData captureImage(SensorCaptureInfo * info = 0); diff --git a/corelib/include/rtabmap/core/lidar/LidarVLP16.h b/corelib/include/rtabmap/core/lidar/LidarVLP16.h index cb04d4eb..3ff94b41 100644 --- a/corelib/include/rtabmap/core/lidar/LidarVLP16.h +++ b/corelib/include/rtabmap/core/lidar/LidarVLP16.h @@ -29,10 +29,25 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // Should be first on windows to avoid "WinSock.h has already been included" error #include #include +#include #include #include +#include +#include + +// On Apple, closing pcl::HDLGrabber's UDP socket while its read thread is blocked +// in receive_from returns EBADF, which pcl::HDLGrabber::readPacketsFromSocket() +// rethrows uncaught in its own thread, aborting the process on shutdown. There we +// run our own single-threaded UDP receive loop and own the socket lifecycle. +// Other platforms use the base pcl::VLPGrabber path (which also avoids needing +// boost::asio::io_context, absent from the Boost 1.65 on e.g. Ubuntu Bionic). +// Comment this out to force the base path everywhere (e.g. A/B testing on macOS). +#if defined(__APPLE__) +#define RTABMAP_VLP16_USE_CUSTOM_SOCKET +#endif + namespace rtabmap { struct PointXYZIT { @@ -68,9 +83,28 @@ public: void setOrganized(bool enable); +#ifdef RTABMAP_VLP16_USE_CUSTOM_SOCKET + // Overridden only on Apple: in network mode we run our own UDP receive loop + // instead of pcl::HDLGrabber's, because on macOS/BSD closing the grabber's + // socket while its read thread is blocked in receive_from returns EBADF, a + // code that pcl::HDLGrabber::readPacketsFromSocket() rethrows uncaught (in + // its own thread), aborting the process on shutdown. Owning the socket lets + // us tear it down with the non-throwing receive_from overload. Like + // pcl::HDLGrabber, we bind to the given LOCAL interface address (to scope + // reception on a multi-homed host) and fall back to all interfaces (0.0.0.0) + // when it is unspecified or not a local address. In PCAP mode we delegate to + // the base grabber. On other platforms we inherit pcl::VLPGrabber directly. + virtual void start() override; + virtual void stop() override; + virtual bool isRunning() const override; +#endif + private: void buildTimings(bool dualMode); virtual void toPointClouds (HDLDataPacket *dataPacket) override; +#ifdef RTABMAP_VLP16_USE_CUSTOM_SOCKET + void readPackets(); +#endif protected: virtual SensorData captureData(SensorCaptureInfo * info = 0) override; @@ -88,6 +122,18 @@ private: std::vector > accumulatedScans_; USemaphore scanReady_; UMutex lastScanMutex_; + + // Own UDP receive path (network mode only). + bool networkMode_; + boost::asio::ip::address ipAddress_; + std::uint16_t port_; + bool receivedScan_; // set once the first scan is received +#ifdef RTABMAP_VLP16_USE_CUSTOM_SOCKET + boost::asio::io_context ioContext_; + boost::asio::ip::udp::socket * socket_; + std::thread * readThread_; + std::atomic terminate_; +#endif }; } /* namespace rtabmap */ diff --git a/corelib/src/Parameters.cpp b/corelib/src/Parameters.cpp index ed7e981b..935233bf 100644 --- a/corelib/src/Parameters.cpp +++ b/corelib/src/Parameters.cpp @@ -1247,7 +1247,12 @@ void readINIImpl(const CSimpleIniA & ini, const std::string & configFilePath, Pa { 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) + // Detect the user's private config - matches both the legacy + // ~/.rtabmap/rtabmap.ini and the Windows %LOCALAPPDATA%/rtabmap/rtabmap.ini + // (".rtabmap/rtabmap.ini" also contains "rtabmap/rtabmap.ini"): downgrade-on-save is fine there. + // Accept both forward and backward slashes (native Windows paths). + if(configFilePath.find("rtabmap/rtabmap.ini") != std::string::npos || + configFilePath.find("rtabmap\\rtabmap.ini") != 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 downgraded " diff --git a/corelib/src/SensorCaptureThread.cpp b/corelib/src/SensorCaptureThread.cpp index 6414fb9f..b9fc7bb7 100644 --- a/corelib/src/SensorCaptureThread.cpp +++ b/corelib/src/SensorCaptureThread.cpp @@ -114,7 +114,6 @@ SensorCaptureThread::SensorCaptureThread( _camera(camera), _odomSensor(odomSensor), _lidar(lidar), - _extrinsicsOdomToCamera(extrinsics * CameraModel::opticalRotation()), _odomAsGt(false), _poseTimeOffset(poseTimeOffset), _poseScaleFactor(poseScaleFactor), @@ -153,10 +152,14 @@ SensorCaptureThread::SensorCaptureThread( { if(_camera) { - if(_odomSensor == _camera && _extrinsicsOdomToCamera.isNull()) + if(_odomSensor == _camera && extrinsics.isNull()) { _extrinsicsOdomToCamera.setIdentity(); } + else + { + _extrinsicsOdomToCamera = extrinsics * CameraModel::opticalRotation(); + } UASSERT(!_extrinsicsOdomToCamera.isNull()); UDEBUG("_extrinsicsOdomToCamera=%s", _extrinsicsOdomToCamera.prettyPrint().c_str()); } @@ -492,20 +495,26 @@ void SensorCaptureThread::mainLoop() } } - // Adjust local transform of the camera based on the pose frame + // Adjust local transform of the camera(s) based on the pose frame. The correction + // and odom->camera extrinsics are frame-level, so apply the same prefix to each + // camera while keeping its own local transform (multi-camera supported). if(!data.cameraModels().empty()) { - UASSERT(data.cameraModels().size()==1); - CameraModel model = data.cameraModels()[0]; - model.setLocalTransform(cameraCorrection*_extrinsicsOdomToCamera); - data.setCameraModel(model); + std::vector models = data.cameraModels(); + for(size_t i=0; i models = data.stereoCameraModels(); + for(size_t i=0; i(_camera) != 0 && data.id()>0)) // intermediate nodes could not have image set + if(this->isKilled()) + { + // A kill was requested (e.g. while we were blocked capturing this frame): don't + // publish anything so we never deliver events to handlers that are being torn down. + } + else if(!data.imageCompressed().empty() || !data.imageRaw().empty() || !data.laserScanRaw().empty() || (dynamic_cast(_camera) != 0 && data.id()>0)) // intermediate nodes could not have image set { postUpdate(&data, &info); info.cameraName = _lidar?_lidar->getSerial():_camera->getSerial(); info.timeTotal = totalTime.ticks(); this->post(new SensorEvent(data, info)); } - else if(!this->isKilled()) + else { + // Not killed but no data: end of stream. Signal consumers once, then stop. UWARN("no more data..."); this->kill(); this->post(new SensorEvent()); diff --git a/corelib/src/SensorData.cpp b/corelib/src/SensorData.cpp index 8954af0f..f04dd8e3 100644 --- a/corelib/src/SensorData.cpp +++ b/corelib/src/SensorData.cpp @@ -952,6 +952,22 @@ void SensorData::setFeatures(const std::vector & keypoints, const _descriptors = descriptors; } +void SensorData::setIMU(const IMU & imu) +{ + UASSERT( + uIsFinite(imu.orientation()[0]) && + uIsFinite(imu.orientation()[1]) && + uIsFinite(imu.orientation()[2]) && + uIsFinite(imu.orientation()[3]) && + uIsFinite(imu.angularVelocity()[0]) && + uIsFinite(imu.angularVelocity()[1]) && + uIsFinite(imu.angularVelocity()[2]) && + uIsFinite(imu.linearAcceleration()[0]) && + uIsFinite(imu.linearAcceleration()[1]) && + uIsFinite(imu.linearAcceleration()[2])); + imu_ = imu; +} + unsigned long SensorData::getMemoryUsed() const // Return memory usage in Bytes { return sizeof(SensorData) + diff --git a/corelib/src/camera/CameraRealSense2.cpp b/corelib/src/camera/CameraRealSense2.cpp index a82b514f..8b80984f 100644 --- a/corelib/src/camera/CameraRealSense2.cpp +++ b/corelib/src/camera/CameraRealSense2.cpp @@ -107,10 +107,14 @@ void CameraRealSense2::close() { if(!_sensor.get_active_streams().empty()) { + std::string sensorName = _sensor.supports(RS2_CAMERA_INFO_NAME) ? _sensor.get_info(RS2_CAMERA_INFO_NAME) : "?"; try { + UDEBUG("stop() sensor \"%s\"...", sensorName.c_str()); _sensor.stop(); + UDEBUG("stop() sensor \"%s\" done; close()...", sensorName.c_str()); _sensor.close(); + UDEBUG("close() sensor \"%s\" done", sensorName.c_str()); } catch(const rs2::error & error) { @@ -119,12 +123,15 @@ void CameraRealSense2::close() } } #ifdef WIN32 + UDEBUG("Windows-only: Hardware reset (to avoid freezing when clearing devices)..."); dev_[i].hardware_reset(); // To avoid freezing on some Windows computers in the following destructor // Don't do this on linux (tested on Ubuntu 18.04, realsense v2.41.0): T265 cannot be restarted + UDEBUG("Windows-only: Hardware reset... done"); #endif } UDEBUG("Clearing devices..."); dev_.clear(); + UDEBUG("Clearing devices... done!"); } catch(const rs2::error & error) { @@ -554,10 +561,10 @@ bool CameraRealSense2::init(const std::string & calibrationFolder, const std::st catch(const rs2::error & error) { #ifdef __APPLE__ - UWARN("%s. Is the camera already used with another app? On macOS, accessing a " + UERROR("%s. Is the camera already used with another app? On macOS, accessing a " "RealSense camera requires root privileges, so try running with sudo.", error.what()); #else - UWARN("%s. Is the camera already used with another app?", error.what()); + UERROR("%s. Is the camera already used with another app?", error.what()); #endif } diff --git a/corelib/src/camera/CameraStereoZed.cpp b/corelib/src/camera/CameraStereoZed.cpp index 2fc57a26..d3d5c779 100644 --- a/corelib/src/camera/CameraStereoZed.cpp +++ b/corelib/src/camera/CameraStereoZed.cpp @@ -29,6 +29,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include #ifdef RTABMAP_ZED #include @@ -77,7 +78,7 @@ static cv::Mat slMat2cvMat(sl::Mat& input) { #endif } -Transform zedPoseToTransform(const sl::Pose & pose) +static Transform zedPoseToTransform(const sl::Pose & pose) { return Transform( pose.pose_data.m[0], pose.pose_data.m[1], pose.pose_data.m[2], pose.pose_data.m[3], @@ -86,7 +87,7 @@ Transform zedPoseToTransform(const sl::Pose & pose) } #if ZED_SDK_MAJOR_VERSION < 3 -IMU zedIMUtoIMU(const sl::IMUData & imuData, const Transform & imuLocalTransform) +static IMU zedIMUtoIMU(const sl::IMUData & imuData, const Transform & imuLocalTransform) { sl::Orientation orientation = imuData.pose_data.getOrientation(); @@ -124,7 +125,7 @@ IMU zedIMUtoIMU(const sl::IMUData & imuData, const Transform & imuLocalTransform imuLocalTransform); } #else -IMU zedIMUtoIMU(const sl::SensorsData & sensorData, const Transform & imuLocalTransform) +static IMU zedIMUtoIMU(const sl::SensorsData & sensorData, const Transform & imuLocalTransform) { sl::Orientation orientation = sensorData.imu.pose.getOrientation(); @@ -161,6 +162,23 @@ IMU zedIMUtoIMU(const sl::SensorsData & sensorData, const Transform & imuLocalTr accCov, imuLocalTransform); } + +// sl::SensorsData::imu.is_available only means the camera has an IMU; a returned +// sample can still contain NaN pose/accel/gyro (e.g. before the IMU fusion has +// initialized, or in SVO/STREAM mode). Validate the actual measurements. +static bool isImuValid(const sl::SensorsData & sensorData) +{ + if(!sensorData.imu.is_available) + { + return false; + } + const sl::float3 & acc = sensorData.imu.linear_acceleration; + const sl::float3 & gyr = sensorData.imu.angular_velocity; + const sl::Orientation ori = sensorData.imu.pose.getOrientation(); + return uIsFinite(acc.v[0]) && uIsFinite(acc.v[1]) && uIsFinite(acc.v[2]) && + uIsFinite(gyr.v[0]) && uIsFinite(gyr.v[1]) && uIsFinite(gyr.v[2]) && + uIsFinite(ori.ox) && uIsFinite(ori.oy) && uIsFinite(ori.oz) && uIsFinite(ori.ow); +} #endif class ZedIMUThread: public UThread @@ -217,7 +235,7 @@ private: #else sl::SensorsData sensordata; sl::ERROR_CODE res = zed_->getSensorsData(sensordata, sl::TIME_REFERENCE::CURRENT); - if(res == sl::ERROR_CODE::SUCCESS && sensordata.imu.is_available) + if(res == sl::ERROR_CODE::SUCCESS && isImuValid(sensordata)) { camera_->postInterIMUPublic(zedIMUtoIMU(sensordata, imuLocalTransform_), double(sensordata.imu.timestamp.getNanoseconds())/10e8); } @@ -251,6 +269,56 @@ int CameraStereoZed::sdkVersion() #endif } +#ifdef RTABMAP_ZED +static void backwardCompatibility(int & resolution, int & quality) +{ + // -1 = AUTO, 0=HD4K 1=QHDPLUS 2=HD2K 3=HD1536 4=HD1080 5=HD1200 6=HD720 7=SVGA 8=VGA 9=XVGA 10=TXVGA +#if ZED_SDK_MAJOR_VERSION < 4 + // Zed3 Supported: 2=HD2K 4=HD1080 6=HD720 8=VGA + if(resolution == 0 || resolution == 1) // 0=HD4K 1=QHDPLUS + { + UWARN("Zed SDK v3 doesn't support HD4K and QHDPLUS, setting HD2K."); + resolution = 2; // 2=HD2K + } + if(resolution == 3 || resolution == 5) // 3=HD1536 5=HD1200 + { + UWARN("Zed SDK v3 doesn't support HD1536 and HD1200, setting HD1080."); + resolution = 4; // 4=HD1080 + } + if(resolution == 7 || resolution >=9) // 7=SVGA 9=XVGA 10=TXVGA + { + UWARN("Zed SDK v3 doesn't support SVGA, XVGA and TXVGA, setting VGA."); + resolution = 8; // 8=VGA + } + if(quality == 4) { // 4=NEURAL_LIGHT 6=NEURAL_PLUS + UWARN("Zed SDK v3 doesn't support NEURAL_LIGHT and NEURAL PLUS, setting NEURAL."); + quality = 5; // NEURAL + } +#else + if(resolution == -1) + { + resolution = int(sl::RESOLUTION::AUTO); // AUTO + } +#if ZED_SDK_MAJOR_VERSION < 5 + // Zed4 Supported: 0=HD4K 1=QHDPLUS 2=HD2K 4=HD1080 5=HD1200 6=HD720 7=SVGA 8=VGA + if(resolution == 3) // 3=HD1536 + { + UWARN("Zed SDK v4 doesn't support HD1536, setting HD1200."); + resolution_ = 5; // 5=HD1200 + } + if(resolution >=9) // 9=XVGA 10=TXVGA + { + UWARN("Zed SDK v4 doesn't support XVGA and TXVGA, setting VGA."); + resolution = 8; // 8=VGA + } + if(quality > 5) { // NEURAL_PLUS + UWARN("Zed SDK v4 doesn't support NEURAL_LIGHT and NEURAL PLUS, setting NEURAL."); + quality = 5; // NEURAL + } +#endif +#endif +} +#endif CameraStereoZed::CameraStereoZed( int deviceId, @@ -285,29 +353,8 @@ CameraStereoZed::CameraStereoZed( { UDEBUG(""); #ifdef RTABMAP_ZED -#if ZED_SDK_MAJOR_VERSION < 4 - if(resolution_ == 1 || resolution_ == 2) // HD2K, HD1080 - { - resolution_ -= 1; // HD2K=0, HD1080=1 - } - if(resolution_ == 3) // HD1200 - { - resolution_ = 1; // HD1080=1 - } - if(resolution_ == 4 || resolution_ == -1) - { - resolution_ = 2; // HD720=2 - } - else if(resolution_ == 5 || resolution_ == 6) // SVGA, VGA - { - resolution_ = 3; // VGA=3 - } -#else // ZED=4 - if(resolution_ == -1) - { - resolution_ = int(sl::RESOLUTION::AUTO); // AUTO - } -#endif + + backwardCompatibility(resolution_, quality_); #if ZED_SDK_MAJOR_VERSION < 3 UASSERT(resolution_ >= sl::RESOLUTION_HD2K && resolution_ = sl::RESOLUTION_HD2K && resolution_ = sl::DEPTH_MODE_NONE && quality_ = 5 + sl::DEPTH_MODE depthMode = (sl::DEPTH_MODE)quality; + if(depthMode == sl::DEPTH_MODE::NEURAL_LIGHT || + depthMode == sl::DEPTH_MODE::NEURAL || + depthMode == sl::DEPTH_MODE::NEURAL_PLUS) + { + sl::AI_MODELS aiModel = + depthMode == sl::DEPTH_MODE::NEURAL_LIGHT ? sl::AI_MODELS::NEURAL_LIGHT_DEPTH : + depthMode == sl::DEPTH_MODE::NEURAL_PLUS ? sl::AI_MODELS::NEURAL_PLUS_DEPTH : + sl::AI_MODELS::NEURAL_DEPTH; + sl::AI_Model_status status = sl::checkAIModelStatus(aiModel); + if(!status.downloaded || !status.optimized) + { + return uFormat("The selected ZED NEURAL depth model is not ready yet (downloaded=%s, " + "optimized=%s): the first start may take significantly longer while the " + "ZED SDK downloads and/or optimizes the model for your GPU. Subsequent " + "starts will be faster.", + status.downloaded?"true":"false", status.optimized?"true":"false"); + } + } +#endif + return std::string(); +} + bool CameraStereoZed::init(const std::string & calibrationFolder, const std::string & cameraName) { UDEBUG(""); @@ -463,9 +538,35 @@ bool CameraStereoZed::init(const std::string & calibrationFolder, const std::str r = zed_->open(param); } +#if ZED_SDK_MAJOR_VERSION >= 3 + // CORRUPTED_SDK_INSTALLATION on open() is typically a NEURAL depth mode whose optional + // neural/TensorRT runtime files aren't installed. Give a clear, actionable error. + if(r == sl::ERROR_CODE::CORRUPTED_SDK_INSTALLATION && +#if ZED_SDK_MAJOR_VERSION >= 5 + param.depth_mode >= sl::DEPTH_MODE::NEURAL_LIGHT) +#else + param.depth_mode >= sl::DEPTH_MODE::NEURAL) +#endif + { + UERROR("ZED open() returned \"%s\": the optional NEURAL/TensorRT runtime files are " + "likely missing. Install ZED SDK %d.%d, or select a non-NEURAL depth mode " + "(e.g. PERFORMANCE).", toString(r).c_str(), ZED_SDK_MAJOR_VERSION, ZED_SDK_MINOR_VERSION); + // Do NOT delete zed_ here: after a CORRUPTED_SDK_INSTALLATION open failure (NEURAL depth + // mode selected but the TensorRT/neural runtime isn't installed) the ZED SDK is left in a + // bad state and ~sl::Camera() crashes inside sl_zed64.dll. Leak the object (one-time, + // terminal error path) so we fail gracefully with the message above instead of crashing. + zed_ = 0; + return false; + } +#endif + if(r!=sl::ERROR_CODE::SUCCESS) { +#if ZED_SDK_MAJOR_VERSION >= 4 + UERROR("Camera initialization failed: \"%s\": %s", toString(r).c_str(), toVerbose(r).c_str()); +#else UERROR("Camera initialization failed: \"%s\"", toString(r).c_str()); +#endif delete zed_; zed_ = 0; return false; @@ -502,7 +603,11 @@ bool CameraStereoZed::init(const std::string & calibrationFolder, const std::str #endif if(r!=sl::ERROR_CODE::SUCCESS) { +#if ZED_SDK_MAJOR_VERSION >= 4 + UERROR("Camera tracking initialization failed: \"%s\": %s", toString(r).c_str(), toVerbose(r).c_str()); +#else UERROR("Camera tracking initialization failed: \"%s\"", toString(r).c_str()); +#endif } } @@ -731,16 +836,24 @@ SensorData CameraStereoZed::captureImage(SensorCaptureInfo * info) res = zed_->grab(rparam); timestamp = zed_->getTimestamp(sl::TIME_REFERENCE::IMAGE); - // If the sensor supports IMU, wait IMU to be available before sending data. + // If the sensor supports IMU, wait for IMU to be available before sending data. if(imuPublishingThread_ == 0 && !imuLocalTransform_.isNull()) { sl::SensorsData imudatatmp; res = zed_->getSensorsData(imudatatmp, sl::TIME_REFERENCE::IMAGE); - imuReceived = res == sl::ERROR_CODE::SUCCESS && imudatatmp.imu.is_available && imudatatmp.imu.timestamp.getNanoseconds() != 0; + imuReceived = res == sl::ERROR_CODE::SUCCESS && isImuValid(imudatatmp) && imudatatmp.imu.timestamp.getNanoseconds() != 0; } } while(src_ == CameraVideo::kUsbDevice && (res!=sl::ERROR_CODE::SUCCESS || !imuReceived) && timer.elapsed() < 2.0); + // If no valid IMU arrived within the 2 sec startup window, null the IMU transform so + // we don't re-wait 2 sec on every subsequent frame (camera likely has no working IMU). + if(imuPublishingThread_ == 0 && !imuLocalTransform_.isNull() && !imuReceived) + { + UWARN("No valid IMU received within 2 sec; ignoring IMU for the rest of this session."); + imuLocalTransform_.setNull(); + } + if(res==sl::ERROR_CODE::SUCCESS) #endif { @@ -794,7 +907,7 @@ SensorData CameraStereoZed::captureImage(SensorCaptureInfo * info) #endif } - if(imuPublishingThread_ == 0) + if(imuPublishingThread_ == 0 && !imuLocalTransform_.isNull()) { #if ZED_SDK_MAJOR_VERSION < 3 sl::IMUData imudata; @@ -803,7 +916,7 @@ SensorData CameraStereoZed::captureImage(SensorCaptureInfo * info) #else sl::SensorsData imudata; res = zed_->getSensorsData(imudata, sl::TIME_REFERENCE::IMAGE); - if(res == sl::ERROR_CODE::SUCCESS && imudata.imu.is_available) + if(res == sl::ERROR_CODE::SUCCESS && isImuValid(imudata)) #endif { //ZED-Mini @@ -876,7 +989,11 @@ SensorData CameraStereoZed::captureImage(SensorCaptureInfo * info) } else if(src_ == CameraVideo::kUsbDevice) { +#if ZED_SDK_MAJOR_VERSION >= 4 + UERROR("CameraStereoZed: Failed to grab images after 2 seconds! (%s: %s)", toString(res).c_str(), toVerbose(res).c_str()); +#else UERROR("CameraStereoZed: Failed to grab images after 2 seconds!"); +#endif } else { diff --git a/corelib/src/lidar/LidarVLP16.cpp b/corelib/src/lidar/LidarVLP16.cpp index 2fa758a0..a626e65b 100644 --- a/corelib/src/lidar/LidarVLP16.cpp +++ b/corelib/src/lidar/LidarVLP16.cpp @@ -34,6 +34,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #define VLP_DUAL_MODE 0x39 #endif +// RTABMAP_VLP16_USE_CUSTOM_SOCKET is defined (Apple only, and toggleable) in +// LidarVLP16.h. When set, network mode uses our own single-threaded UDP receive +// loop; otherwise start()/stop()/isRunning() delegate to pcl::VLPGrabber. + namespace rtabmap { /** @brief Function used to check that hour assigned to timestamp in conversion is @@ -92,8 +96,17 @@ LidarVLP16::LidarVLP16( startSweepTimeHost_(0), organized_(organized), useHostTime_(false), - stampLast_(stampLast) + stampLast_(stampLast), + networkMode_(false), + port_(0), + receivedScan_(false) +#ifdef RTABMAP_VLP16_USE_CUSTOM_SOCKET + ,socket_(0) + ,readThread_(0) + ,terminate_(false) +#endif { + // ipAddress_ unused in PCAP mode (default-constructed / unspecified). UDEBUG("Using PCAP file \"%s\"", pcapFile.c_str()); } LidarVLP16::LidarVLP16( @@ -111,7 +124,19 @@ LidarVLP16::LidarVLP16( startSweepTimeHost_(0), organized_(organized), useHostTime_(useHostTime), - stampLast_(stampLast) + stampLast_(stampLast), + // networkMode_ = live network capture (vs PCAP playback), on all platforms; + // it drives the "no data received" warning below. The custom socket path it + // also selects in start()/stop() is Apple-only (RTABMAP_VLP16_USE_CUSTOM_SOCKET). + networkMode_(true), + ipAddress_(ipAddress), + port_(port), + receivedScan_(false) +#ifdef RTABMAP_VLP16_USE_CUSTOM_SOCKET + ,socket_(0) + ,readThread_(0) + ,terminate_(false) +#endif { UDEBUG("Using network lidar with IP=%s port=%d", ipAddress.to_string().c_str(), port); } @@ -129,6 +154,132 @@ void LidarVLP16::setOrganized(bool enable) organized_ = true; } +#ifdef RTABMAP_VLP16_USE_CUSTOM_SOCKET +void LidarVLP16::start() +{ + if(networkMode_) + { + if(readThread_ != 0) + { + // already running + return; + } + + terminate_ = false; + // ipAddress_ is the LOCAL interface to listen on (as in pcl::HDLGrabber), + // not the sensor's address. Binding to a specific local IP scopes + // reception to that NIC on a multi-homed host. If it is unspecified + // (0.0.0.0) or not a local address (e.g. the sensor's IP was entered by + // mistake), fall back to listening on all interfaces, like pcl::HDLGrabber. + try + { + boost::asio::ip::udp::endpoint endpoint( + ipAddress_.is_unspecified() ? boost::asio::ip::address(boost::asio::ip::address_v4::any()) : ipAddress_, + port_); + // Unlike pcl::HDLGrabber, we read and process packets in a single + // thread instead of a producer/consumer queue. A large receive buffer + // lets the kernel hold a backlog (~800 VLP16 packets here) so a brief + // stall in toPointClouds() doesn't drop packets. + const int receiveBufferSize = 1024 * 1024; // 1 MB + try + { + socket_ = new boost::asio::ip::udp::socket(ioContext_); + socket_->open(boost::asio::ip::udp::v4()); + socket_->set_option(boost::asio::socket_base::reuse_address(true)); + socket_->set_option(boost::asio::socket_base::receive_buffer_size(receiveBufferSize)); + socket_->bind(endpoint); + } + catch(const std::exception & e) + { + UWARN("Could not bind VLP16 socket to local address %s (%s); " + "falling back to listening on all interfaces (0.0.0.0).", + endpoint.address().to_string().c_str(), e.what()); + delete socket_; + socket_ = new boost::asio::ip::udp::socket(ioContext_); + socket_->open(boost::asio::ip::udp::v4()); + socket_->set_option(boost::asio::socket_base::reuse_address(true)); + socket_->set_option(boost::asio::socket_base::receive_buffer_size(receiveBufferSize)); + socket_->bind(boost::asio::ip::udp::endpoint(boost::asio::ip::address_v4::any(), port_)); + } + } + catch(const std::exception & e) + { + UERROR("Failed to bind to VLP16 UDP port %d: %s", (int)port_, e.what()); + delete socket_; + socket_ = 0; + return; + } + receivedScan_ = false; + readThread_ = new std::thread(&LidarVLP16::readPackets, this); + return; + } + // PCAP mode: use the base grabber. + pcl::VLPGrabber::start(); +} + +void LidarVLP16::stop() +{ + if(networkMode_) + { + terminate_ = true; + if(socket_ != 0) + { + // Closing the socket unblocks receive_from in the read thread. Because + // that call uses the non-throwing (error_code) overload, the EBADF that + // BSD/macOS returns here surfaces as an error code instead of an + // exception, so the thread exits cleanly rather than aborting. + boost::system::error_code ec; + socket_->close(ec); + } + if(readThread_ != 0) + { + readThread_->join(); + delete readThread_; + readThread_ = 0; + } + delete socket_; + socket_ = 0; + return; + } + pcl::VLPGrabber::stop(); +} + +bool LidarVLP16::isRunning() const +{ + if(networkMode_) + { + return readThread_ != 0; + } + return pcl::VLPGrabber::isRunning(); +} + +void LidarVLP16::readPackets() +{ + std::uint8_t data[1500]; + boost::asio::ip::udp::endpoint sender; + while(!terminate_) + { + boost::system::error_code ec; + std::size_t length = socket_->receive_from(boost::asio::buffer(data, sizeof(data)), sender, 0, ec); + if(ec) + { + // On stop() the socket is closed under us (bad_descriptor / + // operation_aborted); only warn if it wasn't an expected shutdown. + if(!terminate_) + { + UWARN("VLP16 socket receive error: %s", ec.message().c_str()); + } + break; + } + // VLP-16 data packets are 1206 bytes; ignore anything shorter. + if(length >= 1206) + { + toPointClouds(reinterpret_cast(data)); + } + } +} +#endif // RTABMAP_VLP16_USE_CUSTOM_SOCKET + bool LidarVLP16::init(const std::string &, const std::string &) { UDEBUG("Init lidar"); @@ -365,8 +516,22 @@ SensorData LidarVLP16::captureData(SensorCaptureInfo * info) { data = lastScan_; lastScan_ = SensorData(); + receivedScan_ = true; } } + else if(networkMode_ && !receivedScan_) + { + // No packet has ever arrived: most likely the sensor isn't sending data + // to this computer. Point the user at the configured listening endpoint. + UWARN("Did not receive any VLP16 data packets for the past 5 seconds. " + "This computer is listening on %s:%d (%s). Make sure the LiDAR is " + "powered on and configured to send its data to this computer's IP " + "address on UDP port %d (set the data destination host in the " + "LiDAR's own web/configuration interface).", + ipAddress_.to_string().c_str(), (int)port_, + ipAddress_.is_unspecified() ? "all local interfaces" : "this interface only", + (int)port_); + } else { UWARN("Did not receive any scans for the past 5 seconds."); diff --git a/guilib/include/rtabmap/gui/PreferencesDialog.h b/guilib/include/rtabmap/gui/PreferencesDialog.h index 5fef4cba..0696ca23 100644 --- a/guilib/include/rtabmap/gui/PreferencesDialog.h +++ b/guilib/include/rtabmap/gui/PreferencesDialog.h @@ -130,6 +130,12 @@ public: virtual QString getIniFilePath() const; virtual QString getTmpIniFilePath() const; + // When running under sudo, Qt's QSettings replaces the ini with a + // root-owned file (write-temp + rename), so later non-root runs can no + // longer save preferences. Restore the invoking user's ownership (from + // SUDO_UID/SUDO_GID) on the config file and its directory. No-op on Windows + // and when not running as root. Best-effort (ignores failures). + static void restoreConfigOwnership(const QString & filePath); void init(const QString & iniFilePath = ""); void setCurrentPanelToSource(); virtual QString getDefaultWorkingDirectory() const; @@ -263,6 +269,9 @@ public: PreferencesDialog::Src getSourceDriver() const; QString getSourceDriverStr() const; QString getSourceDevice() const; + // Non-empty warning to show before opening the selected source when init() is expected to be + // unusually slow; empty otherwise. + QString getSourceInitWarningMsg() const; PreferencesDialog::Src getLidarSourceDriver() const; PreferencesDialog::Src getOdomSourceDriver() const; diff --git a/guilib/src/DatabaseViewer.cpp b/guilib/src/DatabaseViewer.cpp index 63afea48..5ef206f6 100644 --- a/guilib/src/DatabaseViewer.cpp +++ b/guilib/src/DatabaseViewer.cpp @@ -39,6 +39,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include +#include +#include #include #include #include @@ -583,12 +586,33 @@ QString DatabaseViewer::getIniFilePath() const { return iniFilePath_; } +#ifdef WIN32 + // Windows: store settings in the standard per-user config location (%LOCALAPPDATA%\rtabmap) + // instead of a Unix-style dotfile in the home root. A fixed "rtabmap" folder (not the app name) + // is used so the main GUI and DatabaseViewer share the same rtabmap.ini. Other platforms keep + // ~/.rtabmap for consistency. + QString privatePath = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + "/rtabmap"; +#else QString privatePath = QDir::homePath() + "/.rtabmap"; +#endif if(!QDir(privatePath).exists()) { - QDir::home().mkdir(".rtabmap"); + QDir().mkpath(privatePath); } - return privatePath + "/rtabmap.ini"; + QString iniPath = privatePath + "/rtabmap.ini"; +#ifdef WIN32 + // One-time migration: if there's no ini at the new location but the legacy ~/.rtabmap/rtabmap.ini + // exists, copy it over so users keep their existing settings. + if(!QFile::exists(iniPath)) + { + QString legacyIni = QDir::homePath() + "/.rtabmap/rtabmap.ini"; + if(QFile::exists(legacyIni)) + { + QFile::copy(legacyIni, iniPath); + } + } +#endif + return iniPath; } void DatabaseViewer::readSettings() diff --git a/guilib/src/MainWindow.cpp b/guilib/src/MainWindow.cpp index 8daf26b2..10b94fda 100644 --- a/guilib/src/MainWindow.cpp +++ b/guilib/src/MainWindow.cpp @@ -78,6 +78,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include +#include #include #include #include @@ -958,7 +960,7 @@ bool MainWindow::handleEvent(UEvent* anEvent) else { Q_EMIT cameraInfoReceived(sensorEvent->info()); - if (_odomThread == 0 && (_sensorCapture->odomProvided()) && _preferencesDialog->isRGBDMode()) + if (_odomThread == 0 && _sensorCapture && _sensorCapture->odomProvided() && _preferencesDialog->isRGBDMode()) { OdometryInfo odomInfo; odomInfo.reg.covariance = sensorEvent->info().odomCovariance; @@ -5528,6 +5530,10 @@ void MainWindow::saveConfigGUI() _preferencesDialog->saveSettings(); this->saveFigures(); this->setWindowModified(false); + + // If running under sudo, the above writes recreate rtabmap.ini as root; + // restore ownership so non-root runs can still save preferences. + PreferencesDialog::restoreConfigOwnership(_preferencesDialog->getIniFilePath()); } void MainWindow::newDatabase() @@ -5932,6 +5938,31 @@ void MainWindow::startDetection() Camera * camera = 0; Lidar * lidar = 0; + // Creating the sensors below opens the devices (createLidar/createCamera/createOdomSensor -> + // init(), a few seconds for ZED/RealSense) on the GUI thread; show a busy dialog (min==max==0 + // => indeterminate) so the window isn't just frozen. Hidden once all sensors are created below. + QString startLabel = tr("Starting sensor..."); + QString initWarn = _preferencesDialog->getSourceInitWarningMsg(); + if(!initWarn.isEmpty()) + { + startLabel += "\n\n" + initWarn; + } + QProgressDialog progress(startLabel, QString(), 0, 0, this); + if(!initWarn.isEmpty()) + { + QLabel * wrapLabel = new QLabel(startLabel); + wrapLabel->setWordWrap(true); + progress.setLabel(wrapLabel); // QProgressDialog takes ownership + progress.setMinimumWidth(450); + } + progress.setWindowModality(Qt::ApplicationModal); + progress.setCancelButton(0); + progress.setMinimumDuration(0); + progress.setValue(0); + progress.show(); + QApplication::processEvents(); + QApplication::processEvents(); // make sure it is drawn + if(_preferencesDialog->getLidarSourceDriver() != PreferencesDialog::kSrcUndef) { lidar = _preferencesDialog->createLidar(); @@ -5989,6 +6020,8 @@ void MainWindow::startDetection() } } + progress.hide(); // all sensors created/opened + _sensorCapture = new SensorCaptureThread(lidar, camera, odomSensor, extrinsics, poseTimeOffset, scaleFactor, waitTime, parameters); _sensorCapture->setOdomAsGroundTruth(_preferencesDialog->isOdomSensorAsGt()); @@ -6229,6 +6262,24 @@ void MainWindow::stopDetection() } ULOGGER_DEBUG(""); + + // Closing the camera runs on the GUI thread in "delete _sensorCapture" below (via + // ~SensorCaptureThread -> ~Camera::close()) and can block for a while - e.g. the first 2-3 + // RealSense closes per launch stall ~20s in the Motion Module stop() (librealsense warm-up). + // Show a busy dialog (min==max==0 => indeterminate) so the window isn't just frozen. It is + // declared here so it stays visible across the joins/deletes and closes on scope exit. + QProgressDialog progress(tr("Stopping sensor..."), QString(), 0, 0, this); + if(_sensorCapture) + { + progress.setWindowModality(Qt::ApplicationModal); + progress.setCancelButton(0); + progress.setMinimumDuration(0); + progress.setValue(0); + progress.show(); + QApplication::processEvents(); + QApplication::processEvents(); // make sure it is drawn + } + // kill the processes if(_imuThread) { diff --git a/guilib/src/PreferencesDialog.cpp b/guilib/src/PreferencesDialog.cpp index 3a824ed4..3be2f4d0 100644 --- a/guilib/src/PreferencesDialog.cpp +++ b/guilib/src/PreferencesDialog.cpp @@ -39,9 +39,20 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include +#include +#include +#include #include #include +#ifndef _WIN32 +#include // geteuid, chown +#include +#include // atoi, getenv +#include +#include // strerror +#endif + #include #include #include @@ -49,6 +60,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include +#include +#include #include #include #include @@ -437,12 +451,29 @@ PreferencesDialog::PreferencesDialog(QWidget * parent) : _ui->comboBox_cameraStereo->setItemData(kSrcStereoZed - kSrcStereo, 0, Qt::UserRole - 1); _ui->comboBox_odom_sensor->setItemData(2, 0, Qt::UserRole - 1); } - else if(CameraStereoZed::sdkVersion() < 4) + else { - _ui->comboBox_stereoZed_resolution->setItemData(1, 0, Qt::UserRole - 1); - _ui->comboBox_stereoZed_resolution->setItemData(4, 0, Qt::UserRole - 1); - _ui->comboBox_stereoZed_resolution->setItemData(6, 0, Qt::UserRole - 1); - _ui->comboBox_stereoZed_quality->setItemData(3, 0, Qt::UserRole - 1); + if(CameraStereoZed::sdkVersion() < 5) + { + // SDK 4.X + _ui->comboBox_stereoZed_quality->setItemData(5, 0, Qt::UserRole - 1); // NEURAL_ULTRA + _ui->comboBox_stereoZed_resolution->setItemData(4, 0, Qt::UserRole - 1); //HD1536 + _ui->comboBox_stereoZed_resolution->setItemData(10, 0, Qt::UserRole - 1); //XVGA + _ui->comboBox_stereoZed_resolution->setItemData(11, 0, Qt::UserRole - 1); //TXGA + } + if(CameraStereoZed::sdkVersion() < 4) + { + // SDK 3.X + _ui->comboBox_stereoZed_quality->setItemData(3, 0, Qt::UserRole - 1); // NEURAL_LIGHT + _ui->comboBox_stereoZed_resolution->setItemData(1, 0, Qt::UserRole - 1); // HD4K + _ui->comboBox_stereoZed_resolution->setItemData(2, 0, Qt::UserRole - 1); // QHDPLUS + _ui->comboBox_stereoZed_resolution->setItemData(6, 0, Qt::UserRole - 1); // HD1200 + _ui->comboBox_stereoZed_resolution->setItemData(8, 0, Qt::UserRole - 1); // SVGA + } + if(CameraStereoZed::sdkVersion() < 3) + { + _ui->comboBox_stereoZed_quality->setItemData(4, 0, Qt::UserRole - 1); // NEURAL + } } if (!CameraStereoTara::available()) { @@ -985,7 +1016,7 @@ PreferencesDialog::PreferencesDialog(QWidget * parent) : connect(_ui->lineEdit_vlp16_pcap_path, SIGNAL(textChanged(const QString &)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->spinBox_vlp16_ip1, SIGNAL(valueChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->spinBox_vlp16_ip2, SIGNAL(valueChanged(int)), this, SLOT(makeObsoleteSourcePanel())); - connect(_ui->spinBox_vlp16_ip2, SIGNAL(valueChanged(int)), this, SLOT(makeObsoleteSourcePanel())); + connect(_ui->spinBox_vlp16_ip3, SIGNAL(valueChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->spinBox_vlp16_ip4, SIGNAL(valueChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->spinBox_vlp16_port, SIGNAL(valueChanged(int)), this, SLOT(makeObsoleteSourcePanel())); connect(_ui->checkBox_vlp16_organized, SIGNAL(stateChanged(int)), this, SLOT(makeObsoleteSourcePanel())); @@ -2440,10 +2471,10 @@ void PreferencesDialog::resetSettings(QGroupBox * groupBox) _ui->lineEdit_lidar_local_transform->setText("0 0 0 0 0 0"); _ui->lineEdit_vlp16_pcap_path->clear(); - _ui->spinBox_vlp16_ip1->setValue(192); - _ui->spinBox_vlp16_ip2->setValue(168); - _ui->spinBox_vlp16_ip3->setValue(1); - _ui->spinBox_vlp16_ip4->setValue(201); + _ui->spinBox_vlp16_ip1->setValue(0); + _ui->spinBox_vlp16_ip2->setValue(0); + _ui->spinBox_vlp16_ip3->setValue(0); + _ui->spinBox_vlp16_ip4->setValue(0); _ui->spinBox_vlp16_port->setValue(2368); _ui->checkBox_vlp16_organized->setChecked(false); _ui->checkBox_vlp16_hostTime->setChecked(true); @@ -2559,12 +2590,33 @@ QString PreferencesDialog::getWorkingDirectory() const QString PreferencesDialog::getIniFilePath() const { +#ifdef WIN32 + // Windows: store settings in the standard per-user config location (%LOCALAPPDATA%\rtabmap) + // instead of a Unix-style dotfile in the home root. A fixed "rtabmap" folder (not the app name) + // is used so the main GUI and DatabaseViewer share the same rtabmap.ini. Other platforms keep + // ~/.rtabmap for consistency. + QString privatePath = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + "/rtabmap"; +#else QString privatePath = QDir::homePath() + "/.rtabmap"; +#endif if(!QDir(privatePath).exists()) { - QDir::home().mkdir(".rtabmap"); + QDir().mkpath(privatePath); } - return privatePath + "/rtabmap.ini"; + QString iniPath = privatePath + "/rtabmap.ini"; +#ifdef WIN32 + // One-time migration: if there's no ini at the new location but the legacy ~/.rtabmap/rtabmap.ini + // exists, copy it over so users keep their existing settings. + if(!QFile::exists(iniPath)) + { + QString legacyIni = QDir::homePath() + "/.rtabmap/rtabmap.ini"; + if(QFile::exists(legacyIni)) + { + QFile::copy(legacyIni, iniPath); + } + } +#endif + return iniPath; } QString PreferencesDialog::getTmpIniFilePath() const @@ -2572,6 +2624,41 @@ QString PreferencesDialog::getTmpIniFilePath() const return getIniFilePath()+".tmp"; } +void PreferencesDialog::restoreConfigOwnership(const QString & filePath) +{ +#ifndef _WIN32 + // Only relevant when the process is effectively root (e.g. launched with + // sudo). getenv("SUDO_UID"/"SUDO_GID") give the invoking user. + if(geteuid() == 0) + { + const char * sudoUid = getenv("SUDO_UID"); + const char * sudoGid = getenv("SUDO_GID"); + if(sudoUid && sudoGid) + { + uid_t uid = (uid_t)atoi(sudoUid); + gid_t gid = (gid_t)atoi(sudoGid); + // Restore the config file and its containing directory so the user + // can still write preferences without sudo afterwards. + if(!filePath.isEmpty() && QFile::exists(filePath)) + { + if(chown(filePath.toStdString().c_str(), uid, gid) != 0) + { + UWARN("Could not restore ownership of \"%s\" to uid=%d (%s).", + filePath.toStdString().c_str(), (int)uid, strerror(errno)); + } + } + QString dir = QFileInfo(filePath).absolutePath(); + if(!dir.isEmpty()) + { + chown(dir.toStdString().c_str(), uid, gid); + } + } + } +#else + Q_UNUSED(filePath); +#endif +} + void PreferencesDialog::loadConfigFrom() { QString path = QFileDialog::getOpenFileName(this, tr("Load settings..."), this->getWorkingDirectory(), "*.ini"); @@ -3242,6 +3329,9 @@ void PreferencesDialog::writeSettings(const QString & filePath) uInsert(_parameters, _modifiedParameters); // update cached parameters _modifiedParameters.clear(); _obsoletePanels = kPanelDummy; + + // Keep the ini writable by the invoking user even if we are running as root. + restoreConfigOwnership(filePath.isEmpty() ? getIniFilePath() : filePath); } void PreferencesDialog::writeGuiSettings(const QString & filePath) const @@ -6635,6 +6725,16 @@ QString PreferencesDialog::getSourceDriverStr() const return ""; } +QString PreferencesDialog::getSourceInitWarningMsg() const +{ + if(getSourceDriver() == kSrcStereoZed) + { + return QString::fromStdString( + CameraStereoZed::getNeuralModelWarning(_ui->comboBox_stereoZed_quality->currentIndex())); + } + return QString(); +} + QString PreferencesDialog::getSourceDevice() const { return _ui->lineEdit_sourceDevice->text(); @@ -6820,11 +6920,11 @@ double PreferencesDialog::getSourceScanForceGroundNormalsUp() const Camera * PreferencesDialog::createCamera(bool useRawImages, bool useColor) { return createCamera( - this->getSourceDriver(), - _ui->lineEdit_sourceDevice->text(), - _ui->lineEdit_calibrationFile->text(), - useRawImages, - useColor, + this->getSourceDriver(), + _ui->lineEdit_sourceDevice->text(), + _ui->lineEdit_calibrationFile->text(), + useRawImages, + useColor, false, false); } @@ -7728,7 +7828,30 @@ void PreferencesDialog::setSLAMMode(bool enabled) void PreferencesDialog::testOdometry() { + QString startLabel = tr("Starting camera..."); + QString initWarn = getSourceInitWarningMsg(); + if(!initWarn.isEmpty()) + { + startLabel += "\n\n" + initWarn; + } + QProgressDialog progress(startLabel, QString(), 0, 0, this); + if(!initWarn.isEmpty()) + { + QLabel * wrapLabel = new QLabel(startLabel); + wrapLabel->setWordWrap(true); + progress.setLabel(wrapLabel); // QProgressDialog takes ownership + progress.setMinimumWidth(450); + } + progress.setWindowModality(Qt::ApplicationModal); + progress.setCancelButton(0); + progress.setMinimumDuration(0); + progress.setValue(0); + progress.show(); + QApplication::processEvents(); + QApplication::processEvents(); // make sure it is drawn + Camera * camera = this->createCamera(); + progress.hide(); if(!camera) { return; @@ -7776,12 +7899,13 @@ void PreferencesDialog::testOdometry() _ui->odom_dataBufferSize->value()); odomThread.registerToEventsManager(); + // parent = 0 (not 'this'): see testCamera() - avoids the nested-modality crash. OdometryViewer * odomViewer = new OdometryViewer(10, _ui->spinBox_decimation_odom->value(), 0.0f, _ui->doubleSpinBox_maxDepth_odom->value(), this->getOdomQualityWarnThr(), - this, + 0, this->getAllParameters()); odomViewer->setWindowTitle(tr("Odometry viewer")); odomViewer->resize(1280, 480+QPushButton().minimumHeight()); @@ -7843,25 +7967,79 @@ void PreferencesDialog::testOdometry() } odomViewer->exec(); - delete odomViewer; + UDEBUG("Dialog closed, stopping sensor..."); + + // Tear down the pipes first so no more events are routed to the threads/viewer being + // destroyed, then stop the threads, then delete the viewer. This avoids delivering + // events to a handler that is being torn down. + UEventsManager::removePipe(&cameraThread, &odomThread, "SensorEvent"); + if(imuThread) + { + UEventsManager::removePipe(imuThread, &odomThread, "IMUEvent"); + } + UEventsManager::removePipe(&odomThread, odomViewer, "OdometryEvent"); + UEventsManager::removePipe(odomViewer, &odomThread, "OdometryResetEvent"); if(imuThread) { imuThread->join(true); - delete imuThread; } + + // Reuse the same dialog for the close. The device close() runs in cameraThread's destructor + // at function scope end (not in join()), so 'progress' stays visible across it. On Windows + // the first 2-3 RealSense closes per launch stall ~20s in the Motion Module stop(). + progress.setLabelText(tr("Closing camera...")); + progress.show(); + QApplication::processEvents(); + QApplication::processEvents(); // make sure it is drawn cameraThread.join(true); odomThread.join(true); + + // deleteLater() (not delete): see testCamera() - avoids a dangling OpenGL platform + // window that crashes in QWindowsWindow::alertWindow when Preferences later closes. + odomViewer->deleteLater(); + if(imuThread) + { + delete imuThread; + } } void PreferencesDialog::testCamera() { - CameraViewer * window = new CameraViewer(this, this->getAllParameters()); + // Not parented to 'this': the Preferences dialog is itself application-modal, and making + // the viewer a modal *child* of it (nested modality) with an OpenGL/VTK native window + // crashes Qt (QWindowsWindow::alertWindow, this==nullptr) when Preferences later closes. + // exec() below still makes the viewer application-modal, so interaction stays blocked. + CameraViewer * window = new CameraViewer(nullptr, this->getAllParameters()); window->setWindowTitle(tr("Camera viewer")); window->resize(1280, 480+QPushButton().minimumHeight()); window->registerToEventsManager(); + QString startLabel = tr("Starting camera..."); + QString initWarn = getSourceInitWarningMsg(); + if(!initWarn.isEmpty()) + { + startLabel += "\n\n" + initWarn; + } + QProgressDialog progress(startLabel, QString(), 0, 0, this); + if(!initWarn.isEmpty()) + { + QLabel * wrapLabel = new QLabel(startLabel); + wrapLabel->setWordWrap(true); + progress.setLabel(wrapLabel); // QProgressDialog takes ownership + progress.setMinimumWidth(450); + } + progress.setWindowModality(Qt::ApplicationModal); + progress.setCancelButton(0); + progress.setMinimumDuration(0); + progress.setValue(0); + progress.show(); + QApplication::processEvents(); + QApplication::processEvents(); // make sure it is drawn + + // createCamera() init()s the device on the GUI thread (required by ZED) and takes a few seconds. Camera * camera = this->createCamera(); + progress.hide(); if(camera) { SensorCaptureThread cameraThread(camera, this->getAllParameters()); @@ -7906,12 +8084,26 @@ void PreferencesDialog::testCamera() cameraThread.start(); window->exec(); - delete window; - cameraThread.join(true); + UDEBUG("Dialog closed, stopping sensor..."); + UEventsManager::removePipe(&cameraThread, window, "SensorEvent"); + + // Reuse the same dialog for the close. The device close() runs in cameraThread's + // destructor at scope end (not in join()), so 'progress' - declared in the outer scope - + // stays visible across it. On Windows the first 2-3 RealSense closes per launch stall + // ~20s in the Motion Module stop() (librealsense warm-up); this keeps the user informed. + progress.setLabelText(tr("Closing camera...")); + progress.show(); + QApplication::processEvents(); + QApplication::processEvents(); // make sure it is drawn + cameraThread.join(true); // cameraThread's destructor (scope end) closes the device + // deleteLater() (not delete): defer destruction to the event loop so Qt finishes + // tearing down the OpenGL widget's context and window-proc subclass and drains + // pending activation messages first. + window->deleteLater(); } else { - delete window; + window->deleteLater(); } } @@ -8377,14 +8569,25 @@ void PreferencesDialog::calibrateOdomSensorExtrinsics() void PreferencesDialog::testLidar() { - CameraViewer * window = new CameraViewer(this, this->getAllParameters()); + // Not parented to 'this': see testCamera() - avoids the nested-modality crash. + CameraViewer * window = new CameraViewer(nullptr, this->getAllParameters()); window->setWindowTitle(tr("Lidar viewer")); window->setWindowFlags(Qt::Window); window->resize(1280, 480+QPushButton().minimumHeight()); window->registerToEventsManager(); window->setDecimation(1); + QProgressDialog progress(tr("Starting lidar..."), QString(), 0, 0, this); + progress.setWindowModality(Qt::ApplicationModal); + progress.setCancelButton(0); + progress.setMinimumDuration(0); + progress.setValue(0); + progress.show(); + QApplication::processEvents(); + QApplication::processEvents(); // make sure it is drawn + Lidar * lidar = this->createLidar(); + progress.hide(); if(lidar) { SensorCaptureThread lidarThread(lidar, this->getAllParameters()); @@ -8403,12 +8606,24 @@ void PreferencesDialog::testLidar() lidarThread.start(); window->exec(); - delete window; - lidarThread.join(true); + UDEBUG("Dialog closed, stopping sensor..."); + UEventsManager::removePipe(&lidarThread, window, "SensorEvent"); + + // Reuse the same dialog for the close. The device close() runs in lidarThread's + // destructor at scope end (not in join()), so 'progress' - declared in the outer + // scope - stays visible across it. + progress.setLabelText(tr("Closing sensor...")); + progress.show(); + QApplication::processEvents(); + QApplication::processEvents(); // make sure it is drawn + lidarThread.join(true); // lidarThread's destructor (scope end) closes the device + // deleteLater() (not delete): see testCamera() - avoids a dangling OpenGL platform + // window that crashes in QWindowsWindow::alertWindow when Preferences later closes. + window->deleteLater(); } else { - delete window; + window->deleteLater(); } } diff --git a/guilib/src/ui/preferencesDialog.ui b/guilib/src/ui/preferencesDialog.ui index c2fef56f..172c4d9b 100644 --- a/guilib/src/ui/preferencesDialog.ui +++ b/guilib/src/ui/preferencesDialog.ui @@ -5740,11 +5740,21 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki ULTRA + + + NEURAL LIGHT + + NEURAL + + + NEURAL PLUS + + @@ -5903,11 +5913,21 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki HD4K + + + QHDPLUS + + HD2K + + + HD1536 + + HD1080 @@ -5933,6 +5953,16 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki VGA + + + XVGA + + + + + TXVGA + + @@ -9387,9 +9417,6 @@ 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> - QAbstractSpinBox::NoButtons @@ -9400,15 +9427,12 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki 255 - 192 + 0 - - <html><head/><body><p>KITTI: 130 000 points</p></body></html> - QAbstractSpinBox::NoButtons @@ -9419,15 +9443,12 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki 255 - 168 + 0 - - <html><head/><body><p>KITTI: 130 000 points</p></body></html> - QAbstractSpinBox::NoButtons @@ -9438,15 +9459,12 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki 250 - 1 + 0 - - <html><head/><body><p>KITTI: 130 000 points</p></body></html> - QAbstractSpinBox::NoButtons @@ -9457,7 +9475,7 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki 250 - 201 + 0 @@ -9498,8 +9516,11 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki + + <html><head/><body><p>Local network interface (IP address of <span style=" font-weight:600;">this</span> computer) on which to listen for VLP16 packets. Use 0.0.0.0 to listen on all interfaces (recommended). This is NOT the sensor's IP address; the sensor decides where to send its data (configure its destination host in the sensor's own web interface).</p></body></html> + - IP Address. + Local IP to listen on (0.0.0.0 = all interfaces). See tooltip. true @@ -9554,8 +9575,11 @@ when using the file type, logs are saved in LogRtabmap.txt (located in the worki + + <html><head/><body><p>Local UDP port on this computer to listen for VLP16 data packets (default 2368). The LiDAR must be configured to send its data to this port.</p></body></html> + - Port. + Port to listen on (default 2368). true @@ -9567,9 +9591,6 @@ 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> - QAbstractSpinBox::NoButtons diff --git a/tools/CameraRGBD/main.cpp b/tools/CameraRGBD/main.cpp index adfa5e9c..1d701b39 100644 --- a/tools/CameraRGBD/main.cpp +++ b/tools/CameraRGBD/main.cpp @@ -97,7 +97,7 @@ void sighandler(int sig) int main(int argc, char * argv[]) { ULogger::setType(ULogger::kTypeConsole); - ULogger::setLevel(ULogger::kInfo); + ULogger::setLevel(ULogger::kDebug); //ULogger::setPrintTime(false); //ULogger::setPrintWhere(false); @@ -272,7 +272,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), -1, 1, 100, false, rate); + camera = new rtabmap::CameraStereoZed(deviceId.empty()?0:uStr2Int(deviceId), -1, 1, 0, 100, false, rate); } else if (driver == 9) { @@ -523,5 +523,6 @@ int main(int argc, char * argv[]) cameraViewer.exec(); cameraThread.join(true); } + printf("Exiting cleanly.\n"); return 0; } diff --git a/tools/DatabaseViewer/main.cpp b/tools/DatabaseViewer/main.cpp index 597655da..eed6bc80 100644 --- a/tools/DatabaseViewer/main.cpp +++ b/tools/DatabaseViewer/main.cpp @@ -53,6 +53,12 @@ int main(int argc, char * argv[]) QSurfaceFormat::setDefaultFormat(QVTKRenderWidget::defaultFormat()); #endif + // Recommended by VTK when using QVTKOpenGLNativeWidget (a QOpenGLWidget): let all VTK + // render widgets share a single OpenGL context, which is needed for correct rendering + // when render widgets live in / move across multiple top-level windows. Must be set + // before QApplication is constructed. + QApplication::setAttribute(Qt::AA_ShareOpenGLContexts); + #ifdef RTABMAP_PYTHON rtabmap::PythonInterface pythonInterface; #endif diff --git a/tools/LidarViewer/main.cpp b/tools/LidarViewer/main.cpp index bf3ef0df..bbb515f1 100644 --- a/tools/LidarViewer/main.cpp +++ b/tools/LidarViewer/main.cpp @@ -58,7 +58,7 @@ void showUsage() { printf("\nUsage:\n" "rtabmap-lidar_viewer IP PORT driver\n" - " driver Driver number to use: 0=VLP16 (default IP and port are 192.168.1.201 2368)\n"); + " driver Driver number to use: 0=VLP16 (default IP and port are 0.0.0.0 2368)\n"); exit(1); } diff --git a/tools/OdometryViewer/main.cpp b/tools/OdometryViewer/main.cpp index 67c65a0d..5729ad1c 100644 --- a/tools/OdometryViewer/main.cpp +++ b/tools/OdometryViewer/main.cpp @@ -404,13 +404,16 @@ int main (int argc, char * argv[]) { printf("The camera is not calibrated! You should calibrate the camera first.\n"); delete camera; + return 1; } } else { printf("Failed to initialize the camera! Please select another driver (see \"--help\").\n"); delete camera; + return 1; } + printf("Exiting cleanly.\n"); return 0; } diff --git a/utilite/src/ULogger.cpp b/utilite/src/ULogger.cpp index 2d09eef7..15eea368 100644 --- a/utilite/src/ULogger.cpp +++ b/utilite/src/ULogger.cpp @@ -139,10 +139,13 @@ protected: fileToClear.close(); } + // Open in binary mode: the log formatter already emits "\r\n" line endings explicitly. + // On Windows, text mode ("a") would translate the "\n" of that "\r\n" into "\r\n" again, + // producing "\r\r\n" on disk - i.e. an extra blank line between entries. #ifdef _MSC_VER - fopen_s(&fout_, fileName_.c_str(), "a"); + fopen_s(&fout_, fileName_.c_str(), "ab"); #else - fout_ = fopen(fileName_.c_str(), "a"); + fout_ = fopen(fileName_.c_str(), "ab"); #endif if(!fout_) { From e321999b1527047dbf52f32d4ad290ff3492ac30 Mon Sep 17 00:00:00 2001 From: Pierre ELIS Date: Sun, 5 Jul 2026 22:06:08 +0200 Subject: [PATCH 05/51] Faster graph zoom by using a snapshot-preview (#1728) * Faster graph zoom by using a snapshot-preview * Added "Fast Zooming..." menu option --------- Co-authored-by: matlabbe --- guilib/include/rtabmap/gui/GraphViewer.h | 14 +- guilib/src/GraphViewer.cpp | 171 ++++++++++++++++++++--- 2 files changed, 165 insertions(+), 20 deletions(-) diff --git a/guilib/include/rtabmap/gui/GraphViewer.h b/guilib/include/rtabmap/gui/GraphViewer.h index 8cf9d68a..75d35135 100644 --- a/guilib/include/rtabmap/gui/GraphViewer.h +++ b/guilib/include/rtabmap/gui/GraphViewer.h @@ -33,6 +33,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include +#include #include #include #include @@ -130,6 +132,7 @@ public: bool isOrientationENU() const; ViewPlane getViewPlane() const; bool isEnsureFrameVisible() const; + int getFastZoomMinNodes() const; // setters void setWorkingDirectory(const QString & path); @@ -170,6 +173,7 @@ public: void setOrientationENU(bool enabled); void setViewPlane(ViewPlane plane); void setEnsureFrameVisible(bool visible); + void setFastZoomMinNodes(int value); Q_SIGNALS: void configChanged(); @@ -184,11 +188,14 @@ protected: virtual void wheelEvent ( QWheelEvent * event ); virtual void mouseMoveEvent(QMouseEvent * event); virtual void mousePressEvent(QMouseEvent * event); + virtual void mouseReleaseEvent(QMouseEvent * event); virtual void contextMenuEvent(QContextMenuEvent * event); private: void setupGraphicsScene(); - + void startZoomOverlay(); + void updateZoomOverlay(); + void stopZoomOverlay(); private: QString _workingDirectory; QColor _nodeColor; @@ -248,6 +255,11 @@ private: ViewPlane _viewPlane; bool _ensureFrameVisible; QPoint _previousMousePos; + QPoint _initialMousePos; + QTimer _zoomDebounceTimer; + QGraphicsPixmapItem * _zoomOverlayItem; + bool _zoomOverlayActive; + int _fastZoomMinNodes; }; } /* namespace rtabmap */ diff --git a/guilib/src/GraphViewer.cpp b/guilib/src/GraphViewer.cpp index 89f23a86..c0a9a8de 100644 --- a/guilib/src/GraphViewer.cpp +++ b/guilib/src/GraphViewer.cpp @@ -32,6 +32,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include #include #include #include @@ -341,10 +342,18 @@ GraphViewer::GraphViewer(QWidget * parent) : _orientationENU(false), _mouseTracking(false), _viewPlane(XY), - _ensureFrameVisible(true) + _ensureFrameVisible(true), + _previousMousePos(-1,-1), + _initialMousePos(-1,-1), + _zoomDebounceTimer(this), + _zoomOverlayItem(0), + _zoomOverlayActive(false), + _fastZoomMinNodes(0) { this->setDragMode(QGraphicsView::ScrollHandDrag); _workingDirectory = QDir::homePath(); + _zoomDebounceTimer.setSingleShot(true); + connect(&_zoomDebounceTimer, &QTimer::timeout, this, &GraphViewer::stopZoomOverlay); setupGraphicsScene(); @@ -355,6 +364,9 @@ GraphViewer::GraphViewer(QWidget * parent) : this->restoreDefaults(); this->fitInView(this->sceneRect(), Qt::KeepAspectRatio); + + setTransformationAnchor(QGraphicsView::AnchorUnderMouse); + setResizeAnchor(QGraphicsView::AnchorUnderMouse); } GraphViewer::~GraphViewer() @@ -363,11 +375,20 @@ GraphViewer::~GraphViewer() void GraphViewer::setupGraphicsScene() { + _zoomDebounceTimer.stop(); + _zoomOverlayActive = false; + _zoomOverlayItem = 0; if(this->scene()) { delete this->scene(); } this->setScene(new QGraphicsScene(this)); + _zoomOverlayItem = this->scene()->addPixmap(QPixmap()); + _zoomOverlayItem->setZValue(1000000); + _zoomOverlayItem->setVisible(false); + _zoomOverlayItem->setAcceptedMouseButtons(Qt::NoButton); + _zoomOverlayItem->setAcceptHoverEvents(false); + _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); @@ -507,6 +528,62 @@ void GraphViewer::setupGraphicsScene() } +void GraphViewer::startZoomOverlay() +{ + if(_fastZoomMinNodes==0 || _nodeItems.size() < _fastZoomMinNodes) + { + return; + } + + if(_zoomOverlayActive || !_zoomOverlayItem) + { + return; + } + + const QRect viewportRect = this->viewport()->rect(); + if(viewportRect.isEmpty()) + { + return; + } + + const QPixmap snapshot = this->viewport()->grab(); + if(snapshot.isNull()) + { + return; + } + + const QRectF sourceRect = this->mapToScene(viewportRect).boundingRect(); + const qreal sx = sourceRect.width() / qreal(snapshot.width()); + const qreal sy = sourceRect.height() / qreal(snapshot.height()); + + _zoomOverlayItem->setPixmap(snapshot); + _zoomOverlayItem->setTransform(QTransform::fromScale(sx, sy)); + _zoomOverlayItem->setPos(sourceRect.topLeft()); + _zoomOverlayItem->show(); + _world->setVisible(false); + _zoomOverlayActive = true; +} + +void GraphViewer::stopZoomOverlay() +{ + _zoomDebounceTimer.stop(); + if(!_zoomOverlayActive) + { + return; + } + + _zoomOverlayActive = false; + if(_zoomOverlayItem) + { + _zoomOverlayItem->hide(); + } + if(_world) + { + _world->setVisible(true); + } + this->viewport()->update(); +} + void GraphViewer::setWorldMapRotation(const float & theta) { _worldMapRotation = theta; @@ -1432,6 +1509,7 @@ void GraphViewer::clearNodeColorByValue() void GraphViewer::clearAll() { + stopZoomOverlay(); clearMap(); clearGraph(); @@ -1489,6 +1567,7 @@ void GraphViewer::saveSettings(QSettings & settings, const QString & group) cons settings.setValue("orientation_ENU", this->isOrientationENU()); settings.setValue("view_plane", (int)this->getViewPlane()); settings.setValue("ensure_frame_visible", (int)this->isEnsureFrameVisible()); + settings.setValue("fast_zoom_min_nodes", this->getFastZoomMinNodes()); if(!group.isEmpty()) { settings.endGroup(); @@ -1540,6 +1619,7 @@ void GraphViewer::loadSettings(QSettings & settings, const QString & group) this->setOrientationENU(settings.value("orientation_ENU", this->isOrientationENU()).toBool()); this->setViewPlane((ViewPlane)settings.value("view_plane", (int)this->getViewPlane()).toInt()); this->setEnsureFrameVisible(settings.value("ensure_frame_visible", this->isEnsureFrameVisible()).toBool()); + this->setFastZoomMinNodes(settings.value("fast_zoom_min_nodes", this->getFastZoomMinNodes()).toInt()); if(!group.isEmpty()) { settings.endGroup(); @@ -1602,6 +1682,10 @@ bool GraphViewer::isEnsureFrameVisible() const { return _ensureFrameVisible; } +int GraphViewer::getFastZoomMinNodes() const +{ + return _fastZoomMinNodes; +} void GraphViewer::setWorkingDirectory(const QString & path) { @@ -1967,6 +2051,11 @@ void GraphViewer::setEnsureFrameVisible(bool visible) _ensureFrameVisible = visible; } +void GraphViewer::setFastZoomMinNodes(int value) +{ + _fastZoomMinNodes = value; +} + void GraphViewer::restoreDefaults() { @@ -1997,18 +2086,26 @@ void GraphViewer::restoreDefaults() setGlobalPathVisible(true); setLocalPathVisible(true); setGtGraphVisible(true); + setFastZoomMinNodes(0); } -void GraphViewer::wheelEvent ( QWheelEvent * event ) +void GraphViewer::wheelEvent(QWheelEvent * event) { - if(event->angleDelta().y() < 0) + if(event->angleDelta().y() == 0) { - this->scale(0.95, 0.95); - } - else - { - this->scale(1.05, 1.05); + event->ignore(); + return; } + + // Start buffering zoom interaction with a raster snapshot. + startZoomOverlay(); + + const qreal factor = event->angleDelta().y() < 0 ? 0.95 : 1.05; + this->scale(factor, factor); + + // When wheel stops for 400 ms, restore the real scene and let it redraw once. + _zoomDebounceTimer.start(400); + event->accept(); } void GraphViewer::mouseMoveEvent(QMouseEvent * event) @@ -2029,14 +2126,16 @@ void GraphViewer::mouseMoveEvent(QMouseEvent * event) if (event->modifiers() & (Qt::ShiftModifier | Qt::ControlModifier) && event->buttons() & Qt::LeftButton) { // same modifiers than 3D view, change zoom if(_previousMousePos.y()!=0) { - if(event->pos().y() - _previousMousePos.y() > 0) - { - this->scale(0.98, 0.98); - } - else - { - this->scale(1.02, 1.02); - } + qreal factor = (event->pos().y() - _previousMousePos.y() > 0) ? 0.98 : 1.02; + + QGraphicsView::ViewportAnchor oldAnchor = this->transformationAnchor(); + this->setTransformationAnchor(QGraphicsView::NoAnchor); + QPointF scenePointBefore = this->mapToScene(_initialMousePos); + this->scale(factor, factor); + QPointF scenePointAfter = this->mapToScene(_initialMousePos); + QPointF delta = scenePointAfter - scenePointBefore; + this->translate(delta.x(), delta.y()); + this->setTransformationAnchor(oldAnchor); } _previousMousePos = event->pos(); return; @@ -2047,11 +2146,21 @@ void GraphViewer::mouseMoveEvent(QMouseEvent * event) void GraphViewer::mousePressEvent(QMouseEvent * event) { - QGraphicsItem *item = this->scene()->itemAt(mapToScene(event->pos()), QTransform()); + if((event->button() == Qt::LeftButton) && + (event->modifiers() & (Qt::ShiftModifier | Qt::ControlModifier))) + { + startZoomOverlay(); + _previousMousePos = event->pos(); + _initialMousePos = event->pos(); + event->accept(); + return; + } + + QGraphicsItem * item = this->scene()->itemAt(mapToScene(event->pos()), QTransform()); if(item) { - NodeItem *nodeItem = qgraphicsitem_cast(item); - LinkItem *linkItem = qgraphicsitem_cast(item); + NodeItem * nodeItem = qgraphicsitem_cast(item); + LinkItem * linkItem = qgraphicsitem_cast(item); if(nodeItem && nodeItem->parentItem() == _graphRoot && nodeItem->id() != 0) { Q_EMIT nodeSelected(nodeItem->id()); @@ -2071,6 +2180,19 @@ void GraphViewer::mousePressEvent(QMouseEvent * event) } } +void GraphViewer::mouseReleaseEvent(QMouseEvent * event) +{ + _previousMousePos = QPoint(-1, -1); + _initialMousePos = QPoint(-1, -1); + + if(_zoomOverlayActive) + { + stopZoomOverlay(); + } + + QGraphicsView::mouseReleaseEvent(event); +} + QIcon createIcon(const QColor & color) { QPixmap pixmap(50, 50); @@ -2167,6 +2289,7 @@ void GraphViewer::contextMenuEvent(QContextMenuEvent * event) QAction * aShowHideOdomCacheOverlay; QAction * aOrientationENU; QAction * aMouseTracking; + QAction * aFastZooming; QAction * aViewPlaneXY; QAction * aViewPlaneXZ; QAction * aViewPlaneYZ; @@ -2269,6 +2392,7 @@ void GraphViewer::contextMenuEvent(QContextMenuEvent * event) aMouseTracking->setCheckable(true); aMouseTracking->setChecked(_mouseTracking); aMouseTracking->setEnabled(_viewPlane == XY); + aFastZooming = menu.addAction(tr("Fast Zooming...")); aShowHideGraph->setEnabled(_viewPlane == XY); aShowHideGlobalPath->setEnabled(_globalPathLinkItems.size()); aShowHideLocalPath->setEnabled(_localPathLinkItems.size()); @@ -2714,6 +2838,15 @@ void GraphViewer::contextMenuEvent(QContextMenuEvent * event) { _mouseTracking = aMouseTracking->isChecked(); } + else if(r == aFastZooming) + { + bool ok; + double value = QInputDialog::getInt(this, tr("Fast Zooming"), tr("Minimum number of nodes to raster the graph\nwhile zooming (0=disabled):"), _fastZoomMinNodes, 0, 2147483647, 100, &ok); + if(ok) + { + setFastZoomMinNodes(value); + } + } else if(r == aViewPlaneXY) { this->setViewPlane(XY); From d96c30710eebda0ff5e69e217c88cd9e560fd0cc Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sat, 11 Jul 2026 13:08:22 -0700 Subject: [PATCH 06/51] =?UTF-8?q?CI:=20remove=20conflicting=20ros2-testing?= =?UTF-8?q?-apt-source=20before=20setup-ros=20(roll=E2=80=A6=20(#1737)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * CI: remove conflicting ros2-testing-apt-source before setup-ros (rolling) * fixing rolling testing packages * skip gtsam rolling * cleanup --- .github/workflows/cmake-ros.yml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/cmake-ros.yml b/.github/workflows/cmake-ros.yml index 393608f1..942735d2 100644 --- a/.github/workflows/cmake-ros.yml +++ b/.github/workflows/cmake-ros.yml @@ -9,7 +9,6 @@ on: - '**' env: - # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) BUILD_TYPE: Release concurrency: @@ -18,10 +17,6 @@ concurrency: jobs: build: - # The CMake configure and build commands are platform agnostic and should work equally - # 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 }} runs-on: ubuntu-latest strategy: @@ -38,14 +33,16 @@ jobs: - ros_distribution: 'lyrical' skip_keys: "libpointmatcher" - ros_distribution: 'rolling' - skip_keys: "" + skip_keys: "libpointmatcher gtsam" + use_ros2_testing: true # Rolling is using ros2-testing (nightly) container: image: osrf/ros:${{ matrix.ros_distribution }}-desktop-full - steps: + steps: - uses: actions/checkout@v4 - uses: ros-tooling/setup-ros@v0.7 with: required-ros-distributions: ${{ matrix.ros_distribution }} + use-ros2-testing: ${{ matrix.use_ros2_testing || false }} - uses: ros-tooling/action-ros-ci@v0.4 with: package-name: rtabmap From 0f56dd4b4320f1915b2dc48151264273c2e1fbfc Mon Sep 17 00:00:00 2001 From: Borong Yuan Date: Wed, 15 Jul 2026 01:14:11 +0800 Subject: [PATCH 07/51] Fixed some issues related to intermediate nodes and retrieval/immunization (#1733) * Allow ignoring intermediate nodes when calculating WM size * Fix the retrieval logic to avoid unnecessary code execution * Making getWorkingMemSize(true) complexity O(1) * Updated statistics to show STM size, WM size, immunized nodes without intermediate nodes count. Added two new statistics to track number of intermediate nodes in STM and WM. * Correct the condition for counting intermediate nodes * Restore the logic of retrieval 2/3 to avoid affecting updateAge --------- Co-authored-by: matlabbe --- app/android/jni/RTABMapApp.cpp | 2 +- corelib/include/rtabmap/core/Memory.h | 5 + corelib/include/rtabmap/core/Statistics.h | 2 + corelib/src/Memory.cpp | 107 ++++++++++++++++++---- corelib/src/Rtabmap.cpp | 107 +++++++++++++--------- 5 files changed, 161 insertions(+), 62 deletions(-) diff --git a/app/android/jni/RTABMapApp.cpp b/app/android/jni/RTABMapApp.cpp index 2db3e8d3..81e007e9 100644 --- a/app/android/jni/RTABMapApp.cpp +++ b/app/android/jni/RTABMapApp.cpp @@ -204,7 +204,7 @@ rtabmap::ParametersMap RTABMapApp::getRtabmapParameters() uInsert(parameters, rtabmap::ParametersPair(rtabmap::Parameters::kRtabmapMaxRetrieved(), "0")); // deactivate global retrieval uInsert(parameters, rtabmap::ParametersPair(rtabmap::Parameters::kRGBDMaxLocalRetrieved(), "0")); // deactivate local retrieval uInsert(parameters, rtabmap::ParametersPair(rtabmap::Parameters::kMemMapLabelsAdded(), "false")); // don't create map labels - uInsert(parameters, rtabmap::ParametersPair(rtabmap::Parameters::kRtabmapMemoryThr(), "2")); // keep the WM empty + uInsert(parameters, rtabmap::ParametersPair(rtabmap::Parameters::kRtabmapMemoryThr(), "1")); // keep the WM empty uInsert(parameters, rtabmap::ParametersPair(rtabmap::Parameters::kMemSTMSize(), "1")); // STM=1 --> uInsert(parameters, rtabmap::ParametersPair(rtabmap::Parameters::kRGBDProximityBySpace(), "false")); uInsert(parameters, rtabmap::ParametersPair(rtabmap::Parameters::kRGBDLinearUpdate(), "0")); diff --git a/corelib/include/rtabmap/core/Memory.h b/corelib/include/rtabmap/core/Memory.h index 8c3306ad..ab3df0a7 100644 --- a/corelib/include/rtabmap/core/Memory.h +++ b/corelib/include/rtabmap/core/Memory.h @@ -149,6 +149,9 @@ public: //getters const std::map & getWorkingMem() const {return _workingMem;} + size_t getWorkingMemSize(bool ignoreIntermediateNodes = false) const; + int getWorkingMemIntermediateNodesCount() const {return _workingMemIntermediateNodesCount;} + int getStMemIntermediateNodesCount() const {return _stMemIntermediateNodesCount;} const std::set & getStMem() const {return _stMem;} int getMaxStMemSize() const {return _maxStMemSize;} std::multimap getNeighborLinks(int signatureId, @@ -360,6 +363,8 @@ private: bool _memoryChanged; // False by default, become true only when Memory::update() is called. bool _linksChanged; // False by default, become true when links are modified. int _signaturesAdded; + int _workingMemIntermediateNodesCount; // number of nodes with weight==-1 currently in _workingMem + int _stMemIntermediateNodesCount; // number of nodes with weight==-1 currently in _stMem bool _allNodesInWM; bool _receivingOdometryFeatures; GPS _gpsOrigin; diff --git a/corelib/include/rtabmap/core/Statistics.h b/corelib/include/rtabmap/core/Statistics.h index 1cdaf40d..07e610b4 100644 --- a/corelib/include/rtabmap/core/Statistics.h +++ b/corelib/include/rtabmap/core/Statistics.h @@ -137,7 +137,9 @@ class RTABMAP_CORE_EXPORT Statistics RTABMAP_STATS(NeighborLinkRefining, Pts,); RTABMAP_STATS(Memory, Working_memory_size,); + RTABMAP_STATS(Memory, Working_memory_inter_size,); RTABMAP_STATS(Memory, Short_time_memory_size,); + RTABMAP_STATS(Memory, Short_time_memory_inter_size,); RTABMAP_STATS(Memory, Database_memory_used, MB); RTABMAP_STATS(Memory, Signatures_removed,); RTABMAP_STATS(Memory, Immunized_globally,); diff --git a/corelib/src/Memory.cpp b/corelib/src/Memory.cpp index 37afdbf7..e0ffac39 100644 --- a/corelib/src/Memory.cpp +++ b/corelib/src/Memory.cpp @@ -131,6 +131,8 @@ Memory::Memory(const ParametersMap & parameters) : _memoryChanged(false), _linksChanged(false), _signaturesAdded(0), + _workingMemIntermediateNodesCount(0), + _stMemIntermediateNodesCount(0), _allNodesInWM(true), _receivingOdometryFeatures(false), _badSignRatio(Parameters::defaultKpBadSignRatio()), @@ -267,6 +269,10 @@ void Memory::loadDataFromDb(bool postInitClosingEvents) // global loop closures. _signatures.insert(std::pair((*iter)->id(), *iter)); _workingMem.insert(std::make_pair((*iter)->id(), UTimer::now())); + if((*iter)->getWeight() == -1) + { + ++_workingMemIntermediateNodesCount; + } if(!(*iter)->getGroundTruthPose().isNull()) { _groundTruths.insert(std::make_pair((*iter)->id(), (*iter)->getGroundTruthPose())); } @@ -1079,7 +1085,7 @@ bool Memory::update( } else { - if(_workingMem.size() <= 1) + if(this->getWorkingMemSize(true) == 0) { UWARN("The working memory is empty and the memory is not " "incremental (Mem/IncrementalMemory=False), no loop closure " @@ -1255,6 +1261,10 @@ void Memory::addSignatureToStm(Signature * signature, const cv::Mat & covariance _signatures.insert(_signatures.end(), std::pair(signature->id(), signature)); _stMem.insert(_stMem.end(), signature->id()); + if(signature->getWeight() == -1) + { + ++_stMemIntermediateNodesCount; + } if(!signature->getGroundTruthPose().isNull()) { _groundTruths.insert(std::make_pair(signature->id(), signature->getGroundTruthPose())); } @@ -1276,6 +1286,10 @@ void Memory::addSignatureToWmFromLTM(Signature * signature) { UDEBUG("Inserting node %d in WM...", signature->id()); _workingMem.insert(std::make_pair(signature->id(), UTimer::now())); + if(signature->getWeight() == -1) + { + ++_workingMemIntermediateNodesCount; + } _signatures.insert(std::pair(signature->id(), signature)); if(!signature->getGroundTruthPose().isNull()) { _groundTruths.insert(std::make_pair(signature->id(), signature->getGroundTruthPose())); @@ -1466,6 +1480,11 @@ void Memory::moveSignatureToWMFromSTM(int id, int * reducedToOut) if(reducedId == 0) { _workingMem.insert(_workingMem.end(), std::make_pair(*_stMem.begin(), UTimer::now())); + if(this->_getSignature(*_stMem.begin())->getWeight() == -1) + { + ++_workingMemIntermediateNodesCount; + --_stMemIntermediateNodesCount; + } _stMem.erase(*_stMem.begin()); } // else already removed from STM/WM in reduceNode() @@ -1486,6 +1505,19 @@ const VWDictionary * Memory::getVWDictionary() const return _vwd; } +size_t Memory::getWorkingMemSize(bool ignoreIntermediateNodes) const +{ + // -1 removes the virtual place + if(!ignoreIntermediateNodes) + { + return _workingMem.size() - 1; + } + else + { + return _workingMem.size() - 1 - _workingMemIntermediateNodesCount; + } +} + std::multimap Memory::getNeighborLinks( int signatureId, bool lookInDatabase) const @@ -2022,6 +2054,7 @@ void Memory::clear() ULOGGER_ERROR("_stMem must be empty here, size=%d", _stMem.size()); } _stMem.clear(); + _stMemIntermediateNodesCount = 0; this->cleanUnusedWords(); @@ -2038,18 +2071,15 @@ void Memory::clear() } // Save some stats to the db, save only when the mem is not empty - if(_dbDriver && (_stMem.size() || _workingMem.size())) + size_t workingMemSize = this->getWorkingMemSize(false); + if(_dbDriver && (_stMem.size() || workingMemSize)) { - unsigned int memSize = (unsigned int)(_workingMem.size() + _stMem.size()); - if(_workingMem.size() && _workingMem.begin()->first < 0) - { - --memSize; - } + unsigned int memSize = workingMemSize + _stMem.size(); // this is only a safe check...not supposed to occur. UASSERT_MSG(memSize == _signatures.size(), uFormat("The number of signatures don't match! _workingMem=%d, _stMem=%d, _signatures=%d", - _workingMem.size(), _stMem.size(), _signatures.size()).c_str()); + workingMemSize, _stMem.size(), _signatures.size()).c_str()); UDEBUG("Adding statistics after run..."); if(_memoryChanged) @@ -2096,6 +2126,7 @@ void Memory::clear() ULOGGER_ERROR("_workingMem must be empty here, size=%d", _workingMem.size()); } _workingMem.clear(); + _workingMemIntermediateNodesCount = 0; if(_signatures.size()!=0) { ULOGGER_ERROR("_signatures must be empty here, size=%d", _signatures.size()); @@ -2482,7 +2513,7 @@ std::map Memory::loadOptimizedPoses(Transform * lastlocalization "poses to force re-update. If you want to use the " "saved optimized poses, set %s to true", (int)poses.size(), - (int)_workingMem.size()-1, // less virtual place + (int)this->getWorkingMemSize(false), Parameters::kMemInitWMWithAllNodes().c_str()); return std::map(); } @@ -2590,18 +2621,21 @@ public: } int weight, age, id; }; + std::list Memory::getRemovableSignatures(int count, const std::set & ignoredIds) { //UDEBUG(""); std::list removableSignatures; std::map weightAgeIdMap; - // Find the last index to check... - UDEBUG("mem.size()=%d, ignoredIds.size()=%d", (int)_workingMem.size(), (int)ignoredIds.size()); + size_t workingMemSize = this->getWorkingMemSize(true); - if(_workingMem.size()) + // Find the last index to check... + UDEBUG("mem.size()=%d, ignoredIds.size()=%d", (int)workingMemSize, (int)ignoredIds.size()); + + if(workingMemSize > 0) { - int recentWmMaxSize = _recentWmRatio * float(_workingMem.size()); + int recentWmMaxSize = _recentWmRatio * float(workingMemSize); bool recentWmImmunized = false; // look for the position of the lastLoopClosureId in WM int currentRecentWmSize = 0; @@ -2618,7 +2652,7 @@ std::list Memory::getRemovableSignatures(int count, const std::set< { recentWmImmunized = true; } - else if(currentRecentWmSize == 0 && _workingMem.size() > 1) + else if(currentRecentWmSize == 0) { UERROR("Last loop closure id not found in WM (%d)", _lastGlobalLoopClosureId); } @@ -2727,6 +2761,20 @@ void Memory::moveToTrash(Signature * s, bool keepLinkedToGraph, std::list * //UDEBUG("id=%d", s?s->id():0); if(s) { + // Keep the WM/STM intermediate-node counters in sync now, before the weight + // may be set to -9 below. + if(s->getWeight() == -1) + { + if(this->isInWM(s->id())) + { + --_workingMemIntermediateNodesCount; + } + else if(this->isInSTM(s->id())) + { + --_stMemIntermediateNodesCount; + } + } + // Cleanup landmark indexes if(!s->getLandmarks().empty()) { @@ -3054,6 +3102,19 @@ void Memory::convertToIntermediate(int locationId) Signature * location = _getSignature(locationId); if(location) { + // Keep the WM/STM intermediate-node counters in sync if the node is + // converted while already resident in memory. + if(location->getWeight() >= 0) + { + if(this->isInWM(locationId)) + { + ++_workingMemIntermediateNodesCount; + } + else if(this->isInSTM(locationId)) + { + ++_stMemIntermediateNodesCount; + } + } location->setWeight(-1); location->sensorData().setFeatures(std::vector(), std::vector(), cv::Mat()); this->disableWordsRef(locationId); // won't be used for loop closure detection anymore @@ -5194,11 +5255,21 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor UDEBUG("time rectification = %fs", t); } - int treeSize= int(_workingMem.size() + _stMem.size()); - int meanWordsPerLocation = _feature2D->getMaxFeatures()>0?_feature2D->getMaxFeatures():0; - if(meanWordsPerLocation==0 && treeSize > 1) + int notIntermediateNodesCount = 0; + for(std::set::iterator iter=_stMem.begin(); iter!=_stMem.end(); ++iter) { - meanWordsPerLocation = _vwd->getTotalActiveReferences() / (treeSize-1); // ignore virtual signature + const Signature * s = this->getSignature(*iter); + UASSERT(s != 0); + if(s->getWeight() >= 0) + { + ++notIntermediateNodesCount; + } + } + int treeSize= int(this->getWorkingMemSize(true) + notIntermediateNodesCount); + int meanWordsPerLocation = _feature2D->getMaxFeatures()>0?_feature2D->getMaxFeatures():0; + if(meanWordsPerLocation==0 && treeSize > 0) + { + meanWordsPerLocation = _vwd->getTotalActiveReferences() / treeSize; } else if(_useOdometryFeatures) { // To not detect first image as bad signature if odometry diff --git a/corelib/src/Rtabmap.cpp b/corelib/src/Rtabmap.cpp index 775572ed..1c64de81 100644 --- a/corelib/src/Rtabmap.cpp +++ b/corelib/src/Rtabmap.cpp @@ -378,9 +378,7 @@ void Rtabmap::init(const ParametersMap & parameters, const std::string & databas _optimizedPoses = _memory->loadOptimizedPoses(&lastPose); if(!_memory->isIncremental()) { - if(_optimizedPoses.empty() && - _memory->getWorkingMem().size()>1 && - _memory->getWorkingMem().lower_bound(1)!=_memory->getWorkingMem().end()) + if(_optimizedPoses.empty() && _memory->getWorkingMemSize(true) > 0) { cv::Mat cov; this->optimizeCurrentMap( @@ -810,7 +808,7 @@ int Rtabmap::getWMSize() const { if(_memory) { - return (int)_memory->getWorkingMem().size()-1; // remove virtual place + return (int)_memory->getWorkingMemSize(false); } return 0; } @@ -1998,7 +1996,7 @@ bool Rtabmap::process( // If the working memory is empty, don't do the detection. It happens when it // is the first time the detector is started (there needs some images to // fill the short-time memory before a signature is added to the working memory). - if(_memory->getWorkingMem().size()) + if(_memory->getWorkingMemSize(true)) { //============================================================ // Likelihood computation @@ -2174,7 +2172,7 @@ bool Rtabmap::process( } if( (( _memory->isIncremental() && !uContains(_optimizedPoses, _highestHypothesis.first)) || // not linked to previous map of that hypothesis (!_memory->isIncremental() && !hasLoopClosureConstraints)) && // not yet localized to any previous sessions - _memory->getWorkingMem().size()>1 && // should have an old map (beside virtual signature) + _memory->getWorkingMemSize(true)>0 && // should have an old map _rgbdSlamMode && loopThr > _aggressiveLoopThr) { @@ -2227,7 +2225,7 @@ bool Rtabmap::process( hypothesisRatio = _loopClosureHypothesis.second>0?_highestHypothesis.second/_loopClosureHypothesis.second:0; } } // if(_memory->getWorkingMemSize()) - }// !isBadSignature + } // !isBadSignature else if(!signature->isBadSignature() && signature->getWeight()>=0 && (smallDisplacement || tooFastMovement)) { _highestHypothesis = lastHighestHypothesis; @@ -2259,12 +2257,12 @@ bool Rtabmap::process( if(_maxTimeAllowed != 0 || _maxMemoryAllowed != 0) { // with memory management, we have to immunize some nodes - maxLocalLocationsImmunized = _localImmunizationRatio * float(_memory->getWorkingMem().size()); + maxLocalLocationsImmunized = _localImmunizationRatio * float(_memory->getWorkingMemSize(true)); } // no need to do retrieval or immunization of locations if memory management // is disabled and all nodes are in WM. // Also skip memory mangement on intermediate nodes - if(!(_memory->allNodesInWM() && maxLocalLocationsImmunized == 0) && signature->getWeight()>=0) + if(!((_memory->allNodesInWM() || _maxRetrieved==0) && maxLocalLocationsImmunized==0) && signature->getWeight()>=0) { if(retrievalId > 0) { @@ -2320,7 +2318,13 @@ bool Rtabmap::process( //immunized locations in the neighborhood from being transferred if(immunizedLocations.insert(iter->first).second) { - ++immunizedGlobally; + // Count only non-intermediate nodes (intermediate nodes are still + // immunized but don't consume the immunization budget/statistic). + const Signature * sImmunized = _memory->getSignature(iter->first); + if(sImmunized == 0 || sImmunized->getWeight() >= 0) + { + ++immunizedGlobally; + } } //UDEBUG("nt=%d m=%d immunized=1", iter->first, iter->second); @@ -2423,9 +2427,12 @@ bool Rtabmap::process( distanceSoFar += _path[i-1].second.getDistance(_path[i].second); } - if(_memory->getSignature(_path[i].first) != 0) + const Signature * sPath = _memory->getSignature(_path[i].first); + if(sPath != 0) { - if(immunizedLocations.insert(_path[i].first).second) + // Count only non-intermediate nodes (intermediate nodes are still + // immunized but don't consume the immunization budget/statistic). + if(immunizedLocations.insert(_path[i].first).second && sPath->getWeight() >= 0) { ++immunizedLocally; } @@ -2447,7 +2454,7 @@ bool Rtabmap::process( } } - if(!(_memory->allNodesInWM() && maxLocalLocationsImmunized == 0)) + if(!(_memory->allNodesInWM() && maxLocalLocationsImmunized==0)) { // immunize the path from the nearest local location to the current location if(immunizedLocally < maxLocalLocationsImmunized && @@ -2498,15 +2505,16 @@ bool Rtabmap::process( { UWARN("Could not immunize the whole local path (%d) between " "%d and %d (max location immunized=%d). You may want " - "to increase RGBD/LocalImmunizationRatio (current=%f (%d of WM=%d)) " + "to increase %s (current=%f (%d of WM=%d)) " "to be able to immunize longer paths.", (int)path.size(), nearestId, signature->id(), maxLocalLocationsImmunized, + Parameters::kRGBDLocalImmunizationRatio().c_str(), _localImmunizationRatio, maxLocalLocationsImmunized, - (int)_memory->getWorkingMem().size()); + (int)_memory->getWorkingMemSize(true)); } break; } @@ -2514,7 +2522,13 @@ bool Rtabmap::process( { if(immunizedLocations.insert(iter->first).second) { - ++immunizedLocally; + // Count only non-intermediate nodes (intermediate nodes are still + // immunized but don't consume the immunization budget/statistic). + const Signature * sLocal = _memory->getSignature(iter->first); + if(sLocal == 0 || sLocal->getWeight() >= 0) + { + ++immunizedLocally; + } } //UDEBUG("local node %d on path immunized=1", iter->first); } @@ -2538,7 +2552,7 @@ bool Rtabmap::process( maxLocalLocationsImmunized, immunizedLocally, _localImmunizationRatio, - (int)_memory->getWorkingMem().size()); + (int)_memory->getWorkingMemSize(true)); std::list retrievalLocalIdsIntermediate; for(std::multimap::iterator iter=nearNodesByDist.begin(); iter!=nearNodesByDist.end() && (retrievalLocalIds.size() < _maxLocalRetrieved || immunizedLocally < maxLocalLocationsImmunized); @@ -2574,7 +2588,9 @@ bool Rtabmap::process( } if(!_memory->isInSTM(s->id()) && immunizedLocally < maxLocalLocationsImmunized) { - if(immunizedLocations.insert(s->id()).second) + // Count only non-intermediate nodes (intermediate nodes are still + // immunized but don't consume the immunization budget/statistic). + if(immunizedLocations.insert(s->id()).second && s->getWeight() >= 0) { ++immunizedLocally; } @@ -2685,7 +2701,7 @@ bool Rtabmap::process( signature->getWeight() >= 0) // not an intermediate node { if(_startNewMapOnLoopClosure && - _memory->getWorkingMem().size()>=2 && // must have an old map (+1 virtual place) + _memory->getWorkingMemSize(true)>0 && // must have an old map _localizationCovariance.empty() && // if we didn't localize yet graph::filterLinks(signature->getLinks(), Link::kSelfRefLink).size() == 0) // alone in new session { @@ -4451,7 +4467,7 @@ bool Rtabmap::process( _memory->isIncremental() && // only in mapping mode graph::filterLinks(signature->getLinks(), Link::kSelfRefLink).size() == 0 && // alone in the current map (landmarksDetected.empty() || rejectedLoopClosure) && // if we re not seeing a landmark from a previous map - _memory->getWorkingMem().size()>=2) // The working memory should not be empty (beside virtual signature) + _memory->getWorkingMemSize(true)>0) // The working memory should not be empty { UWARN("Ignoring location %d because a global loop closure is required before starting a new map!", signature->id()); @@ -4538,26 +4554,29 @@ bool Rtabmap::process( //============================================================ double totalTime = timerTotal.ticks(); ULOGGER_INFO("Total time processing = %fs...", totalTime); - if(!lastSignatureWasIntermediateNode && // skip memory management on intermediate nodes - ((_maxTimeAllowed != 0 && totalTime*1000>_maxTimeAllowed) || - (_maxMemoryAllowed != 0 && _memory->getWorkingMem().size() > _maxMemoryAllowed))) + if(!lastSignatureWasIntermediateNode) // skip memory management on intermediate nodes { - if(_maxTimeAllowed!=0 && totalTime*1000>_maxTimeAllowed) + size_t workingMemSize = _memory->getWorkingMemSize(true); + if((_maxTimeAllowed != 0 && totalTime*1000 > _maxTimeAllowed) || + (_maxMemoryAllowed != 0 && workingMemSize > _maxMemoryAllowed)) { - 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()); - if(!_someNodesHaveBeenTransferred && transferred.size()) - { - _someNodesHaveBeenTransferred = true; // only used to hide a warning on close nodes immunization + 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 && workingMemSize > _maxMemoryAllowed) + { + ULOGGER_INFO("Removing old signatures because memory limit is reached %d > %d...", + workingMemSize, _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()); + if(!_someNodesHaveBeenTransferred && transferred.size()) + { + _someNodesHaveBeenTransferred = true; // only used to hide a warning on close nodes immunization + } } } _lastProcessTime = totalTime; @@ -4709,8 +4728,10 @@ bool Rtabmap::process( statistics_.addStatistic(Statistics::kMemoryImmunized_locally_max(), maxLocalLocationsImmunized); // place after transfer because the memory/local graph may have changed - statistics_.addStatistic(Statistics::kMemoryWorking_memory_size(), _memory->getWorkingMem().size()); - statistics_.addStatistic(Statistics::kMemoryShort_time_memory_size(), _memory->getStMem().size()); + statistics_.addStatistic(Statistics::kMemoryWorking_memory_size(), _memory->getWorkingMemSize(true)); + statistics_.addStatistic(Statistics::kMemoryWorking_memory_inter_size(), _memory->getWorkingMemIntermediateNodesCount()); + statistics_.addStatistic(Statistics::kMemoryShort_time_memory_size(), _memory->getStMem().size()-_memory->getStMemIntermediateNodesCount()); + statistics_.addStatistic(Statistics::kMemoryShort_time_memory_inter_size(), _memory->getStMemIntermediateNodesCount()); statistics_.addStatistic(Statistics::kMemoryDatabase_memory_used(), _memory->getDatabaseMemoryUsed()); // Set local graph @@ -4859,7 +4880,7 @@ bool Rtabmap::process( } std::vector ids; - ids.reserve(_memory->getWorkingMem().size() + _memory->getStMem().size()); + ids.reserve(_memory->getWorkingMemSize(false) + _memory->getStMem().size()); for(std::set::const_iterator iter=_memory->getStMem().begin(); iter!=_memory->getStMem().end(); ++iter) { ids.push_back(*iter); @@ -4921,7 +4942,7 @@ bool Rtabmap::process( 0, refWordsCount, dictionarySize, - int(_memory->getWorkingMem().size()), + int(_memory->getWorkingMemSize(false)), rejectedLoopClosure?1:0, 0, 0, @@ -5948,7 +5969,7 @@ void Rtabmap::getGraph( } } } - else if(_memory && (_memory->getStMem().size() || _memory->getWorkingMem().size() > 1)) + else if(_memory && (_memory->getStMem().size() || _memory->getWorkingMemSize(!global) > 0)) { UERROR("Last working signature is null!?"); } From 6a605b3be197b8d80a4bfe419db7b2fb284b3620 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Tue, 14 Jul 2026 17:55:25 -0700 Subject: [PATCH 08/51] Reprocess: added option -abort_disconnected_sessions (#1738) * Reprocess: added option -abort_disconnected_sessions (to make reprocess returns error if not all new session s are connected at the end) * comments * updated log * added warnings when option to abort is not enabled --- tools/Reprocess/main.cpp | 139 ++++++++++++++++++++++++++++++++++----- 1 file changed, 123 insertions(+), 16 deletions(-) diff --git a/tools/Reprocess/main.cpp b/tools/Reprocess/main.cpp index a810ed8c..fea524b2 100644 --- a/tools/Reprocess/main.cpp +++ b/tools/Reprocess/main.cpp @@ -98,6 +98,7 @@ void showUsage() " -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" + " -abort_disconnected_sessions Return error if not all sessions are connected together.\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" @@ -231,6 +232,29 @@ void showLocalizationStats(const std::string & outputDatabasePath) ++sessionCount; } +std::set getMapIds(const std::set & ids, const rtabmap::DBDriver & driver) +{ + std::set mapIds; + for(auto id: ids) + { + Transform p; + int mapId; + int w; + std::string l; + double s; + Transform gt; + std::vector v; + GPS g; + EnvSensors ss; + + if(driver.getNodeInfo(id, p, mapId, w, l, s, gt, v, g, ss) && mapId>=0) + { + mapIds.insert(mapId); + } + } + return mapIds; +} + int main(int argc, char * argv[]) { signal(SIGABRT, &sighandler); @@ -283,6 +307,7 @@ int main(int argc, char * argv[]) bool ignoreImu = false; bool republishLoopClosures = false; bool pubInterNodesAsNormalNodes = false; + bool abortDisconnectedSessions = false; bool locNull = false; bool originalGraphAsGT = false; bool scanFromDepth = false; @@ -523,6 +548,11 @@ int main(int argc, char * argv[]) pubInterNodesAsNormalNodes = true; printf("Republish intermdiate nodes as normal nodes (-pub_inter_as_normal option).\n"); } + else if(strcmp(argv[i], "-abort_disconnected_sessions") == 0 || strcmp(argv[i], "--abort_disconnected_sessions") == 0) + { + abortDisconnectedSessions = true; + printf("Will abort if not all sessions are connected at the end (-abort_disconnected_sessions option).\n"); + } else if(strcmp(argv[i], "-loc_null") == 0 || strcmp(argv[i], "--loc_null") == 0) { locNull = true; @@ -668,14 +698,14 @@ int main(int argc, char * argv[]) std::list databases = uSplit(inputDatabasePath, ';'); if (databases.empty()) { - printf("No input database \"%s\" detected!\n", inputDatabasePath.c_str()); + printf("[Error] No input database \"%s\" detected!\n", inputDatabasePath.c_str()); return 1; } for (std::list::iterator iter = databases.begin(); iter != databases.end(); ++iter) { if (!UFile::exists(*iter)) { - printf("Input database \"%s\" doesn't exist!\n", iter->c_str()); + printf("[Error] Input database \"%s\" doesn't exist!\n", iter->c_str()); if(uStrContains(inputDatabasePath,":")) { printf("Did you mean \"%s\"?\n", uReplaceChar(inputDatabasePath, ':', ";").c_str()); @@ -685,14 +715,14 @@ int main(int argc, char * argv[]) if (UFile::getExtension(*iter).compare("db") != 0) { - printf("File \"%s\" is not a database format (*.db)!\n", iter->c_str()); + printf("[Error] File \"%s\" is not a database format (*.db)! Aborting.\n", iter->c_str()); return 1; } } if(UFile::getExtension(outputDatabasePath).compare("db") != 0) { - printf("File \"%s\" is not a database format (*.db)!\n", outputDatabasePath.c_str()); + printf("[Error] File \"%s\" is not a database format (*.db)! Aborting.\n", outputDatabasePath.c_str()); return 1; } @@ -705,7 +735,7 @@ int main(int argc, char * argv[]) std::shared_ptr dbDriver(DBDriver::create()); if(!dbDriver->openConnection(databases.front(), false)) { - printf("Failed opening the input database!\n"); + printf("[Error] Failed opening the input database!\n"); return 1; } @@ -719,7 +749,7 @@ int main(int argc, char * argv[]) std::shared_ptr lastDbDriver(DBDriver::create()); if(!lastDbDriver->openConnection(databases.back(), true)) { - printf("Failed opening the last input database!\n"); + printf("[Error] Failed opening the last input database!\n"); return 1; } parameters = lastDbDriver->getLastParameters(); @@ -732,7 +762,7 @@ int main(int argc, char * argv[]) } 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", targetVersion.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)); } @@ -787,7 +817,7 @@ int main(int argc, char * argv[]) if(databases.size() > 1) { printf("[Warning] \"pub_loops\" option cannot be used with multiple databases input. " - "Disabling \"pub_loops\" to avoid mismatched loop closue ids.\n"); + "Disabling \"pub_loops\" to avoid mismatched loop closure ids.\n"); republishLoopClosures = false; } else @@ -831,11 +861,30 @@ int main(int argc, char * argv[]) dbDriver->getAllNodeIds(ids, false, false, !pubInterNodesAsNormalNodes && !intermediateNodes); if(ids.empty()) { - printf("Input database doesn't have any nodes saved in it.\n"); + printf("[Error] Input database doesn't have any nodes saved in it.\n"); dbDriver->closeConnection(false); return 1; } - if(!((!incrementalMemory || appendMode) && databases.size() > 1)) + + int firstDbMapIds = 0; + int firstDbLastMapId = -1; + firstDbMapIds = getMapIds(ids, *dbDriver).size(); + if(appendMode || !incrementalMemory) + { + if(databases.size() < 2) + { + if(appendMode) { + printf("[Error] -a (append mode) requires at least 2 input databases, provided \"%s\"! Aborting.\n", inputDatabasePath.c_str()); + } + else if(!incrementalMemory) { + printf("[Error] Localization mode is enabled (%s=false), but it requires at least 2 input databases, provided \"%s\"! Aborting.\n", + Parameters::kMemIncrementalMemory().c_str(), inputDatabasePath.c_str()); + } + return 1; + } + dbDriver->getLastMapId(firstDbLastMapId); + } + else { totalIds = ids.size(); } @@ -849,16 +898,18 @@ int main(int argc, char * argv[]) dbDriver->closeConnection(false); // Count remaining ids in the other databases + int newDbMapIds = 0; for (std::list::iterator iter = ++databases.begin(); iter != databases.end(); ++iter) { if (!dbDriver->openConnection(*iter, false)) { - printf("Failed opening input database!\n"); + printf("[Error] Failed opening input database!\n"); return 1; } ids.clear(); dbDriver->getAllNodeIds(ids, false, false, !pubInterNodesAsNormalNodes && !intermediateNodes); totalIds += ids.size(); + newDbMapIds += getMapIds(ids, *dbDriver).size(); dbDriver->closeConnection(false); } dbDriver.reset(); @@ -895,11 +946,30 @@ int main(int argc, char * argv[]) Parameters::parse(parameters, Parameters::kRGBDEnabled(), rgbdEnabled); bool odometryIgnored = !rgbdEnabled; + if(rgbdEnabled && appendMode) + { + // Get the number of sessions linked in the current global graph + std::map poses; + std::multimap constraints; + rtabmap.getGraph(poses, constraints, /*optimized*/ false, /*global*/ true, 0, false, false, false, false, false, false); + std::set mapIds; + for(std::map::iterator iter=poses.begin(); iter!=poses.end(); ++iter) + { + int id; + if((id=rtabmap.getMemory()->getMapId(iter->first, true))>=0) + { + mapIds.insert(id); + } + } + printf("Sessions in global graph of the initial database: %ld/%d\n", mapIds.size(), firstDbMapIds); + firstDbMapIds = mapIds.size(); + } + if(!cameraLocalTransformOffsetOverrides.empty()) { if(!cameraLocalTransformOverrides.empty() && cameraLocalTransformOffsetOverrides.size() > 1 && cameraLocalTransformOffsetOverrides.size() != cameraLocalTransformOverrides.size()) { - printf("Error: -cam_tf_lens_offset size (%ld) is not equal to -cam_tf argument (%ld). " + printf("[Error] -cam_tf_lens_offset size (%ld) is not equal to -cam_tf argument (%ld). " "-cam_tf_lens_offset should be one to affect all cameras or same size than -cam_tf argument.\n", cameraLocalTransformOffsetOverrides.size(), cameraLocalTransformOverrides.size()); showUsage(); @@ -909,7 +979,7 @@ int main(int argc, char * argv[]) { if(cameraLocalTransformOffsetOverrides.size() > 1) { - printf("Error: -cam_tf_lens_offset size (%ld) should be one if -cam_tf is not set.\n", + printf("[Error] -cam_tf_lens_offset size (%ld) should be one if -cam_tf is not set.\n", cameraLocalTransformOffsetOverrides.size()); showUsage(); return 1; @@ -1406,21 +1476,58 @@ int main(int argc, char * argv[]) 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) + if(rgbdEnabled && incrementalMemory) { std::map poses; std::multimap constraints; - rtabmap.getGraph(poses, constraints, 0, 1, 0, false, false, false, false, false, false); + rtabmap.getGraph(poses, constraints, /*optimized*/ false, /*global*/ true, 0, false, false, false, false, false, false); std::set mapIds; + std::set newMapIds; for(std::map::iterator iter=poses.begin(); iter!=poses.end(); ++iter) { int id; if((id=rtabmap.getMemory()->getMapId(iter->first, true))>=0) { mapIds.insert(id); + if(id>firstDbLastMapId) + { + newMapIds.insert(id); + } + } + } + printf("Sessions linked to the last pose: %ld/%d\n", mapIds.size(), firstDbMapIds + newDbMapIds); + if(appendMode) + { + printf("New sessions connected to the global graph: %ld/%d\n", newMapIds.size(), newDbMapIds); + // Check if all new sessions were appended to the original graph. + if((int)newMapIds.size() != newDbMapIds) + { + if(abortDisconnectedSessions) { + printf("[Error] Not all new sessions are connected (option -abort_disconnected_sessions is set).\n"); + return 1; + } + printf("[Warning] Not all new sessions are connected. Add option -abort_disconnected_sessions to report this as an error.\n"); + } + if(mapIds.size() == newMapIds.size()) + { + if(abortDisconnectedSessions) { + printf("[Error] The new sessions are not connected to original map (option -abort_disconnected_sessions is set).\n"); + return 1; + } + printf("[Warning] The new sessions are not connected to original map. Add option -abort_disconnected_sessions to report this as an error.\n"); + } + } + else // we reprocessed everything + { + if((int)mapIds.size() != firstDbMapIds + newDbMapIds) + { + if(abortDisconnectedSessions) { + printf("[Error] Not all sessions are connected (option -abort_disconnected_sessions is set).\n"); + return 1; + } + printf("[Warning] Not all sessions are connected. Add option -abort_disconnected_sessions to report this as an error.\n"); } } - printf("Sessions linked to last pose: %ld/%ld\n", mapIds.size(), databases.size()); } } From 0a41263fbaf941f4f5ecf77ad03072f7c6a410fe Mon Sep 17 00:00:00 2001 From: matlabbe Date: Thu, 16 Jul 2026 15:10:57 -0700 Subject: [PATCH 09/51] CI: macos pin opencv4 till #1732 is merged (#1740) --- .github/workflows/cmake-macos.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cmake-macos.yml b/.github/workflows/cmake-macos.yml index a22e3152..b8e8d92a 100644 --- a/.github/workflows/cmake-macos.yml +++ b/.github/workflows/cmake-macos.yml @@ -38,7 +38,7 @@ jobs: - name: Install Brew Dependencies run: | # Update brew and install from Brewfile if present, or specific packages - brew install pcl opencv octomap pdal yaml-cpp librealsense libfreenect libusb zlib libomp suite-sparse ceres-solver + brew install pcl opencv@4 octomap pdal yaml-cpp librealsense libfreenect libusb zlib libomp suite-sparse ceres-solver - name: Install Source Dependencies # Build (and per-dependency cache) the source-only deps not available from From f569393de132c7047f32b1036e34e0e547a3e9cf Mon Sep 17 00:00:00 2001 From: matlabbe Date: Thu, 16 Jul 2026 15:32:57 -0700 Subject: [PATCH 10/51] Update README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 427c7322..4fd0ec1a 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ This project is supported by [IntRoLab - Intelligent / Interactive / Integrated Build Status - ROS 2 + ROS 2 Humble Build Status @@ -68,6 +68,10 @@ This project is supported by [IntRoLab - Intelligent / Interactive / Integrated Kilted + Build Status + + + Lyrical Build Status From 5fad2d29c347e21f5ececb1701148c72d0c4dbb8 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Fri, 17 Jul 2026 15:05:12 -0700 Subject: [PATCH 11/51] Flann index serialization: deprecate rebalancing factor (#1739) * Flann index serialization: deprecate balancing factor * updated log msg --- corelib/src/FlannIndex.cpp | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/corelib/src/FlannIndex.cpp b/corelib/src/FlannIndex.cpp index b6b09a4c..57cd1217 100644 --- a/corelib/src/FlannIndex.cpp +++ b/corelib/src/FlannIndex.cpp @@ -31,9 +31,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include -#ifdef WIN32 #include -#endif #include "rtflann/flann.hpp" #include @@ -179,8 +177,8 @@ std::vector FlannIndex::serializeIndex(bool computeChecksum) cons indexData.resize(bytes_written+headerSizeBytes); indexData.shrink_to_fit(); - int rebalancingFactorAsInt; - memcpy(&rebalancingFactorAsInt, &rebalancingFactor_, sizeof(rebalancingFactor_)); + int rebalancingFactorAsInt; // Deprecated + memcpy(&rebalancingFactorAsInt, &rebalancingFactor_, sizeof(rebalancingFactor_)); // Deprecated int crcValueAsInt; memcpy(&crcValueAsInt, &crcValue, sizeof(crcValue)); int header[FLANN_INDEX_HEADER_SIZE] = { @@ -188,7 +186,7 @@ std::vector FlannIndex::serializeIndex(bool computeChecksum) cons algorithm_, // 3, featuresDim_, // 4, useDistanceL1_?1:0, // 5, - rebalancingFactorAsInt, // 6, + rebalancingFactorAsInt, // 6, Deprecated dataRows, // 7, dataCols, // 8, dataType, // 9, @@ -199,7 +197,7 @@ std::vector FlannIndex::serializeIndex(bool computeChecksum) cons header[3], header[4], header[5], - rebalancingFactor_, + rebalancingFactor_, // Deprecated header[7], header[8], header[9], crcValueAsInt, header[11]); memcpy(indexData.data(), header, headerSizeBytes); @@ -405,8 +403,8 @@ bool FlannIndex::loadIndex( int savedAlgorithm = header[3]; int savedDim = header[4]; bool savedDistanceL1 = header[5]==1; - float savedRebalancingFactor; - memcpy(&savedRebalancingFactor, &header[6], sizeof(header[6])); + float savedRebalancingFactor; // Deprecated + memcpy(&savedRebalancingFactor, &header[6], sizeof(header[6])); // Deprecated int savedRows = header[7]; int savedCols = header[8]; int savedType = header[9]; @@ -414,12 +412,13 @@ bool FlannIndex::loadIndex( 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", + UDEBUG("Header: \"%d.%d.%d\" alg=%d dim=%d L1=%d factor=%f (deprecated, using %f instead) data(%dx%d type=%d, crc=%X) index size = %d bytes", header[0],header[1],header[2], header[3], header[4], header[5], - savedRebalancingFactor, + savedRebalancingFactor, // Deprecated + rebalancingFactor, header[7], header[8], header[9], savedCrc, header[11]); @@ -441,12 +440,6 @@ bool FlannIndex::loadIndex( } 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); From 89998284bc3a1b3f7c0aee92874183892f1d8ff9 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Wed, 29 Jul 2026 08:19:13 -0700 Subject: [PATCH 12/51] Reduce graph: added option to remove orphan nodes from WM after reduction (#1735) * Reduce graph: remove orphan nodes from WM after reduction * fixed eror * ignore ids< 0 * add warning and cleanup only when option is used * updated feedback * Abort --sync_wm_and_opt_graph if a lot more WM nodes have to be transferred. * typo * refactored... completly * fixed optimized graph cleared * updated option description * updated comment * Added dummy dictionary function to speedup initialization when we dont need the dictionary fully loaded in memory. * Fixed nodes weight not modified in db when deleted * Removed AutoUpdate parameter, not needed * Fixed dummy dictionary usage for detectMoreLoopClosures. Added checks to disable graph reduction when intermediate nodes are detected. * updated log --- corelib/include/rtabmap/core/DBDriver.h | 4 +- .../include/rtabmap/core/DBDriverSqlite3.h | 2 +- corelib/include/rtabmap/core/Memory.h | 6 +- corelib/include/rtabmap/core/Rtabmap.h | 3 + corelib/include/rtabmap/core/Signature.h | 6 +- corelib/src/DBDriver.cpp | 4 +- corelib/src/DBDriverSqlite3.cpp | 51 +++-- corelib/src/Memory.cpp | 114 ++++++++-- corelib/src/Rtabmap.cpp | 19 +- corelib/src/VWDictionary.cpp | 2 +- tools/DetectMoreLoopClosures/main.cpp | 10 +- tools/ReduceGraph/main.cpp | 211 +++++++++++++++++- 12 files changed, 372 insertions(+), 60 deletions(-) diff --git a/corelib/include/rtabmap/core/DBDriver.h b/corelib/include/rtabmap/core/DBDriver.h index 847caa50..cc451dd3 100644 --- a/corelib/include/rtabmap/core/DBDriver.h +++ b/corelib/include/rtabmap/core/DBDriver.h @@ -162,7 +162,7 @@ public: void executeNoResult(const std::string & sql) const; // Load objects - void load(VWDictionary & dictionary, bool lastStateOnly = true) const; + void load(VWDictionary & dictionary, bool lastStateOnly = true, bool idsOnly = false) 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, bool loadWordIdsOnly = false); // returned signatures must be freed after usage @@ -279,7 +279,7 @@ protected: virtual void saveFlannIndexQuery(const std::vector & indexData) const = 0; // Load objects - virtual void loadQuery(VWDictionary & dictionary, bool lastStateOnly = true) const = 0; + virtual void loadQuery(VWDictionary & dictionary, bool lastStateOnly = true, bool idsOnly = false) 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; diff --git a/corelib/include/rtabmap/core/DBDriverSqlite3.h b/corelib/include/rtabmap/core/DBDriverSqlite3.h index a5df8e92..e73bab86 100644 --- a/corelib/include/rtabmap/core/DBDriverSqlite3.h +++ b/corelib/include/rtabmap/core/DBDriverSqlite3.h @@ -138,7 +138,7 @@ protected: virtual void saveFlannIndexQuery(const std::vector & indexData) const; // Load objects - virtual void loadQuery(VWDictionary & dictionary, bool lastStateOnly = true) const; + virtual void loadQuery(VWDictionary & dictionary, bool lastStateOnly = true, bool idsOnly = false) 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; diff --git a/corelib/include/rtabmap/core/Memory.h b/corelib/include/rtabmap/core/Memory.h index ab3df0a7..040b8516 100644 --- a/corelib/include/rtabmap/core/Memory.h +++ b/corelib/include/rtabmap/core/Memory.h @@ -141,11 +141,12 @@ public: const std::map & optimizedPoses, int maxGraphDepth) const; void convertToIntermediate(int locationId); - void deleteLocation(int locationId, std::list * deletedWords = 0); + void deleteLocation(int locationId, std::list * deletedWords = 0, bool keepLinkedInDb = false); void saveLocationData(int locationId); void removeLink(int idA, int idB); 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); + void setDummyDictionary(bool enabled); //getters const std::map & getWorkingMem() const {return _workingMem;} @@ -279,7 +280,6 @@ private: std::list getRemovableSignatures(int count, const std::set & ignoredIds = std::set()); int getNextId(); - void initCountId(); void rehearsal(Signature * signature, Statistics * stats = 0); bool rehearsalMerge(int oldId, int newId); bool canBeReduced(const Link & link, float maxDistance, int direction); @@ -396,6 +396,8 @@ private: MarkerDetector * _markerDetector; GlobalDescriptorExtractor * _globalDescriptorExtractor; + + bool _dummyDictionary; }; } // namespace rtabmap diff --git a/corelib/include/rtabmap/core/Rtabmap.h b/corelib/include/rtabmap/core/Rtabmap.h index b3d438e8..930aac21 100644 --- a/corelib/include/rtabmap/core/Rtabmap.h +++ b/corelib/include/rtabmap/core/Rtabmap.h @@ -229,6 +229,7 @@ public: bool addLink(const Link & link); cv::Mat getInformation(const cv::Mat & covariance) const; void addNodesToRepublish(const std::vector & ids); + void setDummyDictionary(bool enabled); int getPathStatus() const {return _pathStatus;} // -1=failed 0=idle/executing 1=success void clearPath(int status); // -1=failed 0=idle/executing 1=success @@ -402,6 +403,8 @@ private: int _pathStuckCount; float _pathStuckDistance; + bool _dummyDictionary; + #ifdef RTABMAP_PYTHON PythonInterface * _python; #endif diff --git a/corelib/include/rtabmap/core/Signature.h b/corelib/include/rtabmap/core/Signature.h index 9dbbe532..fbb3e217 100644 --- a/corelib/include/rtabmap/core/Signature.h +++ b/corelib/include/rtabmap/core/Signature.h @@ -62,7 +62,7 @@ public: virtual ~Signature(); /** - * Must return a value between >=0 and <=1 (1 means 100% similarity). + * Must return a value between >=0 and <=1 (1 means 100% similarity). */ float compareTo(const Signature & signature) const; bool isBadSignature() const; @@ -70,10 +70,10 @@ public: int id() const {return _id;} int mapId() const {return _mapId;} - void setWeight(int weight) {_modified=_weight!=weight;_weight = weight;} + void setWeight(int weight) {_modified=_modified || _weight!=weight;_weight = weight;} int getWeight() const {return _weight;} - void setLabel(const std::string & label) {_modified=_label.compare(label)!=0;_label = label;} + void setLabel(const std::string & label) {_modified=_modified || _label.compare(label)!=0;_label = label;} const std::string & getLabel() const {return _label;} double getStamp() const {return _stamp;} diff --git a/corelib/src/DBDriver.cpp b/corelib/src/DBDriver.cpp index d71c1400..a78b181e 100644 --- a/corelib/src/DBDriver.cpp +++ b/corelib/src/DBDriver.cpp @@ -539,10 +539,10 @@ 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, bool idsOnly) const { _dbSafeAccessMutex.lock(); - this->loadQuery(dictionary, lastStateOnly); + this->loadQuery(dictionary, lastStateOnly, idsOnly); _dbSafeAccessMutex.unlock(); } diff --git a/corelib/src/DBDriverSqlite3.cpp b/corelib/src/DBDriverSqlite3.cpp index 66986051..1bce54a2 100644 --- a/corelib/src/DBDriverSqlite3.cpp +++ b/corelib/src/DBDriverSqlite3.cpp @@ -3538,7 +3538,7 @@ void DBDriverSqlite3::loadLastNodesQuery(std::list & nodes, bool lo } } -void DBDriverSqlite3::loadQuery(VWDictionary & dictionary, bool lastStateOnly) const +void DBDriverSqlite3::loadQuery(VWDictionary & dictionary, bool lastStateOnly, bool idsOnly) const { ULOGGER_DEBUG(""); if(_ppDb) @@ -3552,7 +3552,12 @@ void DBDriverSqlite3::loadQuery(VWDictionary & dictionary, bool lastStateOnly) c std::list visualWords; // Get the visual words - query << "SELECT id, descriptor_size, descriptor FROM Word "; + query << "SELECT id"; + if(!idsOnly) + { + query << ", descriptor_size, descriptor"; + } + query << " FROM Word "; if(lastStateOnly) { if(uStrNumCmp(_version, "0.11.11") >= 0) @@ -3581,27 +3586,29 @@ void DBDriverSqlite3::loadQuery(VWDictionary & dictionary, bool lastStateOnly) c int index=0; id = sqlite3_column_int(ppStmt, index++); // VisualWord Id - descriptorSize = sqlite3_column_int(ppStmt, index++); // VisualWord descriptor size - descriptor = sqlite3_column_blob(ppStmt, index); // VisualWord descriptor array - dRealSize = sqlite3_column_bytes(ppStmt, index++); - 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); - } + if(!idsOnly) { + descriptorSize = sqlite3_column_int(ppStmt, index++); // VisualWord descriptor size + descriptor = sqlite3_column_blob(ppStmt, index); // VisualWord descriptor array + dRealSize = sqlite3_column_bytes(ppStmt, index++); - memcpy(d.data, descriptor, dRealSize); + 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); + } VisualWord * vw = new VisualWord(id, d); vw->setSaved(true); dictionary.addWord(vw); @@ -3621,7 +3628,7 @@ void DBDriverSqlite3::loadQuery(VWDictionary & dictionary, bool lastStateOnly) c getLastWordId(id); dictionary.setLastWordId(id); - if(uStrNumCmp(_version, "0.23.0") >= 0) { + if(!idsOnly && uStrNumCmp(_version, "0.23.0") >= 0) { // load dictionary index std::stringstream query3; query3 << "SELECT dictionary_index " diff --git a/corelib/src/Memory.cpp b/corelib/src/Memory.cpp index e0ffac39..5b7dc5db 100644 --- a/corelib/src/Memory.cpp +++ b/corelib/src/Memory.cpp @@ -138,7 +138,8 @@ Memory::Memory(const ParametersMap & parameters) : _badSignRatio(Parameters::defaultKpBadSignRatio()), _tfIdfLikelihoodUsed(Parameters::defaultKpTfIdfLikelihoodUsed()), _parallelized(Parameters::defaultKpParallelized()), - _registrationVis(0) + _registrationVis(0), + _dummyDictionary(false) { _feature2D = Feature2D::create(parameters); _vwd = new VWDictionary(parameters); @@ -411,31 +412,55 @@ void Memory::loadDataFromDb(bool postInitClosingEvents) { if(wordIds.size()) { - std::list words; - _dbDriver->loadWords(wordIds, words); - for(std::list::iterator iter = words.begin(); iter!=words.end(); ++iter) + if(_dummyDictionary) { - _vwd->addWord(*iter); + for(std::set::iterator iter = wordIds.begin(); iter!=wordIds.end(); ++iter) + { + VisualWord * w = new VisualWord(*iter, cv::Mat()); + w->setSaved(true); + _vwd->addWord(w); // placeholder descriptor + } + } + else + { + std::list words; + _dbDriver->loadWords(wordIds, words); + for(std::list::iterator iter = words.begin(); iter!=words.end(); ++iter) + { + _vwd->addWord(*iter); + } } // Get Last word id int id = 0; _dbDriver->getLastWordId(id); _vwd->setLastWordId(id); } + else { + _dummyDictionary = false; + } } else { - _dbDriver->load(*_vwd, false); + _dbDriver->load(*_vwd, false, _dummyDictionary); } } else { UDEBUG("load words"); // load the last dictionary - _dbDriver->load(*_vwd, _vwd->isIncremental()); + _dbDriver->load(*_vwd, _vwd->isIncremental(), _dummyDictionary); + } + UDEBUG("%d words loaded! (type=%s, dim=%d)", + _vwd->getUnusedWordsSize(), + _vwd->getVisualWords().empty()?"NA":_vwd->getVisualWords().begin()->second->getDescriptor().empty()?"dummy":_vwd->getVisualWords().begin()->second->getDescriptor().type() == CV_32FC1?"float":"binary", + _vwd->getVisualWords().empty()?0:_vwd->getVisualWords().begin()->second->getDescriptor().cols); + UDEBUG("Dictionary memory usage: %ld Bytes (%ld MB)", _vwd->getMemoryUsed(), _vwd->getMemoryUsed()/(1024*1024)); + if(!_dummyDictionary) { + _vwd->update(); + } + else { + UDEBUG("Dictionary update skipped (dummy dictionary is enabled)"); } - 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..."))); @@ -496,6 +521,24 @@ void Memory::loadDataFromDb(bool postInitClosingEvents) UWARN("%s", msg.c_str()); if(postInitClosingEvents) UEventsManager::post(new RtabmapEventInit(msg)); + if(_dummyDictionary) + { + UWARN("Dummy dictionary cannot be used when repairing the dictionary, disabling dummy dictionary."); + for(std::map::const_iterator i=signatures.begin(); i!=signatures.end(); ++i) + { + Signature * s = this->_getSignature(i->first); + UASSERT(s != 0); + if(!s->isEnabled()) + { + break; + } + this->disableWordsRef(s->id()); + } + _vwd->deleteUnusedWords(); + _vwd->clear(); + _dummyDictionary = false; + } + //remove all words ref const std::map & addedWords = _vwd->getVisualWords(); @@ -595,6 +638,21 @@ void Memory::loadDataFromDb(bool postInitClosingEvents) UDEBUG("map ids start with %d", _idMapCount); } +void Memory::setDummyDictionary(bool enabled) +{ + if(_dbDriver != 0) { + UERROR("Dummy dictionary can only be set if the memory is not yet initialized. Ignoring."); + return; + } + if(enabled) { + UINFO("Dummy dictionary enabled."); + } + else { + UINFO("Dummy dictionary disabled."); + } + _dummyDictionary = enabled; +} + void Memory::saveFlannIndex(bool postInitClosingEvents) { if(!_dbDriver) { @@ -1324,6 +1382,11 @@ int Memory::reduceNode(int id, float maxDistance, bool keepLinkedInDb, int direc UWARN("Node %d is not in WM/STM, cannot reduce it.", id); return 0; } + else if(s->getWeight() == -1) + { + UWARN("Cannot reduce intermediate node %d (not supported).", id); + return 0; + } if(!s->getLabel().empty()) { @@ -1340,6 +1403,11 @@ int Memory::reduceNode(int id, float maxDistance, bool keepLinkedInDb, int direc { float distance = iter->second.transform().getNorm(); reducedTo = iter->second.to(); + if(this->_getSignature(reducedTo) == 0) + { + UWARN("Node %d is not in WM/STM, cannot reduce %d to it.", reducedTo, id); + return 0; + } UDEBUG("Reduce %d to %d (distance=%f)", s->id(), iter->second.to(), distance); } @@ -1347,6 +1415,18 @@ int Memory::reduceNode(int id, float maxDistance, bool keepLinkedInDb, int direc if(iter->second.type() == Link::kNeighbor) { neighbors.insert(*iter); + // neighbors should not be intermediate nodes + Signature * sTo = this->_getSignature(iter->first); + if(sTo == 0) + { + UWARN("Neighbor node %d is not in WM/STM, cannot reduce %d.", iter->first, id); + return 0; + } + else if(sTo->getWeight() == -1) + { + UWARN("Neighbor node %d is an intermediate node (not supported), cannot reduce %d.", iter->first, id); + return 0; + } } } if(reducedTo>0) @@ -1455,10 +1535,10 @@ void Memory::moveSignatureToWMFromSTM(int id, int * reducedToOut) else { std::multimap links = s->getLinks(); - // Setting true to make sure we save all visual + // Setting keepLinkedInDb=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); + reducedId = reduceNode(s->id(), 0, /*keepLinkedInDb*/ true); if(reducedToOut) { *reducedToOut = reducedId; } @@ -3132,13 +3212,18 @@ void Memory::convertToIntermediate(int locationId) } } -void Memory::deleteLocation(int locationId, std::list * deletedWords) +void Memory::deleteLocation(int locationId, std::list * deletedWords, bool keepLinkedInDb) { - UDEBUG("Deleting location %d", locationId); + UDEBUG("Deleting location %d (keepLinkedInDb=%s)", locationId, keepLinkedInDb?"true":"false"); Signature * location = _getSignature(locationId); if(location) { - this->moveToTrash(location, false, deletedWords); + this->moveToTrash(location, keepLinkedInDb, deletedWords); + _memoryChanged = true; + } + else + { + UWARN("Location %d has not been found in STM/WM, cannot delete it.", locationId); } } @@ -5092,6 +5177,7 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor data.depthOrRightRaw().rows, data.depthOrRightRaw().type(), CV_16UC1, CV_32FC1, CV_8UC1, CV_8UC3).c_str()); + UASSERT_MSG(!_dummyDictionary, "Memory::createSignature() cannot be called if the memory has been initialized with a dummy dictionary."); if(!data.depthOrRightRaw().empty() && data.cameraModels().empty() && diff --git a/corelib/src/Rtabmap.cpp b/corelib/src/Rtabmap.cpp index 1c64de81..89351adc 100644 --- a/corelib/src/Rtabmap.cpp +++ b/corelib/src/Rtabmap.cpp @@ -180,7 +180,8 @@ Rtabmap::Rtabmap() : _pathGoalIndex(0), _pathTransformToGoal(Transform::getIdentity()), _pathStuckCount(0), - _pathStuckDistance(0.0f) + _pathStuckDistance(0.0f), + _dummyDictionary(false) #ifdef RTABMAP_PYTHON ,_python(new PythonInterface()) #endif @@ -360,6 +361,10 @@ void Rtabmap::init(const ParametersMap & parameters, const std::string & databas if(!_memory) { _memory = new Memory(allParameters); + if(_dummyDictionary) + { + _memory->setDummyDictionary(true); + } _memory->init(_databasePath, false, allParameters, true); } @@ -6934,6 +6939,18 @@ void Rtabmap::addNodesToRepublish(const std::vector & ids) } } +void Rtabmap::setDummyDictionary(bool enabled) +{ + if(_memory) { + UERROR("Memory is already initialized, cannot set dummy dictionary. This " + "function can only be called after Rtabmap object is created, but " + "before init() is called."); + } + else { + _dummyDictionary = true; + } +} + void Rtabmap::clearPath(int status) { UINFO("status=%d", status); diff --git a/corelib/src/VWDictionary.cpp b/corelib/src/VWDictionary.cpp index 952e012c..ff979825 100644 --- a/corelib/src/VWDictionary.cpp +++ b/corelib/src/VWDictionary.cpp @@ -108,7 +108,7 @@ void VWDictionary::parseParameters(const ParametersMap & parameters) incrementalDictionary = uStr2Bool((*iter).second.c_str()); } - // Verifying hypotheses strategy + // Verifying NN strategy bool treeUpdated = false; if((iter=parameters.find(Parameters::kKpNNStrategy())) != parameters.end()) { diff --git a/tools/DetectMoreLoopClosures/main.cpp b/tools/DetectMoreLoopClosures/main.cpp index 2f4fb31f..d15374e7 100644 --- a/tools/DetectMoreLoopClosures/main.cpp +++ b/tools/DetectMoreLoopClosures/main.cpp @@ -249,9 +249,15 @@ int main(int argc, char * argv[]) UTimer timer; ParametersMap originalParameters = parameters; uInsert(parameters, inputParams); + + // This avoids to load original descriptors in the dictionary + // to save RAM and intialization time (we don't need the dictionary for this tool) + rtabmap.setDummyDictionary(true); // should be set before Rtabmap::init() + rtabmap.init(parameters, dbPath); printf("Initialization... done! (%f sec)\n", timer.ticks()); + // detectMoreLoopClosures would clear the optimized map if loop closures are detected float xMin, yMin, cellSize; bool haveOptimizedMap = !rtabmap.getMemory()->load2DMap(xMin, yMin, cellSize).empty(); @@ -311,7 +317,7 @@ int main(int argc, char * argv[]) // Restore original parameters before saving back the database rtabmap.parseParameters(originalParameters); - rtabmap.close(); + rtabmap.close(detected>0); - return 0; + return detected>=0; } diff --git a/tools/ReduceGraph/main.cpp b/tools/ReduceGraph/main.cpp index 5d44ab9e..2046b643 100644 --- a/tools/ReduceGraph/main.cpp +++ b/tools/ReduceGraph/main.cpp @@ -26,6 +26,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include +#include #include #include #include @@ -56,9 +57,19 @@ void showUsage(const char * exec) "%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" + " --keep_linked Keep reduced nodes linked to graph (by only child->parent link)\n" + " instead of invalidating them. When --remove_orphan_nodes is used,\n" + " orphan nodes are simply transfered to LTM instead of being invalidated.\n" + " --pre_cleanup Remove all user loop closures linking nodes closer than %s nodes 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" + " --remove_orphan_nodes Remove all orphan nodes created by graph reduction \n" + " from WM and LTM. This assumes that the original global \n" + " graph connected all nodes in WM and LTM, otherwise it is skipped.\n" + " --remove_all_orphan_nodes Remove all orphan nodes created or not by graph \n" + " reduction from WM and LTM. Warning: this could remove \n" + " completly unconnected graphes from WM and LTM. Usage of \n" + " --remove_orphan_nodes is safer. Backup your database \n" + " before trying this.\n" " --udebug/--uinfo/--warn can also be used to change verbosity.\n" "\n", exec, Parameters::kMemSTMSize().c_str()); exit(1); @@ -78,6 +89,8 @@ int main(int argc, char * argv[]) bool keepLinked = false; float radius = 1.0f; bool preCleanup = false; + bool removeOrphanNodes = false; + bool removeAllOrphanNodes = false; for(int i=1; i wm; if(driver->openConnection(dbPath)) { parameters = driver->getLastParameters(); + driver->getLastNodeIds(wm); driver->closeConnection(false); } else @@ -154,7 +180,13 @@ int main(int argc, char * argv[]) } delete driver; + size_t wmOrgSize = wm.size(); Memory memory; + + // This avoids to load original descriptors in the dictionary + // to save RAM and intialization time (we don't need the dictionary for this tool) + memory.setDummyDictionary(true); // should be set before Memory::init() + printf("Initialization...\n"); UTimer timer; ParametersMap originalParameters = parameters; @@ -173,10 +205,53 @@ int main(int argc, char * argv[]) return 1; } - Transform lastLocalizationPose; - std::map optimizedPoses = memory.loadOptimizedPoses(&lastLocalizationPose); - float xMin, yMin, cellSize; - bool hasOptimizedMap = !memory.load2DMap(xMin, yMin, cellSize).empty(); + bool isWholeGraphConnected = false; + std::shared_ptr optimizer(Optimizer::create(parameters)); + + std::map poses; + std::multimap constraints; + memory.getMetricConstraints(ids, poses, constraints, false, true); + if(!poses.empty()) + { + std::map posesOut; + std::multimap linksOut; + optimizer->getConnectedGraph( + keepLatest?poses.rbegin()->first:poses.begin()->first, + poses, + constraints, + posesOut, + linksOut); + isWholeGraphConnected = posesOut.size() == poses.size(); + + if(isWholeGraphConnected) + { + printf("The whole global graph is connected to all nodes of WM/LTM (%ld/%ld).\n", + posesOut.size(), ids.size()); + } + else { + //Count number of nodes not in global graph that are in WM + int missing = 0; + for(auto id:wm) + { + if(posesOut.find(id) == posesOut.end()) { + ++missing; + } + } + if(missing>0) + { + printf("The whole global graph is not connected to all nodes of WM/LTM (%ld/%ld) " + "with some (%d) of the disconnected nodes in WM.%s\n", + posesOut.size(), ids.size(), missing, + removeAllOrphanNodes?"":" You may consider using --remove_all_orphan_nodes to remove these nodes from WM if necessary."); + } + else + { + printf("The whole global graph is not connected to all nodes of WM/LTM (%ld/%ld), " + "though no disconnected nodes are in WM.\n", + posesOut.size(), ids.size()); + } + } + } int totalNodesReduced = 0; std::vector vids; @@ -192,6 +267,7 @@ int main(int argc, char * argv[]) vids.insert(vids.end(), ids.rbegin(), ids.rend()); } + int totalLinksRemoved = 0; if(preCleanup) { if(memory.getMaxStMemSize() <= 1) @@ -200,7 +276,6 @@ int main(int argc, char * argv[]) } else { - int totalRemoved = 0; for(auto id: vids) { auto nids = memory.getNeighborsId(id, memory.getMaxStMemSize(), -1, true, true, true); @@ -211,15 +286,20 @@ int main(int argc, char * argv[]) nids.find(link.first)!=nids.end()) { memory.removeLink(id, link.first); - ++totalRemoved; + ++totalLinksRemoved; } } } 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()); + totalLinksRemoved, Parameters::kMemSTMSize().c_str(), memory.getMaxStMemSize()); } } + // Get local optimized graph before reduction + Transform lastLocalizationPose; + std::map optimizedPoses = memory.loadOptimizedPoses(&lastLocalizationPose); + + // Graph reduction for(auto id: vids) { // Nodes can be already reduced by other nodes, check if they are still there @@ -234,6 +314,82 @@ int main(int argc, char * argv[]) } } printf("Reduced a total of %d nodes out of %ld nodes\n", totalNodesReduced, ids.size()); + if(totalNodesReduced==0 && totalLinksRemoved==0 && !removeOrphanNodes) + { + printf("Nothing to do, exiting without updating the database.\n"); + memory.close(false); + return 0; + } + memory.emptyTrash(); + memory.joinTrashThread(); + + if(isWholeGraphConnected || removeAllOrphanNodes) + { + // refetch the global graph after graph reduction + ids = memory.getAllSignatureIds(); + + // Check if some nodes got disconnected from the global graph + size_t totalBefore = poses.size(); + poses.clear(); + constraints.clear(); + memory.getMetricConstraints(ids, poses, constraints, false, true); + std::map posesOut; + std::multimap linksOut; + optimizer->getConnectedGraph( + keepLatest?poses.rbegin()->first:poses.lower_bound(0)->first, + poses, + constraints, + posesOut, + linksOut); + + isWholeGraphConnected = posesOut.size() == poses.size(); + + if(!isWholeGraphConnected) + { + if(removeOrphanNodes) + { + printf("Option %s is enabled, let's cleanup WM and LTM " + "%ld from nodes not in the global graph anymore (%ld -> reduced to %ld -> %ld connected globally).\n", + removeAllOrphanNodes?"--remove_all_orphan_nodes":"--remove_orphan_nodes", + poses.size()-posesOut.size(), + totalBefore, ids.size(), posesOut.size()); + // Not all graph is connected anymore while it was before reduction: + // that means we created orphan nodes, remove them + int transferred = 0; + for(std::map::iterator iter=poses.lower_bound(0); iter!=poses.end(); ++iter) + { + if(posesOut.find(iter->first) == posesOut.end()) + { + memory.deleteLocation(iter->first, 0, keepLinked); + if(keepLinked) { + printf("Transferred %d to LTM (--keep_linked).\n", iter->first); + } + else { + printf("Removed %d from WM/LTM.\n", iter->first); + } + wm.erase(iter->first); + ++transferred; + } + } + if(transferred>0) { + memory.emptyTrash(); + memory.joinTrashThread(); + } + } + else { + printf("[Warning] After graph reduction, the global graph is not all " + "connected anymore while it was before (%ld -> reduced to %ld -> %ld connected globally). " + "Add %s option to remove the %ld orphan nodes from WM and LTM.\n", + totalBefore, ids.size(), posesOut.size(), + removeAllOrphanNodes?"--remove_all_orphan_nodes":"--remove_orphan_nodes", + poses.size()-posesOut.size()); + } + } + else + { + printf("Whole optimized graph is still all connected after graph reduction (%ld/%ld)\n", posesOut.size(), ids.size()); + } + } // else: we cannot detect orphan nodes if the original graph was not all connected. if(!optimizedPoses.empty()) { @@ -251,9 +407,13 @@ int main(int argc, char * argv[]) ++iter; } } - printf("Updated optimized graph from %ld poses to %ld poses\n", optimizedPoses.size()+removed, optimizedPoses.size()); + printf("Updated local optimized graph from %ld poses to %ld poses\n", optimizedPoses.size()+removed, optimizedPoses.size()); + printf("Saving back %ld optimized poses to database.\n", optimizedPoses.size()); memory.saveOptimizedPoses(optimizedPoses, lastLocalizationPose); } + + float xMin, yMin, cellSize; + bool hasOptimizedMap = !memory.load2DMap(xMin, yMin, cellSize).empty(); if(hasOptimizedMap) { printf("The database has a global occupancy grid, regenerating one with the remaining nodes of the optimized graph!\n"); @@ -280,6 +440,37 @@ int main(int argc, char * argv[]) // Restore original parameters before saving back the database memory.parseParameters(originalParameters); + + // Restore Working Memory (Mem/InitWMWithAllNodes is used above): + // When memory is closing, it updates the Info table with current time, + // then move to trash the nodes afterwards so that nodes's update time + // in the database is greater than last info entry. This is how rtabmap + // knows which nodes are in working memory. The idea here is the move to + // trash nodes that were not in original WM before closing Memory. We can + // use deleteLocation with keepLinkedInDb=true to achieve what Memory::clear() does. + ids = memory.getAllSignatureIds(); // LTM ids + // Count number of nodes in WM that were reduced (directly/indirectly) + int wmReduced = 0; + for(auto id:wm) + { + if(ids.find(id) == ids.end()) { + ++wmReduced; + } + } + printf("Restoring Working Memory (org:%ld -> reduced:%ld)...\n", wmOrgSize, wm.size() - wmReduced); + int transferred = 0; + for(auto id:ids) + { + if(wm.find(id) == wm.end() && memory.getWorkingMem().find(id) != memory.getWorkingMem().end()) { + memory.deleteLocation(id, 0, /*keepLinkedInDb*/ true); + ++transferred; + } + } + if(transferred>0) { + memory.emptyTrash(); + memory.joinTrashThread(); + } + printf("Restoring Working Memory... done! Transferred %d nodes.\n", transferred); printf("Saving all changes to database...\n"); memory.close(true); From d9f3337f978fffd9cf4b04bee15636bf00a2b870 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Wed, 29 Jul 2026 22:48:39 -0700 Subject: [PATCH 13/51] OpenCV 5 support (#1732) * OpenCV 5 support * RTABMapConfig.cmake, guard from including stereoRectifyFisheye.h * unified opencv components at the same place * fixing android build * Confirmed stereo calibration with fisheye works. Fix camera start/top progress dialog not drawn. Opencv >=4.7 using new opencv's ArucoDetector class. * pinning opencv for downstream apps * Avoid changing object/image points between fisheye calibration * Fixed stereo calib diverging when recalibrating same data * removed not needed opencv c api * fixing depthai build on opencv5 * bumped version * Adding ci opencv5 with homebrew * fixed ci script * Fixed OptimizerCeres build with opencv5 --- .../install-macos-source-deps/action.yml | 3 +- .github/workflows/cmake-macos.yml | 14 +- CMakeLists.txt | 37 ++- RTABMapConfig.cmake.in | 2 +- .../include/rtabmap/core/DBDriverSqlite3.h | 4 + .../include/rtabmap/core/EpipolarGeometry.h | 4 + corelib/include/rtabmap/core/Features2d.h | 16 +- corelib/include/rtabmap/core/MarkerDetector.h | 11 +- corelib/include/rtabmap/core/Memory.h | 4 + corelib/include/rtabmap/core/OdometryInfo.h | 4 + corelib/include/rtabmap/core/SensorData.h | 4 + corelib/include/rtabmap/core/Signature.h | 4 + corelib/include/rtabmap/core/Statistics.h | 4 + corelib/include/rtabmap/core/VWDictionary.h | 4 + .../core/stereo/stereoRectifyFisheye.h | 9 + .../rtabmap/core/util3d_correspondences.h | 5 + .../include/rtabmap/core/util3d_features.h | 5 + corelib/src/EpipolarGeometry.cpp | 5 +- corelib/src/Features2d.cpp | 40 ++- corelib/src/MarkerDetector.cpp | 97 +++++-- corelib/src/Memory.cpp | 8 +- corelib/src/RegistrationVis.cpp | 6 +- corelib/src/SensorCaptureThread.cpp | 17 +- corelib/src/StereoCameraModel.cpp | 12 +- corelib/src/camera/CameraDepthAI.cpp | 6 +- corelib/src/camera/CameraFreenect.cpp | 3 +- corelib/src/camera/CameraFreenect2.cpp | 17 +- corelib/src/camera/CameraImages.cpp | 6 +- corelib/src/camera/CameraK4A.cpp | 3 +- corelib/src/camera/CameraK4W2.cpp | 5 +- corelib/src/camera/CameraOpenNI2.cpp | 3 +- corelib/src/camera/CameraOpenNICV.cpp | 42 +-- corelib/src/camera/CameraOpenni.cpp | 3 +- corelib/src/camera/CameraRealSense.cpp | 3 +- corelib/src/camera/CameraRealSense2.cpp | 3 +- corelib/src/camera/CameraStereoDC1394.cpp | 5 +- corelib/src/camera/CameraStereoImages.cpp | 3 +- corelib/src/camera/CameraStereoTara.cpp | 16 +- corelib/src/camera/CameraStereoVideo.cpp | 46 ++-- corelib/src/camera/CameraStereoZedOC.cpp | 4 + corelib/src/camera/CameraVideo.cpp | 31 +-- corelib/src/odometry/OdometryDVO.cpp | 3 +- corelib/src/odometry/OdometryF2M.cpp | 4 + corelib/src/odometry/OdometryFovis.cpp | 5 +- corelib/src/odometry/OdometryMSCKF.cpp | 5 +- corelib/src/odometry/OdometryMono.cpp | 4 + corelib/src/odometry/OdometryORBSLAM2.cpp | 13 +- corelib/src/odometry/OdometryORBSLAM3.cpp | 5 +- corelib/src/odometry/OdometryOkvis.cpp | 3 +- corelib/src/odometry/OdometryOpenVINS.cpp | 5 +- corelib/src/odometry/OdometryVINSFusion.cpp | 5 +- corelib/src/odometry/OdometryViso2.cpp | 5 +- corelib/src/opencv/ORBextractor.cc | 4 + corelib/src/opencv/ORBextractor.h | 2 - corelib/src/opencv/Orb.cpp | 5 +- corelib/src/opencv/five-point.h | 4 + corelib/src/opencv/solvepnp.cpp | 10 +- corelib/src/opencv/solvepnp.h | 7 +- corelib/src/optimizer/OptimizerCeres.cpp | 6 + corelib/src/stereo/StereoBM.cpp | 10 +- corelib/src/stereo/StereoSGBM.cpp | 10 +- corelib/src/util2d.cpp | 14 +- corelib/src/util3d.cpp | 9 +- corelib/src/util3d_correspondences.cpp | 4 + corelib/src/util3d_surface.cpp | 16 +- guilib/include/rtabmap/gui/DatabaseViewer.h | 4 + guilib/include/rtabmap/gui/ImageView.h | 5 + guilib/include/rtabmap/gui/KeypointItem.h | 5 + guilib/src/CalibrationDialog.cpp | 249 +++++++++++++----- guilib/src/CameraViewer.cpp | 5 +- guilib/src/DatabaseViewer.cpp | 6 +- guilib/src/GuiUtil.h | 59 +++++ guilib/src/MainWindow.cpp | 17 +- guilib/src/PreferencesDialog.cpp | 52 ++-- guilib/src/ui/calibrationDialog.ui | 8 +- tools/Camera/main.cpp | 3 +- tools/CameraRGBD/main.cpp | 6 +- tools/EpipolarGeometry/main.cpp | 5 +- tools/StereoEval/main.cpp | 7 +- 79 files changed, 741 insertions(+), 366 deletions(-) create mode 100644 guilib/src/GuiUtil.h diff --git a/.github/actions/install-macos-source-deps/action.yml b/.github/actions/install-macos-source-deps/action.yml index 855d646e..83f16215 100644 --- a/.github/actions/install-macos-source-deps/action.yml +++ b/.github/actions/install-macos-source-deps/action.yml @@ -124,7 +124,7 @@ runs: uses: actions/cache@v4 with: path: ${{ runner.temp }}/deps-stage/depthai - key: depthai-2.32.0-usrlocal-${{ inputs.os }}-${{ hashFiles('.github/actions/install-macos-source-deps/patches/depthai-2.32.0-hunter-macos.patch') }}-${{ steps.depver.outputs.hash }} + key: depthai-2.32.0-usrlocal-nocv-${{ inputs.os }}-${{ hashFiles('.github/actions/install-macos-source-deps/patches/depthai-2.32.0-hunter-macos.patch') }}-${{ steps.depver.outputs.hash }} - name: Build depthai if: steps.cache-depthai.outputs.cache-hit != 'true' @@ -161,6 +161,7 @@ runs: -DDEPTHAI_ENABLE_CURL=OFF \ -DDEPTHAI_BUILD_TESTS=OFF \ -DDEPTHAI_BUILD_EXAMPLES=OFF \ + -DDEPTHAI_OPENCV_SUPPORT=OFF \ -DCMAKE_PREFIX_PATH="$(brew --prefix zlib)" "$CMAKE3" --build build -j$NPROC DESTDIR="$STAGE" "$CMAKE3" --install build diff --git a/.github/workflows/cmake-macos.yml b/.github/workflows/cmake-macos.yml index b8e8d92a..240395f3 100644 --- a/.github/workflows/cmake-macos.yml +++ b/.github/workflows/cmake-macos.yml @@ -22,23 +22,33 @@ jobs: strategy: fail-fast: true matrix: - build_name: [macos-sequoia-intel, macos-sequoia-apple-silicon, macos-tahoe-intel, macos-tahoe-apple-silicon] + build_name: [macos-sequoia-intel, macos-sequoia-apple-silicon, macos-tahoe-intel, macos-tahoe-apple-silicon, macos-tahoe-intel-cv5, macos-tahoe-apple-silicon-cv5] include: - build_name: macos-sequoia-intel os: macos-15-intel + cv: opencv@4 - build_name: macos-sequoia-apple-silicon os: macos-15 + cv: opencv@4 - build_name: macos-tahoe-intel os: macos-26-intel + cv: opencv@4 - build_name: macos-tahoe-apple-silicon os: macos-26 + cv: opencv@4 + - build_name: macos-tahoe-intel-cv5 + os: macos-26-intel + cv: opencv + - build_name: macos-tahoe-apple-silicon-cv5 + os: macos-26 + cv: opencv steps: - uses: actions/checkout@v4 - name: Install Brew Dependencies run: | # Update brew and install from Brewfile if present, or specific packages - brew install pcl opencv@4 octomap pdal yaml-cpp librealsense libfreenect libusb zlib libomp suite-sparse ceres-solver + brew install pcl octomap pdal yaml-cpp librealsense libfreenect libusb zlib libomp suite-sparse ceres-solver ${{ matrix.cv }} - name: Install Source Dependencies # Build (and per-dependency cache) the source-only deps not available from diff --git a/CMakeLists.txt b/CMakeLists.txt index 25adaccc..0e971403 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 8) +SET(RTABMAP_PATCH_VERSION 9) SET(RTABMAP_VERSION ${RTABMAP_MAJOR_VERSION}.${RTABMAP_MINOR_VERSION}.${RTABMAP_PATCH_VERSION}) @@ -241,7 +241,25 @@ option(BUILD_WITH_RPATH_NOT_RUNPATH "Explicitly disable usage of RUNPATH for the set(RTABMAP_QT_VERSION AUTO CACHE STRING "Force a specific Qt version.") set_property(CACHE RTABMAP_QT_VERSION PROPERTY STRINGS AUTO 4 5 6) -FIND_PACKAGE(OpenCV REQUIRED QUIET COMPONENTS core calib3d imgproc highgui stitching photo video videoio OPTIONAL_COMPONENTS aruco objdetect xfeatures2d nonfree gpu cudafeatures2d cudaoptflow cudaimgproc) +# OpenCV components. calib3d was split into "calib" + "geometry" in OpenCV 5. +# These lists are reused below to generate RTABMapConfig.cmake so downstream +# find_package(RTABMap) requests the same components this build used. +SET(RTABMAP_OpenCV_COMPONENTS_5 core imgproc highgui stitching photo video videoio calib geometry) +SET(RTABMAP_OpenCV_COMPONENTS_4 core imgproc highgui stitching photo video videoio calib3d) +SET(RTABMAP_OpenCV_OPTIONAL_COMPONENTS_5 objdetect xfeatures2d nonfree gpu cudafeatures2d cudaoptflow cudaimgproc) +SET(RTABMAP_OpenCV_OPTIONAL_COMPONENTS_4 aruco objdetect xfeatures2d nonfree gpu cudafeatures2d cudaoptflow cudaimgproc) + +# Probe OpenCV without a version constraint first, then request the components +# matching the detected major version. A version-constrained find that fails to +# match (e.g. asking for 5 when only 4 is present) resets OpenCV_DIR to NOTFOUND, +# which breaks toolchain builds that rely on a -DOpenCV_DIR hint (e.g. Android, +# where CMAKE_FIND_ROOT_PATH restricts the search). +FIND_PACKAGE(OpenCV REQUIRED QUIET COMPONENTS core) +IF(OpenCV_VERSION_MAJOR GREATER 4) + FIND_PACKAGE(OpenCV REQUIRED QUIET COMPONENTS ${RTABMAP_OpenCV_COMPONENTS_5} OPTIONAL_COMPONENTS ${RTABMAP_OpenCV_OPTIONAL_COMPONENTS_5}) +ELSE() + FIND_PACKAGE(OpenCV REQUIRED QUIET COMPONENTS ${RTABMAP_OpenCV_COMPONENTS_4} OPTIONAL_COMPONENTS ${RTABMAP_OpenCV_OPTIONAL_COMPONENTS_4}) +ENDIF() IF(WITH_QT) FIND_PACKAGE(PCL 1.7 REQUIRED QUIET COMPONENTS common io kdtree search surface filters registration sample_consensus segmentation visualization) @@ -1320,6 +1338,18 @@ install(EXPORT rtabmapTargets #### # Setup RTABMapConfig.cmake #### +IF(OpenCV_VERSION_MAJOR GREATER 4) + SET(CONF_OPENCV_COMPONENTS ${RTABMAP_OpenCV_COMPONENTS_5}) + SET(CONF_OPENCV_OPTIONAL_COMPONENTS ${RTABMAP_OpenCV_OPTIONAL_COMPONENTS_5}) +ELSE() + SET(CONF_OPENCV_COMPONENTS ${RTABMAP_OpenCV_COMPONENTS_4}) + SET(CONF_OPENCV_OPTIONAL_COMPONENTS ${RTABMAP_OpenCV_OPTIONAL_COMPONENTS_4}) +ENDIF() +STRING(REPLACE ";" " " CONF_OPENCV_COMPONENTS "${CONF_OPENCV_COMPONENTS}") +STRING(REPLACE ";" " " CONF_OPENCV_OPTIONAL_COMPONENTS "${CONF_OPENCV_OPTIONAL_COMPONENTS}") +# Pin the OpenCV major version so downstream projects find the same major RTAB-Map was +# built against +SET(CONF_OPENCV_VERSION_MAJOR ${OpenCV_VERSION_MAJOR}) include(CMakePackageConfigHelpers) write_basic_package_version_file( "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake" @@ -1486,7 +1516,8 @@ ENDIF(PCL_COMPILE_OPTIONS) MESSAGE(STATUS "") MESSAGE(STATUS "Optional dependencies ('*' affects some default parameters) :") IF(OpenCV_FOUND) - IF(OPENCV_ARUCO_FOUND) + IF((OpenCV_VERSION_MAJOR LESS 4 AND OPENCV_ARUCO_FOUND) OR + (OpenCV_VERSION_MAJOR GREATER 4 AND OPENCV_OBJDETECT_FOUND)) set(ARUCO_STR "YES") ELSE() set(ARUCO_STR "NO") diff --git a/RTABMapConfig.cmake.in b/RTABMapConfig.cmake.in index 155ef3ef..713359b4 100644 --- a/RTABMapConfig.cmake.in +++ b/RTABMapConfig.cmake.in @@ -1,7 +1,7 @@ include(CMakeFindDependencyMacro) # Mandatory dependencies -find_dependency(OpenCV COMPONENTS core calib3d imgproc highgui stitching photo video OPTIONAL_COMPONENTS aruco objdetect xfeatures2d nonfree gpu cudafeatures2d) +find_dependency(OpenCV @CONF_OPENCV_VERSION_MAJOR@ COMPONENTS @CONF_OPENCV_COMPONENTS@ OPTIONAL_COMPONENTS @CONF_OPENCV_OPTIONAL_COMPONENTS@) if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/RTABMap_guiTargets.cmake") find_dependency(PCL 1.7 COMPONENTS common io kdtree search surface filters registration sample_consensus segmentation visualization) diff --git a/corelib/include/rtabmap/core/DBDriverSqlite3.h b/corelib/include/rtabmap/core/DBDriverSqlite3.h index e73bab86..8687b54e 100644 --- a/corelib/include/rtabmap/core/DBDriverSqlite3.h +++ b/corelib/include/rtabmap/core/DBDriverSqlite3.h @@ -30,7 +30,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/core/rtabmap_core_export.h" // DLL export/import defines #include "rtabmap/core/DBDriver.h" +#if CV_MAJOR_VERSION < 5 #include +#else +#include +#endif typedef struct sqlite3_stmt sqlite3_stmt; typedef struct sqlite3 sqlite3; diff --git a/corelib/include/rtabmap/core/EpipolarGeometry.h b/corelib/include/rtabmap/core/EpipolarGeometry.h index e9408cd8..c739d0d4 100644 --- a/corelib/include/rtabmap/core/EpipolarGeometry.h +++ b/corelib/include/rtabmap/core/EpipolarGeometry.h @@ -31,7 +31,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/core/Parameters.h" #include "rtabmap/utilite/UStl.h" #include +#if CV_MAJOR_VERSION < 5 #include +#else +#include +#endif #include #include #include diff --git a/corelib/include/rtabmap/core/Features2d.h b/corelib/include/rtabmap/core/Features2d.h index eec3f57b..82a6ddb2 100644 --- a/corelib/include/rtabmap/core/Features2d.h +++ b/corelib/include/rtabmap/core/Features2d.h @@ -32,7 +32,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include +#if CV_MAJOR_VERSION < 5 #include +#else +#include +#endif #include #include #include "rtabmap/core/Parameters.h" @@ -70,6 +74,10 @@ class BriefDescriptorExtractor; class SIFT; #endif class SURF; +#if (CV_MAJOR_VERSION == 5) +class BRISK; +class KAZE; +#endif } namespace cuda { class FastFeatureDetector; @@ -89,7 +97,13 @@ typedef cv::xfeatures2d::FREAK CV_FREAK; typedef cv::xfeatures2d::DAISY CV_DAISY; typedef cv::GFTTDetector CV_GFTT; typedef cv::xfeatures2d::BriefDescriptorExtractor CV_BRIEF; +#if (CV_MAJOR_VERSION < 5) typedef cv::BRISK CV_BRISK; +typedef cv::KAZE CV_KAZE; +#else +typedef cv::xfeatures2d::BRISK CV_BRISK; +typedef cv::xfeatures2d::KAZE CV_KAZE; +#endif typedef cv::ORB CV_ORB; typedef cv::cuda::SURF_CUDA CV_SURF_GPU; typedef cv::cuda::ORB CV_ORB_GPU; @@ -578,7 +592,7 @@ private: int diffusivity_; #if CV_MAJOR_VERSION > 2 - cv::Ptr kaze_; + cv::Ptr kaze_; #endif }; diff --git a/corelib/include/rtabmap/core/MarkerDetector.h b/corelib/include/rtabmap/core/MarkerDetector.h index 630bf87f..26b6a73c 100644 --- a/corelib/include/rtabmap/core/MarkerDetector.h +++ b/corelib/include/rtabmap/core/MarkerDetector.h @@ -32,7 +32,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include -#ifdef HAVE_OPENCV_ARUCO +#if (CV_MAJOR_VERSION > 4 || (CV_MAJOR_VERSION==4 && CV_MINOR_VERSION >=7)) && defined(HAVE_OPENCV_OBJDETECT) +#include +#elif defined(HAVE_OPENCV_ARUCO) #include #endif @@ -97,8 +99,11 @@ private: float maxRange_; float minRange_; int dictionaryId_; -#ifdef HAVE_OPENCV_ARUCO - cv::Ptr detectorParams_; +#if ((CV_MAJOR_VERSION > 4 || (CV_MAJOR_VERSION==4 && CV_MINOR_VERSION >=7)) && defined(HAVE_OPENCV_OBJDETECT)) || defined(HAVE_OPENCV_ARUCO) +#if CV_MAJOR_VERSION > 4 || (CV_MAJOR_VERSION==4 && CV_MINOR_VERSION >=7) + cv::Ptr arucoDetector_; +#endif + cv::Ptr detectorParams_; cv::Ptr dictionary_; #endif void * apriltagLibDetector_; diff --git a/corelib/include/rtabmap/core/Memory.h b/corelib/include/rtabmap/core/Memory.h index 040b8516..c4f55df7 100644 --- a/corelib/include/rtabmap/core/Memory.h +++ b/corelib/include/rtabmap/core/Memory.h @@ -41,7 +41,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include "rtabmap/utilite/UStl.h" #include +#if CV_MAJOR_VERSION < 5 #include +#else +#include +#endif #include namespace rtabmap { diff --git a/corelib/include/rtabmap/core/OdometryInfo.h b/corelib/include/rtabmap/core/OdometryInfo.h index 484692b8..4f96a598 100644 --- a/corelib/include/rtabmap/core/OdometryInfo.h +++ b/corelib/include/rtabmap/core/OdometryInfo.h @@ -34,7 +34,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/core/RegistrationInfo.h" #include "rtabmap/core/CameraModel.h" #include "rtabmap/core/LaserScan.h" +#if CV_MAJOR_VERSION < 5 #include +#else +#include +#endif namespace rtabmap { diff --git a/corelib/include/rtabmap/core/SensorData.h b/corelib/include/rtabmap/core/SensorData.h index cad4f6cc..b69e194d 100644 --- a/corelib/include/rtabmap/core/SensorData.h +++ b/corelib/include/rtabmap/core/SensorData.h @@ -34,7 +34,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#if CV_MAJOR_VERSION < 5 #include +#else +#include +#endif #include #include #include diff --git a/corelib/include/rtabmap/core/Signature.h b/corelib/include/rtabmap/core/Signature.h index fbb3e217..865fac1d 100644 --- a/corelib/include/rtabmap/core/Signature.h +++ b/corelib/include/rtabmap/core/Signature.h @@ -31,7 +31,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include +#if CV_MAJOR_VERSION < 5 #include +#else +#include +#endif #include #include #include diff --git a/corelib/include/rtabmap/core/Statistics.h b/corelib/include/rtabmap/core/Statistics.h index 07e610b4..d1e1eefa 100644 --- a/corelib/include/rtabmap/core/Statistics.h +++ b/corelib/include/rtabmap/core/Statistics.h @@ -31,7 +31,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/core/rtabmap_core_export.h" // DLL export/import defines #include +#if CV_MAJOR_VERSION < 5 #include +#else +#include +#endif #include #include #include diff --git a/corelib/include/rtabmap/core/VWDictionary.h b/corelib/include/rtabmap/core/VWDictionary.h index ebfa3a62..3a7e195e 100644 --- a/corelib/include/rtabmap/core/VWDictionary.h +++ b/corelib/include/rtabmap/core/VWDictionary.h @@ -31,7 +31,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include +#if CV_MAJOR_VERSION < 5 #include +#else +#include +#endif #include #include #include "rtabmap/core/Parameters.h" diff --git a/corelib/include/rtabmap/core/stereo/stereoRectifyFisheye.h b/corelib/include/rtabmap/core/stereo/stereoRectifyFisheye.h index 2fc12a25..60b1d792 100644 --- a/corelib/include/rtabmap/core/stereo/stereoRectifyFisheye.h +++ b/corelib/include/rtabmap/core/stereo/stereoRectifyFisheye.h @@ -32,6 +32,15 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #ifndef CORELIB_SRC_OPENCV_STEREORECTIFYFISHEYE_H_ #define CORELIB_SRC_OPENCV_STEREORECTIFYFISHEYE_H_ +// This header relies on the OpenCV C API (cvRodrigues2, cvProjectPoints2, ...) +// which was removed in OpenCV 5. Pull in only the version macros (available in +// all OpenCV versions) so we can fail early with a clear message rather than +// with cryptic errors from the includes below. +#include +#if CV_MAJOR_VERSION >= 5 +#error "stereoRectifyFisheye.h is not supported with OpenCV 5 or later (it uses the removed OpenCV C API). Use cv::fisheye::stereoRectify() instead, or guard your include with '#if CV_MAJOR_VERSION < 5'." +#endif + #include #if CV_MAJOR_VERSION >= 3 #include diff --git a/corelib/include/rtabmap/core/util3d_correspondences.h b/corelib/include/rtabmap/core/util3d_correspondences.h index 6bbaeb2d..d53ece4c 100644 --- a/corelib/include/rtabmap/core/util3d_correspondences.h +++ b/corelib/include/rtabmap/core/util3d_correspondences.h @@ -32,7 +32,12 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include +#include +#if CV_MAJOR_VERSION < 5 #include +#else +#include +#endif #include #include #include diff --git a/corelib/include/rtabmap/core/util3d_features.h b/corelib/include/rtabmap/core/util3d_features.h index 2ea95bf7..6547e5c3 100644 --- a/corelib/include/rtabmap/core/util3d_features.h +++ b/corelib/include/rtabmap/core/util3d_features.h @@ -30,7 +30,12 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include +#include +#if CV_MAJOR_VERSION < 5 #include +#else +#include +#endif #include #include #include diff --git a/corelib/src/EpipolarGeometry.cpp b/corelib/src/EpipolarGeometry.cpp index 38f15844..758a14af 100644 --- a/corelib/src/EpipolarGeometry.cpp +++ b/corelib/src/EpipolarGeometry.cpp @@ -33,8 +33,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/utilite/UMath.h" #include -#include +#if CV_MAJOR_VERSION < 5 #include +#else +#include +#endif #include namespace rtabmap diff --git a/corelib/src/Features2d.cpp b/corelib/src/Features2d.cpp index da83db71..184a4d8a 100644 --- a/corelib/src/Features2d.cpp +++ b/corelib/src/Features2d.cpp @@ -36,7 +36,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/utilite/UMath.h" #include "rtabmap/utilite/ULogger.h" #include "rtabmap/utilite/UTimer.h" -#include #include #include @@ -865,7 +864,7 @@ std::vector Feature2D::generateKeypoints(const cv::Mat & image, co cv::cornerSubPix( image, corners, cv::Size( _subPixWinSize, _subPixWinSize ), cv::Size( -1, -1 ), - cv::TermCriteria( CV_TERMCRIT_ITER | CV_TERMCRIT_EPS, _subPixIterations, _subPixEps ) ); + cv::TermCriteria( cv::TermCriteria::MAX_ITER | cv::TermCriteria::EPS, _subPixIterations, _subPixEps ) ); for(unsigned int i=0;i 4 +#ifdef HAVE_OPENCV_XFEATURES2D + brisk_ = CV_BRISK::create(thresh_, octaves_, patternScale_); +#else + UWARN("RTAB-Map is not built with OpenCV xfeatures2d module so BRISK cannot be used!"); +#endif +#elif CV_MAJOR_VERSION < 3 brisk_ = cv::Ptr(new CV_BRISK(thresh_, octaves_, patternScale_)); #else brisk_ = CV_BRISK::create(thresh_, octaves_, patternScale_); @@ -2389,6 +2393,7 @@ std::vector BRISK::generateKeypointsImpl(const cv::Mat & image, co { UASSERT(!image.empty() && image.channels() == 1 && image.depth() == CV_8U); std::vector keypoints; +#if CV_MAJOR_VERSION < 5 || (CV_MAJOR_VERSION > 4 && defined(HAVE_OPENCV_XFEATURES2D)) cv::Mat imgRoi(image, roi); cv::Mat maskRoi; if(!mask.empty()) @@ -2396,6 +2401,9 @@ std::vector BRISK::generateKeypointsImpl(const cv::Mat & image, co maskRoi = cv::Mat(mask, roi); } brisk_->detect(imgRoi, keypoints, maskRoi); // Opencv keypoints +#else + UWARN("RTAB-Map is not built with BRISK feature support!"); +#endif return keypoints; } @@ -2403,7 +2411,11 @@ cv::Mat BRISK::generateDescriptorsImpl(const cv::Mat & image, std::vector 4 && defined(HAVE_OPENCV_XFEATURES2D)) brisk_->compute(image, keypoints, descriptors); +#else + UWARN("RTAB-Map is not built with BRISK feature support!"); +#endif return descriptors; } @@ -2436,10 +2448,16 @@ void KAZE::parseParameters(const ParametersMap & parameters) Parameters::parse(parameters, Parameters::kKAZENOctaveLayers(), nOctaveLayers_); Parameters::parse(parameters, Parameters::kKAZEDiffusivity(), diffusivity_); -#if CV_MAJOR_VERSION > 3 - kaze_ = cv::KAZE::create(extended_, upright_, threshold_, nOctaves_, nOctaveLayers_, (cv::KAZE::DiffusivityType)diffusivity_); +#if CV_MAJOR_VERSION > 4 +#ifdef HAVE_OPENCV_XFEATURES2D + kaze_ = CV_KAZE::create(extended_, upright_, threshold_, nOctaves_, nOctaveLayers_, (CV_KAZE::DiffusivityType)diffusivity_); +#else + UWARN("RTAB-Map is not built with OpenCV xfeatures2d module so KAZE cannot be used!"); +#endif +#elif CV_MAJOR_VERSION > 3 + kaze_ = CV_KAZE::create(extended_, upright_, threshold_, nOctaves_, nOctaveLayers_, (CV_KAZE::DiffusivityType)diffusivity_); #elif CV_MAJOR_VERSION > 2 - kaze_ = cv::KAZE::create(extended_, upright_, threshold_, nOctaves_, nOctaveLayers_, diffusivity_); + kaze_ = CV_KAZE::create(extended_, upright_, threshold_, nOctaves_, nOctaveLayers_, diffusivity_); #else UWARN("RTAB-Map is not built with OpenCV3 so Kaze feature cannot be used!"); #endif @@ -2449,7 +2467,7 @@ std::vector KAZE::generateKeypointsImpl(const cv::Mat & image, con { UASSERT(!image.empty() && image.channels() == 1 && image.depth() == CV_8U); std::vector keypoints; -#if CV_MAJOR_VERSION > 2 +#if (CV_MAJOR_VERSION > 2 && CV_MAJOR_VERSION < 5) || (CV_MAJOR_VERSION > 4 && defined(HAVE_OPENCV_XFEATURES2D)) cv::Mat imgRoi(image, roi); cv::Mat maskRoi; if (!mask.empty()) @@ -2458,7 +2476,7 @@ std::vector KAZE::generateKeypointsImpl(const cv::Mat & image, con } kaze_->detect(imgRoi, keypoints, maskRoi); // Opencv keypoints #else - UWARN("RTAB-Map is not built with OpenCV3 so Kaze feature cannot be used!"); + UWARN("RTAB-Map is not built with Kaze feature support!"); #endif return keypoints; } @@ -2467,10 +2485,10 @@ cv::Mat KAZE::generateDescriptorsImpl(const cv::Mat & image, std::vector 2 +#if (CV_MAJOR_VERSION > 2 && CV_MAJOR_VERSION < 5) || (CV_MAJOR_VERSION > 4 && defined(HAVE_OPENCV_XFEATURES2D)) kaze_->compute(image, keypoints, descriptors); #else - UWARN("RTAB-Map is not built with OpenCV3 so Kaze feature cannot be used!"); + UWARN("RTAB-Map is not built with Kaze feature support!"); #endif return descriptors; } diff --git a/corelib/src/MarkerDetector.cpp b/corelib/src/MarkerDetector.cpp index 14840827..0b7acde9 100644 --- a/corelib/src/MarkerDetector.cpp +++ b/corelib/src/MarkerDetector.cpp @@ -30,6 +30,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include +#if CV_MAJOR_VERSION > 4 +#include +#endif + #ifdef HAVE_OPENCV_ARUCO #if CV_MAJOR_VERSION < 4 || (CV_MAJOR_VERSION == 4 && CV_MINOR_VERSION <8) namespace cv{ @@ -72,7 +76,7 @@ extern "C" { #include "apriltag/tag36h11.h" } -#ifndef HAVE_OPENCV_ARUCO +#if !((CV_MAJOR_VERSION > 4 || (CV_MAJOR_VERSION==4 && CV_MINOR_VERSION >=7)) && defined(HAVE_OPENCV_OBJDETECT)) && !defined(HAVE_OPENCV_ARUCO) // To match opencv::aruco module, add opencv::aruco dictionary enum namespace cv{ namespace aruco { @@ -200,7 +204,7 @@ MarkerDetector::MarkerDetector(const ParametersMap & parameters) : apriltagLibDetector_(NULL), apriltagLibFamily_(NULL) { -#ifdef HAVE_OPENCV_ARUCO +#if ((CV_MAJOR_VERSION > 4 || (CV_MAJOR_VERSION==4 && CV_MINOR_VERSION >=7)) && defined(HAVE_OPENCV_OBJDETECT)) || defined(HAVE_OPENCV_ARUCO) #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) @@ -310,7 +314,7 @@ void MarkerDetector::parseParameters(const ParametersMap & parameters) } } -#ifdef HAVE_OPENCV_ARUCO +#if ((CV_MAJOR_VERSION > 4 || (CV_MAJOR_VERSION==4 && CV_MINOR_VERSION >=7)) && defined(HAVE_OPENCV_OBJDETECT)) || defined(HAVE_OPENCV_ARUCO) detectorParams_->adaptiveThreshWinSizeMin = 3; detectorParams_->adaptiveThreshWinSizeMax = 23; detectorParams_->adaptiveThreshWinSizeStep = 10; @@ -364,6 +368,7 @@ void MarkerDetector::parseParameters(const ParametersMap & parameters) 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_)); @@ -373,6 +378,11 @@ void MarkerDetector::parseParameters(const ParametersMap & parameters) dictionary_.reset(new cv::aruco::Dictionary()); *dictionary_ = cv::aruco::getPredefinedDictionary(cv::aruco::PredefinedDictionaryType(dictionaryId_)); #endif + +#if CV_MAJOR_VERSION > 4 || (CV_MAJOR_VERSION==4 && CV_MINOR_VERSION >=7) + arucoDetector_.reset(new cv::aruco::ArucoDetector(*dictionary_, *detectorParams_)); +#endif + #else if(strategy_ == 0) { @@ -441,7 +451,7 @@ void MarkerDetector::parseParameters(const ParametersMap & parameters) #else if(strategy_ == 1) { -#ifdef HAVE_OPENCV_ARUCO +#if ((CV_MAJOR_VERSION > 4 || (CV_MAJOR_VERSION==4 && CV_MINOR_VERSION >=7)) && defined(HAVE_OPENCV_OBJDETECT)) || defined(HAVE_OPENCV_ARUCO) UERROR("RTAB-Map is not built with apriltag library! Fallback to OpenCV (%s=0).", Parameters::kMarkerStrategy().c_str()); strategy_ = kStrategyOpencv; #else @@ -471,6 +481,34 @@ std::map MarkerDetector::detect(const cv::Mat & image, const Cam return detections; } +#if (((CV_MAJOR_VERSION > 4 || (CV_MAJOR_VERSION==4 && CV_MINOR_VERSION >=7)) && defined(HAVE_OPENCV_OBJDETECT)) || defined(HAVE_OPENCV_ARUCO)) && (CV_MAJOR_VERSION > 4 || (CV_MAJOR_VERSION == 4 && CV_MINOR_VERSION >= 7)) +// Drop-in replacement for cv::aruco::estimatePoseSingleMarkers(), which was deprecated +// in OpenCV 4.7 and removed in OpenCV 5. It reproduces the legacy default behavior (marker +// object points ordered as ARUCO_CCW_CENTER + SOLVEPNP_ITERATIVE) using cv::solvePnP() per +// marker. For OpenCV < 4.7 the native cv::aruco::estimatePoseSingleMarkers() is still used. +static void estimatePoseSingleMarkers( + const std::vector > & corners, + float markerLength, + const cv::Mat & cameraMatrix, + const cv::Mat & distCoeffs, + std::vector & rvecs, + std::vector & tvecs) +{ + cv::Mat objPoints(4, 1, CV_32FC3); + objPoints.ptr(0)[0] = cv::Vec3f(-markerLength/2.f, markerLength/2.f, 0); + objPoints.ptr(0)[1] = cv::Vec3f( markerLength/2.f, markerLength/2.f, 0); + objPoints.ptr(0)[2] = cv::Vec3f( markerLength/2.f, -markerLength/2.f, 0); + objPoints.ptr(0)[3] = cv::Vec3f(-markerLength/2.f, -markerLength/2.f, 0); + + rvecs.resize(corners.size()); + tvecs.resize(corners.size()); + for(size_t i=0; i MarkerDetector::detect(const cv::Mat & image, const std::vector & models, const cv::Mat & depth, @@ -482,20 +520,27 @@ std::map MarkerDetector::detect(const cv::Mat & image, UASSERT(int((depth.cols/models.size())*models.size()) == depth.cols); int subRGBWidth = image.cols/models.size(); + // Only a real depth map (CV_16UC1/CV_32FC1) can be used for marker length estimation. + // Ignore anything else (e.g. a stereo right image passed by mistake) instead of crashing + // later in util2d::getDepth(). + cv::Mat depthMap = depth; + if(!depthMap.empty() && depthMap.type()!=CV_16UC1 && depthMap.type()!=CV_32FC1) + { + UWARN("Marker detection: ignoring depth image with unsupported type=%d (expected CV_16UC1 or CV_32FC1).", depthMap.type()); + depthMap = cv::Mat(); + } + float rgbToDepthFactorX = 1.0f; float rgbToDepthFactorY = 1.0f; - if(!depth.empty()) + if(!depthMap.empty()) { - rgbToDepthFactorX = float(depth.cols) / float(image.cols); - rgbToDepthFactorY = float(depth.rows) / float(image.rows); + rgbToDepthFactorX = float(depthMap.cols) / float(image.cols); + rgbToDepthFactorY = float(depthMap.rows) / float(image.rows); } else if(markerLength_ == 0) { - if(depth.empty()) - { - UERROR("Depth image is empty, please set %s parameter to non-null.", Parameters::kMarkerLength().c_str()); - return std::map(); - } + UERROR("Depth image is empty, please set %s parameter to non-null.", Parameters::kMarkerLength().c_str()); + return std::map(); } std::vector< int > ids; @@ -632,8 +677,10 @@ std::map MarkerDetector::detect(const cv::Mat & image, { 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) +#if ((CV_MAJOR_VERSION > 4 || (CV_MAJOR_VERSION==4 && CV_MINOR_VERSION >=7)) && defined(HAVE_OPENCV_OBJDETECT)) || defined(HAVE_OPENCV_ARUCO) +#if CV_MAJOR_VERSION > 4 || (CV_MAJOR_VERSION==4 && CV_MINOR_VERSION >=7) + arucoDetector_->detectMarkers(image, cvCorners, cvIds, cvRejected); +#elif 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); @@ -686,9 +733,15 @@ std::map MarkerDetector::detect(const cv::Mat & image, for(size_t cam=0; cam < cvCornersPerCam.size(); ++cam) { - std::vector< cv::Vec3d > rvecs, tvecs; const CameraModel & model = models[cam]; + + std::vector< cv::Vec3d > rvecs, tvecs; +#if CV_MAJOR_VERSION > 4 || (CV_MAJOR_VERSION == 4 && CV_MINOR_VERSION >= 7) + estimatePoseSingleMarkers(cvCornersPerCam[cam], 1.0f, model.K(), model.D(), rvecs, tvecs); +#else cv::aruco::estimatePoseSingleMarkers(cvCornersPerCam[cam], 1.0f, model.K(), model.D(), rvecs, tvecs); +#endif + float offsetX = cam*subRGBWidth; for(size_t i=0; i MarkerDetector::detect(const cv::Mat & image, { float length = 0.0f; std::map::const_iterator findIter = extraMarkerLengths.find(ids[i]); - if(markerLengths_.empty() && !depth.empty() && (markerLength_ == 0 || (markerLength_<0 && findIter==extraMarkerLengths.end()))) + if(markerLengths_.empty() && !depthMap.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); + float d = util2d::getDepth(depthMap, (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(depthMap, corners[i][0].x*rgbToDepthFactorX, corners[i][0].y*rgbToDepthFactorY, true, 0.02f, true); + float d2 = util2d::getDepth(depthMap, corners[i][1].x*rgbToDepthFactorX, corners[i][1].y*rgbToDepthFactorY, true, 0.02f, true); + float d3 = util2d::getDepth(depthMap, corners[i][2].x*rgbToDepthFactorX, corners[i][2].y*rgbToDepthFactorY, true, 0.02f, true); + float d4 = util2d::getDepth(depthMap, 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) @@ -887,7 +940,7 @@ std::map MarkerDetector::detect(const cv::Mat & image, if(!ids.empty()) { -#ifdef HAVE_OPENCV_ARUCO +#if ((CV_MAJOR_VERSION > 4 || (CV_MAJOR_VERSION==4 && CV_MINOR_VERSION >=7)) && defined(HAVE_OPENCV_OBJDETECT)) || defined(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."); diff --git a/corelib/src/Memory.cpp b/corelib/src/Memory.cpp index 5b7dc5db..11046fb3 100644 --- a/corelib/src/Memory.cpp +++ b/corelib/src/Memory.cpp @@ -62,8 +62,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include -#include #include +#if CV_MAJOR_VERSION >=5 +#include +#endif namespace rtabmap { @@ -5530,7 +5532,7 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor cv::Mat imageMono; if(decimatedData.imageRaw().channels() == 3) { - cv::cvtColor(decimatedData.imageRaw(), imageMono, CV_BGR2GRAY); + cv::cvtColor(decimatedData.imageRaw(), imageMono, cv::COLOR_BGR2GRAY); } else { @@ -5838,7 +5840,7 @@ Signature * Memory::createSignature(const SensorData & inputData, const Transfor cv::Mat imageMono; if(data.imageRaw().channels() == 3) { - cv::cvtColor(data.imageRaw(), imageMono, CV_BGR2GRAY); + cv::cvtColor(data.imageRaw(), imageMono, cv::COLOR_BGR2GRAY); } else { diff --git a/corelib/src/RegistrationVis.cpp b/corelib/src/RegistrationVis.cpp index 738e72b3..3e5cee39 100644 --- a/corelib/src/RegistrationVis.cpp +++ b/corelib/src/RegistrationVis.cpp @@ -43,7 +43,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include -#include +#if CV_MAJOR_VERSION > 4 +#include +#endif #if defined(HAVE_OPENCV_XFEATURES2D) && (CV_MAJOR_VERSION > 3 || (CV_MAJOR_VERSION==3 && CV_MINOR_VERSION >=4 && CV_SUBMINOR_VERSION >= 1)) #include // For GMS matcher @@ -2172,7 +2174,7 @@ Transform RegistrationVis::computeTransformationImpl( if(!transform.isNull() && !pcaData.empty()) { cv::Mat pcaEigenVectors, pcaEigenValues; - cv::PCA pca_analysis(pcaData, cv::Mat(), CV_PCA_DATA_AS_ROW); + cv::PCA pca_analysis(pcaData, cv::Mat(), cv::PCA::DATA_AS_ROW); // We take the second eigen value info.inliersDistribution = pca_analysis.eigenvalues.at(0, 1); diff --git a/corelib/src/SensorCaptureThread.cpp b/corelib/src/SensorCaptureThread.cpp index b9fc7bb7..9246755a 100644 --- a/corelib/src/SensorCaptureThread.cpp +++ b/corelib/src/SensorCaptureThread.cpp @@ -39,7 +39,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/core/IMUFilter.h" #include "rtabmap/core/Features2d.h" #include "rtabmap/core/clams/discrete_depth_distortion_model.h" -#include #include #include #include @@ -757,11 +756,11 @@ void SensorCaptureThread::postUpdate(SensorData * dataPtr, SensorCaptureInfo * i else if(data.imageRaw().type() == CV_8UC3) { cv::Mat channels[3]; - cv::cvtColor(data.imageRaw(), image, CV_BGR2YCrCb); + cv::cvtColor(data.imageRaw(), image, cv::COLOR_BGR2YCrCb); cv::split(image, channels); cv::equalizeHist(channels[0], channels[0]); cv::merge(channels, 3, image); - cv::cvtColor(image, image, CV_YCrCb2BGR); + cv::cvtColor(image, image, cv::COLOR_YCrCb2BGR); } if(!data.depthRaw().empty()) { @@ -777,11 +776,11 @@ void SensorCaptureThread::postUpdate(SensorData * dataPtr, SensorCaptureInfo * i else if(data.rightRaw().type() == CV_8UC3) { cv::Mat channels[3]; - cv::cvtColor(data.rightRaw(), right, CV_BGR2YCrCb); + cv::cvtColor(data.rightRaw(), right, cv::COLOR_BGR2YCrCb); cv::split(right, channels); cv::equalizeHist(channels[0], channels[0]); cv::merge(channels, 3, right); - cv::cvtColor(right, right, CV_YCrCb2BGR); + cv::cvtColor(right, right, cv::COLOR_YCrCb2BGR); } data.setStereoImage(image, right, data.stereoCameraModels()[0]); } @@ -796,11 +795,11 @@ void SensorCaptureThread::postUpdate(SensorData * dataPtr, SensorCaptureInfo * i else if(data.imageRaw().type() == CV_8UC3) { cv::Mat channels[3]; - cv::cvtColor(data.imageRaw(), image, CV_BGR2YCrCb); + cv::cvtColor(data.imageRaw(), image, cv::COLOR_BGR2YCrCb); cv::split(image, channels); clahe->apply(channels[0], channels[0]); cv::merge(channels, 3, image); - cv::cvtColor(image, image, CV_YCrCb2BGR); + cv::cvtColor(image, image, cv::COLOR_YCrCb2BGR); } if(!data.depthRaw().empty()) { @@ -816,11 +815,11 @@ void SensorCaptureThread::postUpdate(SensorData * dataPtr, SensorCaptureInfo * i else if(data.rightRaw().type() == CV_8UC3) { cv::Mat channels[3]; - cv::cvtColor(data.rightRaw(), right, CV_BGR2YCrCb); + cv::cvtColor(data.rightRaw(), right, cv::COLOR_BGR2YCrCb); cv::split(right, channels); clahe->apply(channels[0], channels[0]); cv::merge(channels, 3, right); - cv::cvtColor(right, right, CV_YCrCb2BGR); + cv::cvtColor(right, right, cv::COLOR_YCrCb2BGR); } data.setStereoImage(image, right, data.stereoCameraModels()[0]); } diff --git a/corelib/src/StereoCameraModel.cpp b/corelib/src/StereoCameraModel.cpp index 9c338174..c6ea1acd 100644 --- a/corelib/src/StereoCameraModel.cpp +++ b/corelib/src/StereoCameraModel.cpp @@ -33,7 +33,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include -#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))) +#if (CV_MAJOR_VERSION > 2 and CV_MAJOR_VERSION < 5) or (CV_MAJOR_VERSION == 2 and (CV_MINOR_VERSION >4 or (CV_MINOR_VERSION == 4 and CV_SUBMINOR_VERSION >=10))) #include #endif @@ -179,12 +179,20 @@ void StereoCameraModel::updateStereoRectification() { cv::Vec4d D_left(left_.D_raw().at(0,0), left_.D_raw().at(0,1), left_.D_raw().at(0,4), left_.D_raw().at(0,5)); cv::Vec4d D_right(right_.D_raw().at(0,0), right_.D_raw().at(0,1), right_.D_raw().at(0,4), right_.D_raw().at(0,5)); - +#if CV_MAJOR_VERSION < 5 stereoRectifyFisheye( left_.K_raw(), D_left, right_.K_raw(), D_right, left_.imageSize(), R_, T_, R1, R2, P1, P2, Q, cv::CALIB_ZERO_DISPARITY, 0, left_.imageSize()); +#else + double balance = 0.0, fov_scale = 1.0; + cv::fisheye::stereoRectify( + left_.K_raw(), D_left, + right_.K_raw(), D_right, + left_.imageSize(), R_, T_, R1, R2, P1, P2, Q, + cv::CALIB_ZERO_DISPARITY, left_.imageSize(), balance, fov_scale); +#endif // Re-zoom to original focal distance if(P1.at(0,0) < 0) diff --git a/corelib/src/camera/CameraDepthAI.cpp b/corelib/src/camera/CameraDepthAI.cpp index 4dbbafa6..0c8bdfbb 100644 --- a/corelib/src/camera/CameraDepthAI.cpp +++ b/corelib/src/camera/CameraDepthAI.cpp @@ -32,7 +32,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include - +#if CV_MAJOR_VERSION < 5 +#include +#else +#include +#endif namespace rtabmap { diff --git a/corelib/src/camera/CameraFreenect.cpp b/corelib/src/camera/CameraFreenect.cpp index 9d48215f..a402c7b7 100644 --- a/corelib/src/camera/CameraFreenect.cpp +++ b/corelib/src/camera/CameraFreenect.cpp @@ -28,7 +28,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include -#include #ifdef RTABMAP_FREENECT #include @@ -183,7 +182,7 @@ private: if(color_) { - cv::cvtColor(rgbIrBuffer_, rgbIrLastFrame_, CV_RGB2BGR); + cv::cvtColor(rgbIrBuffer_, rgbIrLastFrame_, cv::COLOR_RGB2BGR); } else // IrDepth { diff --git a/corelib/src/camera/CameraFreenect2.cpp b/corelib/src/camera/CameraFreenect2.cpp index 85f7bdf0..1514311e 100644 --- a/corelib/src/camera/CameraFreenect2.cpp +++ b/corelib/src/camera/CameraFreenect2.cpp @@ -29,7 +29,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include -#include #ifdef RTABMAP_FREENECT2 #include @@ -430,11 +429,11 @@ SensorData CameraFreenect2::captureImage(SensorCaptureInfo * info) cv::Mat rgbMat; // rtabmap uses 3 channels RGB #ifdef LIBFREENECT2_WITH_TEGRAJPEG_SUPPORT - cv::cvtColor(rgbMatC4, rgbMat, CV_RGBA2BGR); + cv::cvtColor(rgbMatC4, rgbMat, cv::COLOR_RGBA2BGR); #else - cv::cvtColor(rgbMatC4, rgbMat, CV_BGRA2BGR); + cv::cvtColor(rgbMatC4, rgbMat, cv::COLOR_BGRA2BGR); #endif cv::flip(rgbMat, rgb, 1); @@ -490,11 +489,11 @@ SensorData CameraFreenect2::captureImage(SensorCaptureInfo * info) cv::Mat rgbMat; // rtabmap uses 3 channels RGB #ifdef LIBFREENECT2_WITH_TEGRAJPEG_SUPPORT - cv::cvtColor(rgbMatC4, rgbMat, CV_RGB2BGR); + cv::cvtColor(rgbMatC4, rgbMat, cv::COLOR_RGB2BGR); #else - cv::cvtColor(rgbMatC4, rgbMat, CV_BGRA2BGR); + cv::cvtColor(rgbMatC4, rgbMat, cv::COLOR_BGRA2BGR); #endif cv::flip(rgbMat, rgb, 1); @@ -607,11 +606,11 @@ SensorData CameraFreenect2::captureImage(SensorCaptureInfo * info) // rtabmap uses 3 channels RGB #ifdef LIBFREENECT2_WITH_TEGRAJPEG_SUPPORT - cv::cvtColor(rgbMatBGRA, rgb, CV_RGBA2BGR); + cv::cvtColor(rgbMatBGRA, rgb, cv::COLOR_RGBA2BGR); #else - cv::cvtColor(rgbMatBGRA, rgb, CV_BGRA2BGR); + cv::cvtColor(rgbMatBGRA, rgb, cv::COLOR_BGRA2BGR); #endif cv::flip(rgb, rgb, 1); @@ -629,11 +628,11 @@ SensorData CameraFreenect2::captureImage(SensorCaptureInfo * info) // rtabmap uses 3 channels RGB #ifdef LIBFREENECT2_WITH_TEGRAJPEG_SUPPORT - cv::cvtColor(rgbMatBGRA, rgb, CV_RGBA2BGR); + cv::cvtColor(rgbMatBGRA, rgb, cv::COLOR_RGBA2BGR); #else - cv::cvtColor(rgbMatBGRA, rgb, CV_BGRA2BGR); + cv::cvtColor(rgbMatBGRA, rgb, cv::COLOR_BGRA2BGR); #endif cv::flip(rgb, rgb, 1); diff --git a/corelib/src/camera/CameraImages.cpp b/corelib/src/camera/CameraImages.cpp index be6383a2..535b04dc 100644 --- a/corelib/src/camera/CameraImages.cpp +++ b/corelib/src/camera/CameraImages.cpp @@ -34,7 +34,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include -#include +#include #include namespace rtabmap @@ -1111,7 +1111,7 @@ SensorData CameraImages::captureImage(SensorCaptureInfo * info) { UWARN("Conversion from 4 channels to 3 channels (file=%s)", imageFilePath.c_str()); cv::Mat out; - cv::cvtColor(img, out, CV_BGRA2BGR); + cv::cvtColor(img, out, cv::COLOR_BGRA2BGR); img = out; } else if(!img.empty() && _bayerMode >= 0 && _bayerMode <=3) @@ -1119,7 +1119,7 @@ SensorData CameraImages::captureImage(SensorCaptureInfo * info) cv::Mat debayeredImg; try { - cv::cvtColor(img, debayeredImg, CV_BayerBG2BGR + _bayerMode); + cv::cvtColor(img, debayeredImg, cv::COLOR_BayerBG2BGR + _bayerMode); img = debayeredImg; } catch(const cv::Exception & e) diff --git a/corelib/src/camera/CameraK4A.cpp b/corelib/src/camera/CameraK4A.cpp index ad5714d7..caf659ba 100644 --- a/corelib/src/camera/CameraK4A.cpp +++ b/corelib/src/camera/CameraK4A.cpp @@ -29,7 +29,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include -#include #ifdef RTABMAP_K4A #include @@ -508,7 +507,7 @@ SensorData CameraK4A::captureImage(SensorCaptureInfo * info) CV_8UC4, (void*)k4a_image_get_buffer(rgb_image_)); - cv::cvtColor(bgra, bgrCV, CV_BGRA2BGR); + cv::cvtColor(bgra, bgrCV, cv::COLOR_BGRA2BGR); } bgrCV = model_.rectifyImage(bgrCV); diff --git a/corelib/src/camera/CameraK4W2.cpp b/corelib/src/camera/CameraK4W2.cpp index e289029e..9a96251f 100644 --- a/corelib/src/camera/CameraK4W2.cpp +++ b/corelib/src/camera/CameraK4W2.cpp @@ -28,7 +28,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include -#include #ifdef RTABMAP_K4W2 #include @@ -486,11 +485,11 @@ SensorData CameraK4W2::captureImage(SensorCaptureInfo * info) { cv::Mat tmp; cv::resize(cv::Mat(nColorHeight, nColorWidth, CV_8UC4, pColorBuffer), tmp, cv::Size(), 0.5, 0.5, cv::INTER_AREA); - cv::cvtColor(tmp, imageColor, CV_BGRA2BGR); + cv::cvtColor(tmp, imageColor, cv::COLOR_BGRA2BGR); } else { - cv::cvtColor(cv::Mat(nColorHeight, nColorWidth, CV_8UC4, pColorBuffer), imageColor, CV_BGRA2BGR); + cv::cvtColor(cv::Mat(nColorHeight, nColorWidth, CV_8UC4, pColorBuffer), imageColor, cv::COLOR_BGRA2BGR); } // loop over output pixels for (int depthIndex = 0; depthIndex < (nDepthWidth*nDepthHeight); ++depthIndex) diff --git a/corelib/src/camera/CameraOpenNI2.cpp b/corelib/src/camera/CameraOpenNI2.cpp index 8be86b74..4d2ff1ad 100644 --- a/corelib/src/camera/CameraOpenNI2.cpp +++ b/corelib/src/camera/CameraOpenNI2.cpp @@ -29,7 +29,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include -#include #ifdef RTABMAP_OPENNI2 #include @@ -514,7 +513,7 @@ SensorData CameraOpenNI2::captureImage(SensorCaptureInfo * info) cv::Mat tmp(h, w, CV_8UC3, (void *)colorFrame.getData()); if(_type==kTypeColorDepth) { - cv::cvtColor(tmp, rgb, CV_RGB2BGR); + cv::cvtColor(tmp, rgb, cv::COLOR_RGB2BGR); } else // IR { diff --git a/corelib/src/camera/CameraOpenNICV.cpp b/corelib/src/camera/CameraOpenNICV.cpp index 13536d6f..13595610 100644 --- a/corelib/src/camera/CameraOpenNICV.cpp +++ b/corelib/src/camera/CameraOpenNICV.cpp @@ -26,9 +26,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include -#if CV_MAJOR_VERSION > 3 -#include -#endif +#include namespace rtabmap { @@ -59,30 +57,34 @@ bool CameraOpenNICV::init(const std::string & calibrationFolder, const std::stri } ULOGGER_DEBUG("Camera::init()"); - _capture.open( _asus?CV_CAP_OPENNI_ASUS:CV_CAP_OPENNI ); +#if CV_MAJOR_VERSION < 5 + _capture.open( _asus?cv::CAP_OPENNI_ASUS:cv::CAP_OPENNI ); +#else + _capture.open( _asus?cv::CAP_OPENNI2_ASUS:cv::CAP_OPENNI2 ); +#endif if(_capture.isOpened()) { - _capture.set( CV_CAP_OPENNI_IMAGE_GENERATOR_OUTPUT_MODE, CV_CAP_OPENNI_VGA_30HZ ); - _depthFocal = _capture.get( CV_CAP_OPENNI_DEPTH_GENERATOR_FOCAL_LENGTH ); + _capture.set( cv::CAP_OPENNI_IMAGE_GENERATOR_OUTPUT_MODE, cv::CAP_OPENNI_VGA_30HZ ); + _depthFocal = _capture.get( cv::CAP_OPENNI_DEPTH_GENERATOR_FOCAL_LENGTH ); // Print some avalible device settings. UINFO("Depth generator output mode:"); - UINFO("FRAME_WIDTH %f", _capture.get( CV_CAP_PROP_FRAME_WIDTH )); - UINFO("FRAME_HEIGHT %f", _capture.get( CV_CAP_PROP_FRAME_HEIGHT )); - UINFO("FRAME_MAX_DEPTH %f mm", _capture.get( CV_CAP_PROP_OPENNI_FRAME_MAX_DEPTH )); - UINFO("BASELINE %f mm", _capture.get( CV_CAP_PROP_OPENNI_BASELINE )); - UINFO("FPS %f", _capture.get( CV_CAP_PROP_FPS )); - UINFO("Focal %f", _capture.get( CV_CAP_OPENNI_DEPTH_GENERATOR_FOCAL_LENGTH )); - UINFO("REGISTRATION %f", _capture.get( CV_CAP_PROP_OPENNI_REGISTRATION )); - if(_capture.get( CV_CAP_PROP_OPENNI_REGISTRATION ) == 0.0) + UINFO("FRAME_WIDTH %f", _capture.get( cv::CAP_PROP_FRAME_WIDTH )); + UINFO("FRAME_HEIGHT %f", _capture.get( cv::CAP_PROP_FRAME_HEIGHT )); + UINFO("FRAME_MAX_DEPTH %f mm", _capture.get( cv::CAP_PROP_OPENNI_FRAME_MAX_DEPTH )); + UINFO("BASELINE %f mm", _capture.get( cv::CAP_PROP_OPENNI_BASELINE )); + UINFO("FPS %f", _capture.get( cv::CAP_PROP_FPS )); + UINFO("Focal %f", _capture.get( cv::CAP_OPENNI_DEPTH_GENERATOR_FOCAL_LENGTH )); + UINFO("REGISTRATION %f", _capture.get( cv::CAP_PROP_OPENNI_REGISTRATION )); + if(_capture.get( cv::CAP_PROP_OPENNI_REGISTRATION ) == 0.0) { UERROR("Depth registration is not activated on this device!"); } - if( _capture.get( CV_CAP_OPENNI_IMAGE_GENERATOR_PRESENT ) ) + if( _capture.get( cv::CAP_OPENNI_IMAGE_GENERATOR_PRESENT ) ) { UINFO("Image generator output mode:"); - UINFO("FRAME_WIDTH %f", _capture.get( CV_CAP_OPENNI_IMAGE_GENERATOR+CV_CAP_PROP_FRAME_WIDTH )); - UINFO("FRAME_HEIGHT %f", _capture.get( CV_CAP_OPENNI_IMAGE_GENERATOR+CV_CAP_PROP_FRAME_HEIGHT )); - UINFO("FPS %f", _capture.get( CV_CAP_OPENNI_IMAGE_GENERATOR+CV_CAP_PROP_FPS )); + UINFO("FRAME_WIDTH %f", _capture.get( cv::CAP_OPENNI_IMAGE_GENERATOR+cv::CAP_PROP_FRAME_WIDTH )); + UINFO("FRAME_HEIGHT %f", _capture.get( cv::CAP_OPENNI_IMAGE_GENERATOR+cv::CAP_PROP_FRAME_HEIGHT )); + UINFO("FPS %f", _capture.get( cv::CAP_OPENNI_IMAGE_GENERATOR+cv::CAP_PROP_FPS )); } else { @@ -112,8 +114,8 @@ SensorData CameraOpenNICV::captureImage(SensorCaptureInfo * info) { _capture.grab(); cv::Mat depth, rgb; - _capture.retrieve(depth, CV_CAP_OPENNI_DEPTH_MAP ); - _capture.retrieve(rgb, CV_CAP_OPENNI_BGR_IMAGE ); + _capture.retrieve(depth, cv::CAP_OPENNI_DEPTH_MAP ); + _capture.retrieve(rgb, cv::CAP_OPENNI_BGR_IMAGE ); depth = depth.clone(); rgb = rgb.clone(); diff --git a/corelib/src/camera/CameraOpenni.cpp b/corelib/src/camera/CameraOpenni.cpp index 9fb8dc73..68cba168 100644 --- a/corelib/src/camera/CameraOpenni.cpp +++ b/corelib/src/camera/CameraOpenni.cpp @@ -28,7 +28,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include -#include #ifdef RTABMAP_OPENNI #include @@ -94,7 +93,7 @@ void CameraOpenni::image_cb ( cv::Mat rgbFrame(rgb->getHeight(), rgb->getWidth(), CV_8UC3); rgb->fillRGB(rgb->getWidth(), rgb->getHeight(), rgbFrame.data); - cv::cvtColor(rgbFrame, rgb_, CV_RGB2BGR); + cv::cvtColor(rgbFrame, rgb_, cv::COLOR_RGB2BGR); depth_ = cv::Mat(rgb->getHeight(), rgb->getWidth(), CV_16UC1); depth->fillDepthImageRaw(rgb->getWidth(), rgb->getHeight(), (unsigned short*)depth_.data); diff --git a/corelib/src/camera/CameraRealSense.cpp b/corelib/src/camera/CameraRealSense.cpp index 3f545bb5..01362382 100644 --- a/corelib/src/camera/CameraRealSense.cpp +++ b/corelib/src/camera/CameraRealSense.cpp @@ -29,7 +29,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include -#include #ifdef RTABMAP_REALSENSE #include @@ -938,7 +937,7 @@ SensorData CameraRealSense::captureImage(SensorCaptureInfo * info) } else { - cv::cvtColor(rgb, bgr, CV_RGB2BGR); + cv::cvtColor(rgb, bgr, cv::COLOR_RGB2BGR); } bool rectified = false; diff --git a/corelib/src/camera/CameraRealSense2.cpp b/corelib/src/camera/CameraRealSense2.cpp index 8b80984f..5942d83f 100644 --- a/corelib/src/camera/CameraRealSense2.cpp +++ b/corelib/src/camera/CameraRealSense2.cpp @@ -31,7 +31,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include -#include #ifdef RTABMAP_REALSENSE2 #include @@ -1440,7 +1439,7 @@ SensorData CameraRealSense2::captureImage(SensorCaptureInfo * info) cv::Mat bgr; if(rgb.channels() == 3) { - cv::cvtColor(rgb, bgr, CV_RGB2BGR); + cv::cvtColor(rgb, bgr, cv::COLOR_RGB2BGR); } else { diff --git a/corelib/src/camera/CameraStereoDC1394.cpp b/corelib/src/camera/CameraStereoDC1394.cpp index 691516b7..2c211088 100644 --- a/corelib/src/camera/CameraStereoDC1394.cpp +++ b/corelib/src/camera/CameraStereoDC1394.cpp @@ -28,7 +28,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include -#include #ifdef RTABMAP_DC1394 #include @@ -295,8 +294,8 @@ public: //DC1394_COLOR_CODING_RAW16: //DC1394_COLOR_FILTER_BGGR - cv::cvtColor(cv::Mat(frame->size[1], frame->size[0], CV_8UC1, capture_buffer), left, CV_BayerRG2BGR); - cv::cvtColor(cv::Mat(frame->size[1], frame->size[0], CV_8UC1, capture_buffer+image.total()), right, CV_BayerRG2GRAY); + cv::cvtColor(cv::Mat(frame->size[1], frame->size[0], CV_8UC1, capture_buffer), left, cv::COLOR_BayerRG2BGR); + cv::cvtColor(cv::Mat(frame->size[1], frame->size[0], CV_8UC1, capture_buffer+image.total()), right, cv::COLOR_BayerRG2GRAY); dc1394_capture_enqueue(camera_, frame); diff --git a/corelib/src/camera/CameraStereoImages.cpp b/corelib/src/camera/CameraStereoImages.cpp index 67f7bb7b..365c7227 100644 --- a/corelib/src/camera/CameraStereoImages.cpp +++ b/corelib/src/camera/CameraStereoImages.cpp @@ -29,7 +29,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include -#include namespace rtabmap { @@ -339,7 +338,7 @@ SensorData CameraStereoImages::captureImage(SensorCaptureInfo * info) if(rightImage.type() != CV_8UC1 && rightGrayScale_) { cv::Mat tmp; - cv::cvtColor(rightImage, tmp, CV_BGR2GRAY); + cv::cvtColor(rightImage, tmp, cv::COLOR_BGR2GRAY); rightImage = tmp; } diff --git a/corelib/src/camera/CameraStereoTara.cpp b/corelib/src/camera/CameraStereoTara.cpp index e4ea3cdf..f4ec60aa 100644 --- a/corelib/src/camera/CameraStereoTara.cpp +++ b/corelib/src/camera/CameraStereoTara.cpp @@ -34,9 +34,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include -#if CV_MAJOR_VERSION > 3 -#include -#endif +#include namespace rtabmap { @@ -81,18 +79,18 @@ bool CameraStereoTara::init(const std::string & calibrationFolder, const std::st capture_.open(usbDevice_); - capture_.set(CV_CAP_PROP_FOURCC, CV_FOURCC('Y', '1', '6', ' ')); - capture_.set(CV_CAP_PROP_FPS, 60); - capture_.set(CV_CAP_PROP_FRAME_WIDTH, 752); - capture_.set(CV_CAP_PROP_FRAME_HEIGHT, 480); - capture_.set(CV_CAP_PROP_CONVERT_RGB,false); + capture_.set(cv::CAP_PROP_FOURCC, cv::VideoWriter::fourcc('Y', '1', '6', ' ')); + capture_.set(cv::CAP_PROP_FPS, 60); + capture_.set(cv::CAP_PROP_FRAME_WIDTH, 752); + capture_.set(cv::CAP_PROP_FRAME_HEIGHT, 480); + capture_.set(cv::CAP_PROP_CONVERT_RGB,false); ULOGGER_DEBUG("CameraStereoTara: Usb device initialization on device %d", usbDevice_); if (cameraName_.empty()) { - unsigned int guid = (unsigned int)capture_.get(CV_CAP_PROP_GUID); + unsigned int guid = (unsigned int)capture_.get(cv::CAP_PROP_GUID); if (guid != 0 && guid != 0xffffffff) { cameraName_ = uFormat("%08x", guid); diff --git a/corelib/src/camera/CameraStereoVideo.cpp b/corelib/src/camera/CameraStereoVideo.cpp index f31f40b6..5de23a67 100644 --- a/corelib/src/camera/CameraStereoVideo.cpp +++ b/corelib/src/camera/CameraStereoVideo.cpp @@ -28,13 +28,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include -#include -#if CV_MAJOR_VERSION > 3 -#include -#if CV_MAJOR_VERSION > 4 -#include -#endif -#endif +#include namespace rtabmap { @@ -172,7 +166,7 @@ bool CameraStereoVideo::init(const std::string & calibrationFolder, const std::s if (cameraName_.empty()) { - unsigned int guid = (unsigned int)capture_.get(CV_CAP_PROP_GUID); + unsigned int guid = (unsigned int)capture_.get(cv::CAP_PROP_GUID); if (guid != 0 && guid != 0xffffffff) { cameraName_ = uFormat("%08x", guid); @@ -214,17 +208,17 @@ bool CameraStereoVideo::init(const std::string & calibrationFolder, const std::s if(capture_.isOpened()) { bool resolutionSet = false; - resolutionSet = capture_.set(CV_CAP_PROP_FRAME_WIDTH, stereoModel_.left().imageWidth()*(capture2_.isOpened()?1:2)); - resolutionSet = resolutionSet && capture_.set(CV_CAP_PROP_FRAME_HEIGHT, stereoModel_.left().imageHeight()); + resolutionSet = capture_.set(cv::CAP_PROP_FRAME_WIDTH, stereoModel_.left().imageWidth()*(capture2_.isOpened()?1:2)); + resolutionSet = resolutionSet && capture_.set(cv::CAP_PROP_FRAME_HEIGHT, stereoModel_.left().imageHeight()); if(capture2_.isOpened()) { - resolutionSet = resolutionSet && capture2_.set(CV_CAP_PROP_FRAME_WIDTH, stereoModel_.right().imageWidth()); - resolutionSet = resolutionSet && capture2_.set(CV_CAP_PROP_FRAME_HEIGHT, stereoModel_.right().imageHeight()); + resolutionSet = resolutionSet && capture2_.set(cv::CAP_PROP_FRAME_WIDTH, stereoModel_.right().imageWidth()); + resolutionSet = resolutionSet && capture2_.set(cv::CAP_PROP_FRAME_HEIGHT, stereoModel_.right().imageHeight()); } // Check if the resolution was set successfully - int actualWidth = int(capture_.get(CV_CAP_PROP_FRAME_WIDTH)); - int actualHeight = int(capture_.get(CV_CAP_PROP_FRAME_HEIGHT)); + int actualWidth = int(capture_.get(cv::CAP_PROP_FRAME_WIDTH)); + int actualHeight = int(capture_.get(cv::CAP_PROP_FRAME_HEIGHT)); if(!resolutionSet || actualWidth != stereoModel_.left().imageWidth()*(capture2_.isOpened()?1:2) || actualHeight != stereoModel_.left().imageHeight()) @@ -244,17 +238,17 @@ bool CameraStereoVideo::init(const std::string & calibrationFolder, const std::s if(capture_.isOpened()) { bool resolutionSet = false; - resolutionSet = capture_.set(CV_CAP_PROP_FRAME_WIDTH, _width*(capture2_.isOpened()?1:2)); - resolutionSet = resolutionSet && capture_.set(CV_CAP_PROP_FRAME_HEIGHT, _height); + resolutionSet = capture_.set(cv::CAP_PROP_FRAME_WIDTH, _width*(capture2_.isOpened()?1:2)); + resolutionSet = resolutionSet && capture_.set(cv::CAP_PROP_FRAME_HEIGHT, _height); if(capture2_.isOpened()) { - resolutionSet = resolutionSet && capture2_.set(CV_CAP_PROP_FRAME_WIDTH, _width); - resolutionSet = resolutionSet && capture2_.set(CV_CAP_PROP_FRAME_HEIGHT, _height); + resolutionSet = resolutionSet && capture2_.set(cv::CAP_PROP_FRAME_WIDTH, _width); + resolutionSet = resolutionSet && capture2_.set(cv::CAP_PROP_FRAME_HEIGHT, _height); } // Check if the resolution was set successfully - int actualWidth = int(capture_.get(CV_CAP_PROP_FRAME_WIDTH)); - int actualHeight = int(capture_.get(CV_CAP_PROP_FRAME_HEIGHT)); + int actualWidth = int(capture_.get(cv::CAP_PROP_FRAME_WIDTH)); + int actualHeight = int(capture_.get(cv::CAP_PROP_FRAME_HEIGHT)); if(!resolutionSet || actualWidth != _width*(capture2_.isOpened()?1:2) || actualHeight != _height) @@ -273,10 +267,10 @@ bool CameraStereoVideo::init(const std::string & calibrationFolder, const std::s if (this->getFrameRate() > 0) { bool fpsSupported = false; - fpsSupported = capture_.set(CV_CAP_PROP_FPS, this->getFrameRate()); + fpsSupported = capture_.set(cv::CAP_PROP_FPS, this->getFrameRate()); if (capture2_.isOpened()) { - fpsSupported = fpsSupported && capture2_.set(CV_CAP_PROP_FPS, this->getFrameRate()); + fpsSupported = fpsSupported && capture2_.set(cv::CAP_PROP_FPS, this->getFrameRate()); } if(fpsSupported) { @@ -310,14 +304,14 @@ bool CameraStereoVideo::init(const std::string & calibrationFolder, const std::s std::string fourccUpperCase = uToUpperCase(_fourcc); int fourcc = cv::VideoWriter::fourcc(fourccUpperCase.at(0), fourccUpperCase.at(1), fourccUpperCase.at(2), fourccUpperCase.at(3)); bool fourccSupported = false; - fourccSupported = capture_.set(CV_CAP_PROP_FOURCC, fourcc); + fourccSupported = capture_.set(cv::CAP_PROP_FOURCC, fourcc); if (capture2_.isOpened()) { - fourccSupported = fourccSupported && capture2_.set(CV_CAP_PROP_FOURCC, fourcc); + fourccSupported = fourccSupported && capture2_.set(cv::CAP_PROP_FOURCC, fourcc); } // Check if the FOURCC was set successfully - int actualFourcc = int(capture_.get(CV_CAP_PROP_FOURCC)); + int actualFourcc = int(capture_.get(cv::CAP_PROP_FOURCC)); if(!fourccSupported || actualFourcc != fourcc) { @@ -386,7 +380,7 @@ SensorData CameraStereoVideo::captureImage(SensorCaptureInfo * info) if(rightImage.type() != CV_8UC1 && rightGrayScale_) { cv::Mat tmp; - cv::cvtColor(rightImage, tmp, CV_BGR2GRAY); + cv::cvtColor(rightImage, tmp, cv::COLOR_BGR2GRAY); rightImage = tmp; rightCvt = true; } diff --git a/corelib/src/camera/CameraStereoZedOC.cpp b/corelib/src/camera/CameraStereoZedOC.cpp index 014976a6..e3ba6d43 100644 --- a/corelib/src/camera/CameraStereoZedOC.cpp +++ b/corelib/src/camera/CameraStereoZedOC.cpp @@ -38,6 +38,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include "SimpleIni.h" +#if CV_MAJOR_VERSION >= 5 +#include +#endif + /////////////////////////////////////////////////////////////////////////// // // Copyright (c) 2018, STEREOLABS. diff --git a/corelib/src/camera/CameraVideo.cpp b/corelib/src/camera/CameraVideo.cpp index 9a0a70d0..1776bca1 100644 --- a/corelib/src/camera/CameraVideo.cpp +++ b/corelib/src/camera/CameraVideo.cpp @@ -28,12 +28,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include -#if CV_MAJOR_VERSION > 3 -#include -#if CV_MAJOR_VERSION > 4 -#include -#endif -#endif +#include namespace rtabmap { @@ -105,7 +100,7 @@ bool CameraVideo::init(const std::string & calibrationFolder, const std::string { if (_guid.empty()) { - unsigned int guid = (unsigned int)_capture.get(CV_CAP_PROP_GUID); + unsigned int guid = (unsigned int)_capture.get(cv::CAP_PROP_GUID); if (guid != 0 && guid != 0xffffffff) { _guid = uFormat("%08x", guid); @@ -143,12 +138,12 @@ bool CameraVideo::init(const std::string & calibrationFolder, const std::string } bool resolutionSet = false; - resolutionSet = _capture.set(CV_CAP_PROP_FRAME_WIDTH, _model.imageWidth()); - resolutionSet = resolutionSet && _capture.set(CV_CAP_PROP_FRAME_HEIGHT, _model.imageHeight()); + resolutionSet = _capture.set(cv::CAP_PROP_FRAME_WIDTH, _model.imageWidth()); + resolutionSet = resolutionSet && _capture.set(cv::CAP_PROP_FRAME_HEIGHT, _model.imageHeight()); // Check if the resolution was set successfully - int actualWidth = int(_capture.get(CV_CAP_PROP_FRAME_WIDTH)); - int actualHeight = int(_capture.get(CV_CAP_PROP_FRAME_HEIGHT)); + int actualWidth = int(_capture.get(cv::CAP_PROP_FRAME_WIDTH)); + int actualHeight = int(_capture.get(cv::CAP_PROP_FRAME_HEIGHT)); if(!resolutionSet || actualWidth != _model.imageWidth() || actualHeight != _model.imageHeight()) @@ -165,12 +160,12 @@ bool CameraVideo::init(const std::string & calibrationFolder, const std::string else if(_width > 0 && _height > 0) { int resolutionSet = false; - resolutionSet = _capture.set(CV_CAP_PROP_FRAME_WIDTH, _width); - resolutionSet = resolutionSet && _capture.set(CV_CAP_PROP_FRAME_HEIGHT, _height); + resolutionSet = _capture.set(cv::CAP_PROP_FRAME_WIDTH, _width); + resolutionSet = resolutionSet && _capture.set(cv::CAP_PROP_FRAME_HEIGHT, _height); // Check if the resolution was set successfully - int actualWidth = int(_capture.get(CV_CAP_PROP_FRAME_WIDTH)); - int actualHeight = int(_capture.get(CV_CAP_PROP_FRAME_HEIGHT)); + int actualWidth = int(_capture.get(cv::CAP_PROP_FRAME_WIDTH)); + int actualHeight = int(_capture.get(cv::CAP_PROP_FRAME_HEIGHT)); if(!resolutionSet || actualWidth != _width || actualHeight != _height) { UWARN("Desired resolution (%dx%d) cannot be set to camera driver, " @@ -182,7 +177,7 @@ bool CameraVideo::init(const std::string & calibrationFolder, const std::string } // Set FPS - if (this->getFrameRate() > 0 && _capture.set(CV_CAP_PROP_FPS, this->getFrameRate())) + if (this->getFrameRate() > 0 && _capture.set(cv::CAP_PROP_FPS, this->getFrameRate())) { // Check if the FPS was set successfully double actualFPS = _capture.get(cv::CAP_PROP_FPS); @@ -213,10 +208,10 @@ bool CameraVideo::init(const std::string & calibrationFolder, const std::string std::string fourccUpperCase = uToUpperCase(_fourcc); int fourcc = cv::VideoWriter::fourcc(fourccUpperCase.at(0), fourccUpperCase.at(1), fourccUpperCase.at(2), fourccUpperCase.at(3)); - bool fourccSupported = _capture.set(CV_CAP_PROP_FOURCC, fourcc); + bool fourccSupported = _capture.set(cv::CAP_PROP_FOURCC, fourcc); // Check if the FOURCC was set successfully - int actualFourcc = int(_capture.get(CV_CAP_PROP_FOURCC)); + int actualFourcc = int(_capture.get(cv::CAP_PROP_FOURCC)); if(!fourccSupported || actualFourcc != fourcc) { diff --git a/corelib/src/odometry/OdometryDVO.cpp b/corelib/src/odometry/OdometryDVO.cpp index 2f7190d0..a1e425d3 100644 --- a/corelib/src/odometry/OdometryDVO.cpp +++ b/corelib/src/odometry/OdometryDVO.cpp @@ -31,7 +31,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/utilite/ULogger.h" #include "rtabmap/utilite/UTimer.h" #include "rtabmap/utilite/UStl.h" -#include #ifdef RTABMAP_DVO #include @@ -124,7 +123,7 @@ Transform OdometryDVO::computeTransform( { if(data.imageRaw().type() == CV_8UC3) { - cv::cvtColor(data.imageRaw(), grey, CV_BGR2GRAY); + cv::cvtColor(data.imageRaw(), grey, cv::COLOR_BGR2GRAY); } else { diff --git a/corelib/src/odometry/OdometryF2M.cpp b/corelib/src/odometry/OdometryF2M.cpp index 58d0e3aa..63b096d9 100644 --- a/corelib/src/odometry/OdometryF2M.cpp +++ b/corelib/src/odometry/OdometryF2M.cpp @@ -42,7 +42,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/utilite/UTimer.h" #include "rtabmap/utilite/UMath.h" #include "rtabmap/utilite/UConversion.h" +#if CV_MAJOR_VERSION < 5 #include +#else +#include +#endif #include #include diff --git a/corelib/src/odometry/OdometryFovis.cpp b/corelib/src/odometry/OdometryFovis.cpp index 673639ad..c1f5a997 100644 --- a/corelib/src/odometry/OdometryFovis.cpp +++ b/corelib/src/odometry/OdometryFovis.cpp @@ -31,7 +31,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/utilite/ULogger.h" #include "rtabmap/utilite/UTimer.h" #include "rtabmap/utilite/UStl.h" -#include #ifdef RTABMAP_FOVIS #include @@ -137,7 +136,7 @@ Transform OdometryFovis::computeTransform( cv::Mat gray; if(data.imageRaw().type() == CV_8UC3) { - cv::cvtColor(data.imageRaw(), gray, CV_BGR2GRAY); + cv::cvtColor(data.imageRaw(), gray, cv::COLOR_BGR2GRAY); } else if(data.imageRaw().type() == CV_8UC1) { @@ -302,7 +301,7 @@ Transform OdometryFovis::computeTransform( } if(data.rightRaw().type() == CV_8UC3) { - cv::cvtColor(data.rightRaw(), right, CV_BGR2GRAY); + cv::cvtColor(data.rightRaw(), right, cv::COLOR_BGR2GRAY); } else if(data.rightRaw().type() == CV_8UC1) { diff --git a/corelib/src/odometry/OdometryMSCKF.cpp b/corelib/src/odometry/OdometryMSCKF.cpp index ac62c251..709f9d11 100644 --- a/corelib/src/odometry/OdometryMSCKF.cpp +++ b/corelib/src/odometry/OdometryMSCKF.cpp @@ -32,7 +32,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/utilite/UTimer.h" #include "rtabmap/utilite/UStl.h" #include "rtabmap/utilite/UThread.h" -#include #ifdef RTABMAP_MSCKF_VIO #include @@ -867,7 +866,7 @@ Transform OdometryMSCKF::computeTransform( if(data.imageRaw().type() == CV_8UC3) { - cv::cvtColor(data.imageRaw(), cam0.image, CV_BGR2GRAY); + cv::cvtColor(data.imageRaw(), cam0.image, cv::COLOR_BGR2GRAY); } else { @@ -875,7 +874,7 @@ Transform OdometryMSCKF::computeTransform( } if(data.rightRaw().type() == CV_8UC3) { - cv::cvtColor(data.rightRaw(), cam1.image, CV_BGR2GRAY); + cv::cvtColor(data.rightRaw(), cam1.image, cv::COLOR_BGR2GRAY); } else { diff --git a/corelib/src/odometry/OdometryMono.cpp b/corelib/src/odometry/OdometryMono.cpp index b942aa88..e29e2e24 100644 --- a/corelib/src/odometry/OdometryMono.cpp +++ b/corelib/src/odometry/OdometryMono.cpp @@ -43,7 +43,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/utilite/UStl.h" #include "rtabmap/utilite/UMath.h" #include +#if CV_MAJOR_VERSION < 5 #include +#else +#include +#endif #include #include diff --git a/corelib/src/odometry/OdometryORBSLAM2.cpp b/corelib/src/odometry/OdometryORBSLAM2.cpp index 1ae10bc1..616678cf 100644 --- a/corelib/src/odometry/OdometryORBSLAM2.cpp +++ b/corelib/src/odometry/OdometryORBSLAM2.cpp @@ -33,7 +33,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/utilite/UStl.h" #include "rtabmap/utilite/UDirectory.h" #include -#include #include #if defined(RTABMAP_ORB_SLAM) and RTABMAP_ORB_SLAM == 2 @@ -426,7 +425,7 @@ public: } else { - cvtColor(mImGray,mImGray,CV_BGR2GRAY); + cvtColor(mImGray,mImGray,cv::COLOR_BGR2GRAY); } } else if(mImGray.channels()==4) @@ -437,7 +436,7 @@ public: } else { - cvtColor(mImGray,mImGray,CV_BGRA2GRAY); + cvtColor(mImGray,mImGray,cv::COLOR_BGRA2GRAY); } } if(imGrayRight.channels()==3) @@ -448,7 +447,7 @@ public: } else { - cvtColor(imGrayRight,imGrayRight,CV_BGR2GRAY); + cvtColor(imGrayRight,imGrayRight,cv::COLOR_BGR2GRAY); } } else if(imGrayRight.channels()==4) @@ -459,7 +458,7 @@ public: } else { - cvtColor(imGrayRight,imGrayRight,CV_BGRA2GRAY); + cvtColor(imGrayRight,imGrayRight,cv::COLOR_BGRA2GRAY); } } @@ -480,14 +479,14 @@ public: if(mbRGB) cvtColor(mImGray,mImGray,CV_RGB2GRAY); else - cvtColor(mImGray,mImGray,CV_BGR2GRAY); + cvtColor(mImGray,mImGray,cv::COLOR_BGR2GRAY); } else if(mImGray.channels()==4) { if(mbRGB) cvtColor(mImGray,mImGray,CV_RGBA2GRAY); else - cvtColor(mImGray,mImGray,CV_BGRA2GRAY); + cvtColor(mImGray,mImGray,cv::COLOR_BGRA2GRAY); } UASSERT(imDepth.type()==CV_32F); diff --git a/corelib/src/odometry/OdometryORBSLAM3.cpp b/corelib/src/odometry/OdometryORBSLAM3.cpp index f0e5d644..77e7d45c 100644 --- a/corelib/src/odometry/OdometryORBSLAM3.cpp +++ b/corelib/src/odometry/OdometryORBSLAM3.cpp @@ -34,7 +34,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/utilite/UDirectory.h" #include "rtabmap/utilite/UFile.h" #include -#include #include #if defined(RTABMAP_ORB_SLAM) and RTABMAP_ORB_SLAM == 3 @@ -469,12 +468,12 @@ Transform OdometryORBSLAM3::computeTransform( cv::Mat leftMono = data.imageRaw(); if(data.imageRaw().channels() == 3) { leftMono = cv::Mat(); - cv::cvtColor(data.imageRaw(), leftMono, CV_BGR2GRAY); + cv::cvtColor(data.imageRaw(), leftMono, cv::COLOR_BGR2GRAY); } cv::Mat rightMono = data.rightRaw(); if(data.rightRaw().channels() == 3) { rightMono = cv::Mat(); - cv::cvtColor(data.imageRaw(), rightMono, CV_BGR2GRAY); + cv::cvtColor(data.imageRaw(), rightMono, cv::COLOR_BGR2GRAY); } UDEBUG("Adding Stereo Frame %f", data.stamp()); Tcw = orbslam_->TrackStereo(leftMono, rightMono, data.stamp(), orbslamImus_); diff --git a/corelib/src/odometry/OdometryOkvis.cpp b/corelib/src/odometry/OdometryOkvis.cpp index 10497b9e..7c97e274 100644 --- a/corelib/src/odometry/OdometryOkvis.cpp +++ b/corelib/src/odometry/OdometryOkvis.cpp @@ -34,7 +34,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/utilite/UThread.h" #include "rtabmap/utilite/UFile.h" #include "rtabmap/utilite/UDirectory.h" -#include #ifdef RTABMAP_OKVIS #include @@ -427,7 +426,7 @@ Transform OdometryOkvis::computeTransform( cv::Mat gray; if(images[i].type() == CV_8UC3) { - cv::cvtColor(images[i], gray, CV_BGR2GRAY); + cv::cvtColor(images[i], gray, cv::COLOR_BGR2GRAY); } else if(images[i].type() == CV_8UC1) { diff --git a/corelib/src/odometry/OdometryOpenVINS.cpp b/corelib/src/odometry/OdometryOpenVINS.cpp index e9539688..4c9dcc41 100644 --- a/corelib/src/odometry/OdometryOpenVINS.cpp +++ b/corelib/src/odometry/OdometryOpenVINS.cpp @@ -32,7 +32,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/utilite/ULogger.h" #include "rtabmap/utilite/UTimer.h" #include -#include #ifdef RTABMAP_OPENVINS #include "core/VioManager.h" @@ -419,7 +418,7 @@ Transform OdometryOpenVINS::computeTransform( cv::Mat image; if(data.imageRaw().type() == CV_8UC3) - cv::cvtColor(data.imageRaw(), image, CV_BGR2GRAY); + cv::cvtColor(data.imageRaw(), image, cv::COLOR_BGR2GRAY); else if(data.imageRaw().type() == CV_8UC1) image = data.imageRaw().clone(); else @@ -450,7 +449,7 @@ Transform OdometryOpenVINS::computeTransform( if(!data.rightRaw().empty()) { if(data.rightRaw().type() == CV_8UC3) - cv::cvtColor(data.rightRaw(), image, CV_BGR2GRAY); + cv::cvtColor(data.rightRaw(), image, cv::COLOR_BGR2GRAY); else if(data.rightRaw().type() == CV_8UC1) image = data.rightRaw().clone(); else diff --git a/corelib/src/odometry/OdometryVINSFusion.cpp b/corelib/src/odometry/OdometryVINSFusion.cpp index 9c633605..96779403 100644 --- a/corelib/src/odometry/OdometryVINSFusion.cpp +++ b/corelib/src/odometry/OdometryVINSFusion.cpp @@ -33,7 +33,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/utilite/UStl.h" #include "rtabmap/utilite/UThread.h" #include "rtabmap/utilite/UDirectory.h" -#include #ifdef RTABMAP_VINS_FUSION #include @@ -444,7 +443,7 @@ Transform OdometryVINSFusion::computeTransform( cv::Mat right; if(data.imageRaw().type() == CV_8UC3) { - cv::cvtColor(data.imageRaw(), left, CV_BGR2GRAY); + cv::cvtColor(data.imageRaw(), left, cv::COLOR_BGR2GRAY); } else if(data.imageRaw().type() == CV_8UC1) { @@ -456,7 +455,7 @@ Transform OdometryVINSFusion::computeTransform( } if(data.rightRaw().type() == CV_8UC3) { - cv::cvtColor(data.rightRaw(), right, CV_BGR2GRAY); + cv::cvtColor(data.rightRaw(), right, cv::COLOR_BGR2GRAY); } else if(data.rightRaw().type() == CV_8UC1) { diff --git a/corelib/src/odometry/OdometryViso2.cpp b/corelib/src/odometry/OdometryViso2.cpp index 808e7637..dc6192b4 100644 --- a/corelib/src/odometry/OdometryViso2.cpp +++ b/corelib/src/odometry/OdometryViso2.cpp @@ -31,7 +31,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/utilite/ULogger.h" #include "rtabmap/utilite/UTimer.h" #include "rtabmap/utilite/UStl.h" -#include #ifdef RTABMAP_VISO2 #include @@ -131,7 +130,7 @@ Transform OdometryViso2::computeTransform( cv::Mat leftGray; if(data.imageRaw().type() == CV_8UC3) { - cv::cvtColor(data.imageRaw(), leftGray, CV_BGR2GRAY); + cv::cvtColor(data.imageRaw(), leftGray, cv::COLOR_BGR2GRAY); } else if(data.imageRaw().type() == CV_8UC1) { @@ -144,7 +143,7 @@ Transform OdometryViso2::computeTransform( cv::Mat rightGray; if(data.rightRaw().type() == CV_8UC3) { - cv::cvtColor(data.rightRaw(), rightGray, CV_BGR2GRAY); + cv::cvtColor(data.rightRaw(), rightGray, cv::COLOR_BGR2GRAY); } else if(data.rightRaw().type() == CV_8UC1) { diff --git a/corelib/src/opencv/ORBextractor.cc b/corelib/src/opencv/ORBextractor.cc index 2d3cd7cc..542e169c 100644 --- a/corelib/src/opencv/ORBextractor.cc +++ b/corelib/src/opencv/ORBextractor.cc @@ -64,7 +64,11 @@ #include #include +#if CV_MAJOR_VERSION < 5 #include +#else +#include +#endif #include #include #include diff --git a/corelib/src/opencv/ORBextractor.h b/corelib/src/opencv/ORBextractor.h index 861ca05e..82ede5b8 100644 --- a/corelib/src/opencv/ORBextractor.h +++ b/corelib/src/opencv/ORBextractor.h @@ -31,8 +31,6 @@ #include #include -#include - namespace rtabmap { diff --git a/corelib/src/opencv/Orb.cpp b/corelib/src/opencv/Orb.cpp index f56874e7..c5034a7f 100644 --- a/corelib/src/opencv/Orb.cpp +++ b/corelib/src/opencv/Orb.cpp @@ -40,7 +40,6 @@ #include "opencv2/features2d/features2d.hpp" #include "opencv2/imgproc/imgproc.hpp" -#include "opencv2/imgproc/imgproc_c.h" #include #include @@ -252,7 +251,7 @@ static void computeOrbDescriptor(const KeyPoint& kpt, } } else - CV_Error( CV_StsBadSize, "Wrong WTA_K. It can be only 2, 3 or 4." ); + CV_Error( cv::Error::StsBadSize, "Wrong WTA_K. It can be only 2, 3 or 4." ); #undef GET_VALUE } @@ -752,7 +751,7 @@ void CV_ORB::operator()( InputArray _image, InputArray _mask, std::vectornlevels; diff --git a/corelib/src/opencv/five-point.h b/corelib/src/opencv/five-point.h index 1a5b778a..f1aa8c7e 100644 --- a/corelib/src/opencv/five-point.h +++ b/corelib/src/opencv/five-point.h @@ -8,6 +8,10 @@ #ifndef CORELIB_SRC_OPENCV_FIVE_POINT_H_ #define CORELIB_SRC_OPENCV_FIVE_POINT_H_ +#if CV_MAJOR_VERSION > 4 +#include +#endif + namespace cv3 { diff --git a/corelib/src/opencv/solvepnp.cpp b/corelib/src/opencv/solvepnp.cpp index f5b3a077..ecd2be9a 100644 --- a/corelib/src/opencv/solvepnp.cpp +++ b/corelib/src/opencv/solvepnp.cpp @@ -53,7 +53,7 @@ class PnPRansacCallback : public PointSetRegistrator::Callback public: - PnPRansacCallback(Mat _cameraMatrix=Mat(3,3,CV_64F), Mat _distCoeffs=Mat(4,1,CV_64F), int _flags=CV_ITERATIVE, + PnPRansacCallback(Mat _cameraMatrix=Mat(3,3,CV_64F), Mat _distCoeffs=Mat(4,1,CV_64F), int _flags=cv::SOLVEPNP_ITERATIVE, bool _useExtrinsicGuess=false, Mat _rvec=Mat(), Mat _tvec=Mat() ) : cameraMatrix(_cameraMatrix), distCoeffs(_distCoeffs), flags(_flags), useExtrinsicGuess(_useExtrinsicGuess), rvec(_rvec), tvec(_tvec) {} @@ -142,12 +142,12 @@ bool solvePnPRansac(InputArray _opoints, InputArray _ipoints, Mat cameraMatrix = _cameraMatrix.getMat(), distCoeffs = _distCoeffs.getMat(); int model_points = 6; - int ransac_kernel_method = CV_EPNP; + int ransac_kernel_method = cv::SOLVEPNP_EPNP; if( npoints == 4 ) { model_points = 4; - ransac_kernel_method = CV_P3P; + ransac_kernel_method = cv::SOLVEPNP_P3P; } Ptr cb; // pointer to callback @@ -178,7 +178,7 @@ bool solvePnPRansac(InputArray _opoints, InputArray _ipoints, opoints_inliers.resize(npoints1); ipoints_inliers.resize(npoints1); result = solvePnP(opoints_inliers, ipoints_inliers, cameraMatrix, - distCoeffs, rvec, tvec, useExtrinsicGuess, flags == CV_P3P ? CV_EPNP : flags) ? 1 : -1; + distCoeffs, rvec, tvec, useExtrinsicGuess, flags == cv::SOLVEPNP_P3P ? cv::SOLVEPNP_EPNP : flags) ? 1 : -1; } if( result <= 0 || _local_model.rows <= 0) @@ -213,7 +213,7 @@ bool solvePnPRansac(InputArray _opoints, InputArray _ipoints, int RANSACUpdateNumIters( double p, double ep, int modelPoints, int maxIters ) { if( modelPoints <= 0 ) - CV_Error( 0, "the number of model points should be positive" ); + CV_Error( cv::Error::Code::StsBadArg, "the number of model points should be positive" ); p = MAX(p, 0.); p = MIN(p, 1.); diff --git a/corelib/src/opencv/solvepnp.h b/corelib/src/opencv/solvepnp.h index e88b5700..382f9db7 100644 --- a/corelib/src/opencv/solvepnp.h +++ b/corelib/src/opencv/solvepnp.h @@ -45,9 +45,10 @@ #define RTABMAP_CORELIB_SRC_OPENCV_SOLVEPNP_H_ #include +#if CV_MAJOR_VERSION >= 5 +#include +#else #include -#if CV_MAJOR_VERSION >= 3 -#include #endif namespace cv3 { @@ -95,7 +96,7 @@ bool solvePnPRansac( cv::InputArray objectPoints, cv::InputArray imagePoints, cv::OutputArray rvec, cv::OutputArray tvec, bool useExtrinsicGuess = false, int iterationsCount = 100, float reprojectionError = 8.0, double confidence = 0.99, - cv::OutputArray inliers = cv::noArray(), int flags = CV_ITERATIVE ); + cv::OutputArray inliers = cv::noArray(), int flags = cv::SOLVEPNP_ITERATIVE ); int RANSACUpdateNumIters( double p, double ep, int modelPoints, int maxIters ); diff --git a/corelib/src/optimizer/OptimizerCeres.cpp b/corelib/src/optimizer/OptimizerCeres.cpp index 046d401c..d2d64c3e 100644 --- a/corelib/src/optimizer/OptimizerCeres.cpp +++ b/corelib/src/optimizer/OptimizerCeres.cpp @@ -26,6 +26,12 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "rtabmap/core/Graph.h" +#if CV_MAJOR_VERSION < 5 +#include +#else +#include +#endif + #include #include #include diff --git a/corelib/src/stereo/StereoBM.cpp b/corelib/src/stereo/StereoBM.cpp index 1b390c68..57f6b7d0 100644 --- a/corelib/src/stereo/StereoBM.cpp +++ b/corelib/src/stereo/StereoBM.cpp @@ -27,9 +27,13 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include +#if CV_MAJOR_VERSION < 5 #include +#else +#include +#include +#endif #include -#include namespace rtabmap { @@ -88,7 +92,7 @@ cv::Mat StereoBM::computeDisparity( cv::Mat leftMono; if(leftImage.channels() == 3) { - cv::cvtColor(leftImage, leftMono, CV_BGR2GRAY); + cv::cvtColor(leftImage, leftMono, cv::COLOR_BGR2GRAY); } else { @@ -98,7 +102,7 @@ cv::Mat StereoBM::computeDisparity( cv::Mat rightMono; if(rightImage.channels() == 3) { - cv::cvtColor(rightImage, rightMono, CV_BGR2GRAY); + cv::cvtColor(rightImage, rightMono, cv::COLOR_BGR2GRAY); } else { diff --git a/corelib/src/stereo/StereoSGBM.cpp b/corelib/src/stereo/StereoSGBM.cpp index 6763aeb6..d8e651a5 100644 --- a/corelib/src/stereo/StereoSGBM.cpp +++ b/corelib/src/stereo/StereoSGBM.cpp @@ -27,9 +27,13 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include +#if CV_MAJOR_VERSION < 5 #include +#else +#include +#include +#endif #include -#include namespace rtabmap { @@ -77,7 +81,7 @@ cv::Mat StereoSGBM::computeDisparity( cv::Mat leftMono; if(leftImage.channels() == 3) { - cv::cvtColor(leftImage, leftMono, CV_BGR2GRAY); + cv::cvtColor(leftImage, leftMono, cv::COLOR_BGR2GRAY); } else { @@ -87,7 +91,7 @@ cv::Mat StereoSGBM::computeDisparity( cv::Mat rightMono; if(rightImage.channels() == 3) { - cv::cvtColor(rightImage, rightMono, CV_BGR2GRAY); + cv::cvtColor(rightImage, rightMono, cv::COLOR_BGR2GRAY); } else { diff --git a/corelib/src/util2d.cpp b/corelib/src/util2d.cpp index 0251d0fe..0c9318f2 100644 --- a/corelib/src/util2d.cpp +++ b/corelib/src/util2d.cpp @@ -34,11 +34,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include -#include #include #include #include -#include #include #include @@ -46,6 +44,12 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #endif +#if CV_MAJOR_VERSION < 5 +#include +#else +#include +#endif + namespace rtabmap { @@ -747,7 +751,7 @@ cv::Mat disparityFromStereoImages( cv::Mat leftMono; if(leftImage.channels() == 3) { - cv::cvtColor(leftImage, leftMono, CV_BGR2GRAY); + cv::cvtColor(leftImage, leftMono, cv::COLOR_BGR2GRAY); } else { @@ -2042,8 +2046,8 @@ cv::Mat brightnessAndContrastAuto(const cv::Mat &src, const cv::Mat & mask, floa //to calculate grayscale histogram cv::Mat gray; if (src.type() == CV_8UC1) gray = src; - else if (src.type() == CV_8UC3) cvtColor(src, gray, CV_BGR2GRAY); - else if (src.type() == CV_8UC4) cvtColor(src, gray, CV_BGRA2GRAY); + else if (src.type() == CV_8UC3) cvtColor(src, gray, cv::COLOR_BGR2GRAY); + else if (src.type() == CV_8UC4) cvtColor(src, gray, cv::COLOR_BGRA2GRAY); if (clipLowHistPercent == 0 && clipHighHistPercent == 0) { // keep full available range diff --git a/corelib/src/util3d.cpp b/corelib/src/util3d.cpp index facb0d6c..dfad02a8 100644 --- a/corelib/src/util3d.cpp +++ b/corelib/src/util3d.cpp @@ -41,7 +41,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include -#include namespace rtabmap { @@ -892,7 +891,7 @@ pcl::PointCloud::Ptr cloudFromStereoImages( cv::Mat leftMono; if(leftColor.channels() == 3) { - cv::cvtColor(leftColor, leftMono, CV_BGR2GRAY); + cv::cvtColor(leftColor, leftMono, cv::COLOR_BGR2GRAY); } else { @@ -902,7 +901,7 @@ pcl::PointCloud::Ptr cloudFromStereoImages( cv::Mat rightMono; if(rightColor.channels() == 3) { - cv::cvtColor(rightColor, rightMono, CV_BGR2GRAY); + cv::cvtColor(rightColor, rightMono, cv::COLOR_BGR2GRAY); } else { @@ -1038,7 +1037,7 @@ std::vector::Ptr> cloudsFromSensorData( cv::Mat leftMono; if(sensorData.imageRaw().channels() == 3) { - cv::cvtColor(sensorData.imageRaw(), leftMono, CV_BGR2GRAY); + cv::cvtColor(sensorData.imageRaw(), leftMono, cv::COLOR_BGR2GRAY); } else { @@ -1048,7 +1047,7 @@ std::vector::Ptr> cloudsFromSensorData( cv::Mat rightMono; if(sensorData.rightRaw().channels() == 3) { - cv::cvtColor(sensorData.rightRaw(), rightMono, CV_BGR2GRAY); + cv::cvtColor(sensorData.rightRaw(), rightMono, cv::COLOR_BGR2GRAY); } else { diff --git a/corelib/src/util3d_correspondences.cpp b/corelib/src/util3d_correspondences.cpp index 5a81869b..41cc1e65 100644 --- a/corelib/src/util3d_correspondences.cpp +++ b/corelib/src/util3d_correspondences.cpp @@ -30,7 +30,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include +#if CV_MAJOR_VERSION < 5 #include +#else +#include +#endif #include #include diff --git a/corelib/src/util3d_surface.cpp b/corelib/src/util3d_surface.cpp index bccae363..36254264 100644 --- a/corelib/src/util3d_surface.cpp +++ b/corelib/src/util3d_surface.cpp @@ -39,8 +39,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/utilite/UConversion.h" #include "rtabmap/utilite/UMath.h" #include "rtabmap/utilite/UTimer.h" -#include -#include #include #include #include @@ -1745,7 +1743,7 @@ cv::Mat mergeTextures( if(resizedImage.type() == CV_8UC1) { cv::Mat resizedImageColor; - cv::cvtColor(resizedImage, resizedImageColor, CV_GRAY2BGR); + cv::cvtColor(resizedImage, resizedImageColor, cv::COLOR_GRAY2BGR); resizedImage = resizedImageColor; } UASSERT(resizedImage.type() == globalTextures.type()); @@ -2609,7 +2607,7 @@ bool multiBandTexturing( if(imageRoi.channels() == 1) { cv::Mat imageRoiColor; - cv::cvtColor(imageRoi, imageRoiColor, CV_GRAY2BGR); + cv::cvtColor(imageRoi, imageRoiColor, cv::COLOR_GRAY2BGR); imageRoi = imageRoiColor; } @@ -3218,7 +3216,7 @@ float computeNormalsComplexity( } if(oi>1) { - cv::PCA pca_analysis(cv::Mat(data_normals, cv::Range(0, oi*2)), cv::Mat(), CV_PCA_DATA_AS_ROW); + cv::PCA pca_analysis(cv::Mat(data_normals, cv::Range(0, oi*2)), cv::Mat(), cv::PCA::DATA_AS_ROW); if(pcaEigenVectors) { @@ -3279,7 +3277,7 @@ float computeNormalsComplexity( } if(oi>1) { - cv::PCA pca_analysis(cv::Mat(data_normals, cv::Range(0, oi*2)), cv::Mat(), CV_PCA_DATA_AS_ROW); + cv::PCA pca_analysis(cv::Mat(data_normals, cv::Range(0, oi*2)), cv::Mat(), cv::PCA::DATA_AS_ROW); if(pcaEigenVectors) { @@ -3335,7 +3333,7 @@ float computeNormalsComplexity( } if(oi>1) { - cv::PCA pca_analysis(cv::Mat(data_normals, cv::Range(0, oi*2)), cv::Mat(), CV_PCA_DATA_AS_ROW); + cv::PCA pca_analysis(cv::Mat(data_normals, cv::Range(0, oi*2)), cv::Mat(), cv::PCA::DATA_AS_ROW); if(pcaEigenVectors) { @@ -3391,7 +3389,7 @@ float computeNormalsComplexity( } if(oi>1) { - cv::PCA pca_analysis(cv::Mat(data_normals, cv::Range(0, oi*2)), cv::Mat(), CV_PCA_DATA_AS_ROW); + cv::PCA pca_analysis(cv::Mat(data_normals, cv::Range(0, oi*2)), cv::Mat(), cv::PCA::DATA_AS_ROW); if(pcaEigenVectors) { @@ -3447,7 +3445,7 @@ float computeNormalsComplexity( } if(oi>1) { - cv::PCA pca_analysis(cv::Mat(data_normals, cv::Range(0, oi*2)), cv::Mat(), CV_PCA_DATA_AS_ROW); + cv::PCA pca_analysis(cv::Mat(data_normals, cv::Range(0, oi*2)), cv::Mat(), cv::PCA::DATA_AS_ROW); if(pcaEigenVectors) { diff --git a/guilib/include/rtabmap/gui/DatabaseViewer.h b/guilib/include/rtabmap/gui/DatabaseViewer.h index 07f94943..bcd6735b 100644 --- a/guilib/include/rtabmap/gui/DatabaseViewer.h +++ b/guilib/include/rtabmap/gui/DatabaseViewer.h @@ -36,7 +36,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#if CV_MAJOR_VERSION < 5 #include +#else +#include +#endif #include #include #include diff --git a/guilib/include/rtabmap/gui/ImageView.h b/guilib/include/rtabmap/gui/ImageView.h index 8e8df852..95e4ff8f 100644 --- a/guilib/include/rtabmap/gui/ImageView.h +++ b/guilib/include/rtabmap/gui/ImageView.h @@ -34,7 +34,12 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include +#if CV_MAJOR_VERSION < 5 #include +#else +#include +#endif #include #include "rtabmap/utilite/UCv2Qt.h" #include diff --git a/guilib/include/rtabmap/gui/KeypointItem.h b/guilib/include/rtabmap/gui/KeypointItem.h index 7f4f9f64..30db5c31 100644 --- a/guilib/include/rtabmap/gui/KeypointItem.h +++ b/guilib/include/rtabmap/gui/KeypointItem.h @@ -34,7 +34,12 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include +#if CV_MAJOR_VERSION < 5 #include +#else +#include +#endif namespace rtabmap { diff --git a/guilib/src/CalibrationDialog.cpp b/guilib/src/CalibrationDialog.cpp index f6b27fe7..3fd45306 100644 --- a/guilib/src/CalibrationDialog.cpp +++ b/guilib/src/CalibrationDialog.cpp @@ -28,15 +28,17 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/gui/CalibrationDialog.h" #include "ui_calibrationDialog.h" +#include #include #include -#include +#if CV_MAJOR_VERSION >= 5 +#include +#include +#else #include -#if CV_MAJOR_VERSION >= 3 -#include #endif #include -#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))) +#if (CV_MAJOR_VERSION > 2 and CV_MAJOR_VERSION < 5) or (CV_MAJOR_VERSION == 2 and (CV_MINOR_VERSION >4 or (CV_MINOR_VERSION == 4 and CV_SUBMINOR_VERSION >=10))) #include #endif @@ -276,18 +278,23 @@ void CalibrationDialog::generateBoard() if(ui_->comboBox_board_type->currentIndex() >= 1 ) { try { + const int marginInPixels = squareSizeInPixels/4; + cv::Size size( + squareSizeInPixels*ui_->spinBox_boardWidth->value() + 2*marginInPixels, + squareSizeInPixels*ui_->spinBox_boardHeight->value() + 2*marginInPixels); + UINFO("Creating board image of %dx%d pixels (%dx%d squares)", + size.width, size.height, + ui_->spinBox_boardWidth->value(), ui_->spinBox_boardHeight->value()); #if CV_MAJOR_VERSION > 4 || (CV_MAJOR_VERSION == 4 && CV_MINOR_VERSION >= 7) charucoBoard_->generateImage( - cv::Size(squareSizeInPixels*ui_->spinBox_boardWidth->value(), - squareSizeInPixels*ui_->spinBox_boardHeight->value()), + size, image, - squareSizeInPixels/4, 1); + marginInPixels, 1); #else charucoBoard_->draw( - cv::Size(squareSizeInPixels*ui_->spinBox_boardWidth->value(), - squareSizeInPixels*ui_->spinBox_boardHeight->value()), + size, image, - squareSizeInPixels/4, 1); + marginInPixels, 1); #endif int arucoDict = ui_->comboBox_marker_dictionary->currentIndex(); @@ -297,7 +304,7 @@ void CalibrationDialog::generateBoard() } catch(const cv::Exception & e) { - UERROR("%f", e.what()); + UERROR("%s", e.what()); QMessageBox::critical(this, tr("Generating Board"), tr("Cannot generate the board. Make sure the dictionary " "selected is big enough for the board size. Error:\"%1\"").arg(e.what())); @@ -737,7 +744,7 @@ void CalibrationDialog::processImages(const cv::Mat & imageLeft, const cv::Mat & cv::Size boardSize(ui_->spinBox_boardWidth->value(), ui_->spinBox_boardHeight->value()); if(!viewGray.empty()) { - int flags = CV_CALIB_CB_ADAPTIVE_THRESH | CV_CALIB_CB_NORMALIZE_IMAGE; + int flags = cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE; if(!viewGray.empty()) { @@ -748,7 +755,7 @@ void CalibrationDialog::processImages(const cv::Mat & imageLeft, const cv::Mat & if( scale == 1 ) timg = viewGray; else - cv::resize(viewGray, timg, cv::Size(), scale, scale, CV_INTER_CUBIC); + cv::resize(viewGray, timg, cv::Size(), scale, scale, cv::INTER_CUBIC); #ifdef HAVE_CHARUCO if(ui_->comboBox_board_type->currentIndex() >= 1 ) @@ -833,7 +840,7 @@ void CalibrationDialog::processImages(const cv::Mat & imageLeft, const cv::Mat & float ratio = ui_->comboBox_board_type->currentIndex() >= 1 ?6.0f:2.0f; float radius = minSquareDistance==-1.0f?5.0f:(minSquareDistance/ratio); cv::cornerSubPix( viewGray, pointBuf[id], cv::Size(radius, radius), cv::Size(-1,-1), - cv::TermCriteria( CV_TERMCRIT_EPS + CV_TERMCRIT_ITER, 30, 0.1 )); + cv::TermCriteria( cv::TermCriteria::EPS + cv::TermCriteria::MAX_ITER, 30, 0.1 )); // Filter points that drifted to far (caused by reflection or bad subpixel gradient) float threshold = ui_->doubleSpinBox_subpixel_error->value(); @@ -1388,6 +1395,13 @@ void CalibrationDialog::calibrate() UINFO("Calibrating camera %d (samples=%d)", id, (int)imagePoints_[id].size()); logStream << "Calibrating camera " << id << " (samples=" << imagePoints_[id].size() << ")" << ENDL; + // Work on local copies: the fisheye auto-prune below removes ill-conditioned views, + // and we must NOT mutate the persistent buffers, otherwise clicking Calibrate again + // would run on a smaller (already-pruned) sample set and give different results. + std::vector > objectPoints = objectPoints_[id]; + std::vector > imagePoints = imagePoints_[id]; + std::vector imageIds = imageIds_[id]; + //calibrate std::vector rvecs, tvecs; std::vector reprojErrs; @@ -1401,26 +1415,78 @@ void CalibrationDialog::calibrate() if(fishEye) { - try + // cv::fisheye::calibrate() with CALIB_CHECK_COND throws as soon as a single + // view is ill-conditioned (e.g. too few / poorly spread ChArUco corners), + // aborting the whole calibration. Auto-prune the offending view (its index is + // reported in the exception message) and retry until it succeeds, keeping the + // CHECK_COND safety without discarding every good view. + const int minFisheyeViews = COUNT_MIN/2; + bool calibrated = false; + while(!calibrated) { - rms = cv::fisheye::calibrate( - objectPoints_[id], - imagePoints_[id], - imageSize_[id], - K, - D, - rvecs, - tvecs, - cv::fisheye::CALIB_RECOMPUTE_EXTRINSIC | - cv::fisheye::CALIB_CHECK_COND | - cv::fisheye::CALIB_FIX_SKEW); - } - catch(const cv::Exception & e) - { - UERROR("Error: %s (try restarting the calibration)", e.what()); - QMessageBox::warning(this, tr("Calibration failed!"), tr("Error: %1 (try restarting the calibration)").arg(e.what())); - processingData_ = false; - return; + try + { + rms = cv::fisheye::calibrate( + objectPoints, + imagePoints, + imageSize_[id], + K, + D, + rvecs, + tvecs, + cv::fisheye::CALIB_RECOMPUTE_EXTRINSIC | + cv::fisheye::CALIB_CHECK_COND | + cv::fisheye::CALIB_FIX_SKEW); + calibrated = true; + } + catch(const cv::Exception & e) + { + // Parse the ill-conditioned view index, e.g. + // "CALIB_CHECK_COND - Ill-conditioned matrix for input array 43" + int badIndex = -1; + const QString token = "input array "; + QString msg = e.what(); + int tokenPos = msg.indexOf(token); + if(tokenPos >= 0) + { + bool ok = false; + int v = msg.mid(tokenPos + token.length()).section(' ', 0, 0).toInt(&ok); + if(ok) + { + badIndex = v; + } + } + + if(badIndex >= 0 && badIndex < (int)objectPoints.size() && + (int)objectPoints.size() > minFisheyeViews) + { + int removedImageId = badIndex < (int)imageIds.size() ? imageIds[badIndex] : -1; + UWARN("Fisheye calibration: view %d (image %d) is ill-conditioned, " + "removing it and retrying (%d views left).", + badIndex, removedImageId, (int)objectPoints.size()-1); + logStream << "Fisheye calibration: removed ill-conditioned view " << badIndex + << " (image " << removedImageId << "), " + << (int)objectPoints.size()-1 << " views left" << ENDL; + + // Prune the local copies only (never the persistent buffers). Keep them + // aligned: the per-view reprojection loop below indexes them together + // with rvecs/tvecs. + objectPoints.erase(objectPoints.begin()+badIndex); + imagePoints.erase(imagePoints.begin()+badIndex); + if(badIndex < (int)imageIds.size()) + { + imageIds.erase(imageIds.begin()+badIndex); + } + // loop and retry with the pruned set + } + else + { + UERROR("Error: %s (try restarting the calibration)", e.what()); + QMessageBox::warning(this, tr("Calibration failed!"), tr("Error: %1 (try restarting the calibration)").arg(e.what())); + processingData_ = false; + return; + } + } } } else @@ -1429,8 +1495,8 @@ void CalibrationDialog::calibrate() cv::Mat stdDevsMatInt, stdDevsMatExt; cv::Mat perViewErrorsMat; rms = cv::calibrateCamera( - objectPoints_[id], - imagePoints_[id], + objectPoints, + imagePoints, imageSize_[id], K, D, @@ -1440,14 +1506,14 @@ void CalibrationDialog::calibrate() stdDevsMatExt, perViewErrorsMat, ui_->comboBox_calib_model->currentIndex()==2?cv::CALIB_RATIONAL_MODEL:0); - if((int)imageIds_[id].size() == perViewErrorsMat.rows) + if((int)imageIds.size() == perViewErrorsMat.rows) { UINFO("Per view errors:"); logStream << "Per view errors:" << ENDL; for(int i=0; i(i,0)); - logStream << "Image " << imageIds_[id][i] << ": " << perViewErrorsMat.at(i,0) << ENDL; + UINFO("Image %d: %f", imageIds[i], perViewErrorsMat.at(i,0)); + logStream << "Image " << imageIds[i] << ": " << perViewErrorsMat.at(i,0) << ENDL; } } } @@ -1459,23 +1525,23 @@ void CalibrationDialog::calibrate() std::vector imagePoints2; int i, totalPoints = 0; double totalErr = 0, err; - reprojErrs.resize(objectPoints_[id].size()); + reprojErrs.resize(objectPoints.size()); - for( i = 0; i < (int)objectPoints_[id].size(); ++i ) + for( i = 0; i < (int)objectPoints.size(); ++i ) { #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))) if(fishEye) { - cv::fisheye::projectPoints( cv::Mat(objectPoints_[id][i]), imagePoints2, rvecs[i], tvecs[i], K, D); + cv::fisheye::projectPoints( cv::Mat(objectPoints[i]), imagePoints2, rvecs[i], tvecs[i], K, D); } else #endif { - cv::projectPoints( cv::Mat(objectPoints_[id][i]), rvecs[i], tvecs[i], K, D, imagePoints2); + cv::projectPoints( cv::Mat(objectPoints[i]), rvecs[i], tvecs[i], K, D, imagePoints2); } - err = cv::norm(cv::Mat(imagePoints_[id][i]), cv::Mat(imagePoints2), CV_L2); + err = cv::norm(cv::Mat(imagePoints[i]), cv::Mat(imagePoints2), cv::NORM_L2); - int n = (int)objectPoints_[id][i].size(); + int n = (int)objectPoints[i].size(); reprojErrs[i] = (float) std::sqrt(err*err/n); totalErr += err*err; totalPoints += n; @@ -1576,9 +1642,14 @@ void CalibrationDialog::calibrate() cv::Mat P = stereoModel_.right().P().clone(); P.at(0,3) = -P.at(0,0)*ui_->doubleSpinBox_stereoBaseline->value(); double scale = ui_->doubleSpinBox_stereoBaseline->value() / stereoModel_.baseline(); - UWARN("Scale %f (setting square size from %f to %f)", scale, ui_->doubleSpinBox_squareSize->value(), ui_->doubleSpinBox_squareSize->value()*scale); - logStream << "Baseline rescaled from " << stereoModel_.baseline() << " to " << ui_->doubleSpinBox_stereoBaseline->value() << " scale=" << scale << ENDL; - ui_->doubleSpinBox_squareSize->setValue(ui_->doubleSpinBox_squareSize->value()*scale); + UWARN("Scale %f applied to stereo baseline (computed %f m -> expected %f m). " + "If the mismatch is caused by the measured square size, it would be %f m instead of %f m.", + scale, stereoModel_.baseline(), ui_->doubleSpinBox_stereoBaseline->value(), + ui_->doubleSpinBox_squareSize->value()*scale, ui_->doubleSpinBox_squareSize->value()); + logStream << "Baseline rescaled from " << stereoModel_.baseline() << " to " << ui_->doubleSpinBox_stereoBaseline->value() + << " scale=" << scale << " (implied square size " << ui_->doubleSpinBox_squareSize->value()*scale + << " m instead of " << ui_->doubleSpinBox_squareSize->value() << " m)" << ENDL; + UASSERT(!stereoModel_.T().empty()); stereoModel_ = StereoCameraModel( stereoModel_.name(), stereoModel_.left().imageSize(),stereoModel_.left().K_raw(), stereoModel_.left().D_raw(), stereoModel_.left().R(), stereoModel_.left().P(), @@ -1708,26 +1779,58 @@ StereoCameraModel CalibrationDialog::stereoCalibration(const CameraModel & left, cv::Vec4d D_right(right.D_raw().at(0,0), right.D_raw().at(0,1), right.D_raw().at(0,4), right.D_raw().at(0,5)); UASSERT(stereoImagePoints_[0].size() == stereoImagePoints_[1].size()); + UASSERT(stereoObjectPoints_.size() == stereoImagePoints_[0].size()); + // cv::fisheye::stereoCalibrate() reads the number of points from the first view and + // lays out its Jacobian assuming EVERY view has that same count (fisheye.cpp + // "reshape(1, n_points*2)"). ChArUco detects a variable number of corners per view, + // so we make the counts uniform by evenly subsampling every view down to the common + // minimum count (kept spatially spread, not just the first N). + size_t minPoints = stereoImagePoints_[0][0].size(); + for(unsigned int i =0; i > objectPoints(stereoObjectPoints_.size()); std::vector > leftPoints(stereoImagePoints_[0].size()); std::vector > rightPoints(stereoImagePoints_[1].size()); + bool subsampled = false; for(unsigned int i =0; i1 ? (size_t)((k*(n-1))/(minPoints-1)) : 0; + objectPoints[i][k].x = stereoObjectPoints_[i][j].x; + objectPoints[i][k].y = stereoObjectPoints_[i][j].y; + objectPoints[i][k].z = stereoObjectPoints_[i][j].z; + leftPoints[i][k].x = stereoImagePoints_[0][i][j].x; + leftPoints[i][k].y = stereoImagePoints_[0][i][j].y; + rightPoints[i][k].x = stereoImagePoints_[1][i][j].x; + rightPoints[i][k].y = stereoImagePoints_[1][i][j].y; + } + } + if(subsampled) + { + UWARN("Fisheye stereo calibration requires the same number of points in every " + "view; sub-sampled all %d views to the common minimum of %d points.", + (int)stereoImagePoints_[0].size(), (int)minPoints); + if(logStream) (*logStream) << "Fisheye stereo: sub-sampled all views to " << (int)minPoints << " points" << ENDL; } try { rms = cv::fisheye::stereoCalibrate( - stereoObjectPoints_, + objectPoints, leftPoints, rightPoints, left.K_raw(), D_left, right.K_raw(), D_right, @@ -1739,30 +1842,41 @@ StereoCameraModel CalibrationDialog::stereoCalibration(const CameraModel & left, catch(const cv::Exception & e) { UERROR("Error: %s (try restarting the calibration)", e.what()); + QMessageBox::warning(const_cast(this), tr("Calibration failed!"), tr("Error: %1 (try restarting the calibration)").arg(e.what())); return output; } std::cout << "R = " << R << std::endl; std::cout << "T = " << Tvec << std::endl; + // cv::fisheye::stereoCalibrate() returns the translation as a Vec3d (Tvec) and does + // not fill the cv::Mat T; populate it here so the returned model always carries a + // valid 3x1 extrinsic translation (used e.g. by the baseline rescaling below). + T = cv::Mat(3, 1, CV_64FC1); + T.at(0,0) = Tvec[0]; + T.at(1,0) = Tvec[1]; + T.at(2,0) = Tvec[2]; + if(imageSize_[0] == imageSize_[1] && !ignoreStereoRectification) { UINFO("Compute stereo rectification"); cv::Mat R1, R2, P1, P2, Q; +#if CV_MAJOR_VERSION < 5 stereoRectifyFisheye( left.K_raw(), D_left, right.K_raw(), D_right, imageSize, R, Tvec, R1, R2, P1, P2, Q, cv::CALIB_ZERO_DISPARITY, 0, imageSize); - - // Very hard to get good results with this one: - /*double balance = 0.0, fov_scale = 1.0; +#else + // Very hard to get good results with this one, however we cannot use the previous one anymore in opencv5 + double balance = 0.0, fov_scale = 1.0; cv::fisheye::stereoRectify( left.K_raw(), D_left, right.K_raw(), D_right, imageSize, R, Tvec, R1, R2, P1, P2, Q, - cv::CALIB_ZERO_DISPARITY, imageSize, balance, fov_scale);*/ + cv::CALIB_ZERO_DISPARITY, imageSize, balance, fov_scale); +#endif std::cout << "R1 = " << R1 << std::endl; std::cout << "R2 = " << R2 << std::endl; @@ -1791,11 +1905,6 @@ StereoCameraModel CalibrationDialog::stereoCalibration(const CameraModel & left, std::cout << "P1n = " << P1 << std::endl; std::cout << "P2n = " << P2 << std::endl; - - cv::Mat T(3,1,CV_64FC1); - T.at (0,0) = Tvec[0]; - T.at (1,0) = Tvec[1]; - T.at (2,0) = Tvec[2]; output = StereoCameraModel( cameraName_.toStdString(), imageSize_[0], left.K_raw(), left.D_raw(), R1, P1, @@ -1915,8 +2024,8 @@ StereoCameraModel CalibrationDialog::stereoCalibration(const CameraModel & left, { int npt = (int)stereoImagePoints_[0][i].size(); - cv::Mat imgpt0 = cv::Mat(stereoImagePoints_[0][i]); - cv::Mat imgpt1 = cv::Mat(stereoImagePoints_[1][i]); + std::vector imgpt0 = stereoImagePoints_[0][i]; + std::vector imgpt1 = stereoImagePoints_[1][i]; cv::undistortPoints(imgpt0, imgpt0, left.K_raw(), left.D_raw(), R1, P1); cv::undistortPoints(imgpt1, imgpt1, right.K_raw(), right.D_raw(), R2, P2); computeCorrespondEpilines(imgpt0, 1, F, lines[0]); @@ -1925,10 +2034,10 @@ StereoCameraModel CalibrationDialog::stereoCalibration(const CameraModel & left, double sampleErr = 0.0; for(int j = 0; j < npt; j++ ) { - double errij = fabs(stereoImagePoints_[0][i][j].x*lines[1][j][0] + - stereoImagePoints_[0][i][j].y*lines[1][j][1] + lines[1][j][2]) + - fabs(stereoImagePoints_[1][i][j].x*lines[0][j][0] + - stereoImagePoints_[1][i][j].y*lines[0][j][1] + lines[0][j][2]); + double errij = fabs(imgpt0[j].x*lines[1][j][0] + + imgpt0[j].y*lines[1][j][1] + lines[1][j][2]) + + fabs(imgpt1[j].x*lines[0][j][0] + + imgpt1[j].y*lines[0][j][1] + lines[0][j][2]); sampleErr += errij; } UINFO("Stereo image %d: %f", stereoImageIds_[i], sampleErr/npt); diff --git a/guilib/src/CameraViewer.cpp b/guilib/src/CameraViewer.cpp index 6588d51b..3d7bbabe 100644 --- a/guilib/src/CameraViewer.cpp +++ b/guilib/src/CameraViewer.cpp @@ -85,7 +85,7 @@ CameraViewer::CameraViewer(QWidget * parent, const ParametersMap & parameters) : showScanCheckbox_->setChecked(true); markerCheckbox_ = new QCheckBox("Detect markers", this); -#if defined(HAVE_OPENCV_ARUCO) || defined(RTABMAP_APRILTAG) +#if ((CV_MAJOR_VERSION > 4 || (CV_MAJOR_VERSION==4 && CV_MINOR_VERSION >=7)) && defined(HAVE_OPENCV_OBJDETECT)) || defined(HAVE_OPENCV_ARUCO) || defined(RTABMAP_APRILTAG) markerCheckbox_->setEnabled(true); markerDetector_ = new MarkerDetector(parameters); #else @@ -175,7 +175,8 @@ void CameraViewer::showImage(const rtabmap::SensorData & data) if(!models.empty() && models[0].isValidForProjection()) { cv::Mat imageWithDetections; - detections = markerDetector_->detect(left, models, depthOrRight, _landmarksSize, &imageWithDetections); + cv::Mat depth = (depthOrRight.type()==CV_16UC1 || depthOrRight.type()==CV_32FC1) ? depthOrRight : cv::Mat(); + detections = markerDetector_->detect(left, models, depth, _landmarksSize, &imageWithDetections); imageView_->setImage(uCvMat2QImage(imageWithDetections)); for(std::map::iterator iter=detections.begin(); iter!=detections.end(); ++iter) { diff --git a/guilib/src/DatabaseViewer.cpp b/guilib/src/DatabaseViewer.cpp index 5ef206f6..63ec3694 100644 --- a/guilib/src/DatabaseViewer.cpp +++ b/guilib/src/DatabaseViewer.cpp @@ -46,8 +46,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include -#include -#include #include #include #include @@ -5980,7 +5978,7 @@ void DatabaseViewer::updateStereo(const SensorData * data) cv::Mat leftMono; if(data->imageRaw().channels() == 3) { - cv::cvtColor(data->imageRaw(), leftMono, CV_BGR2GRAY); + cv::cvtColor(data->imageRaw(), leftMono, cv::COLOR_BGR2GRAY); } else { @@ -5989,7 +5987,7 @@ void DatabaseViewer::updateStereo(const SensorData * data) cv::Mat rightMono; if(data->rightRaw().channels() == 3) { - cv::cvtColor(data->rightRaw(), rightMono, CV_BGR2GRAY); + cv::cvtColor(data->rightRaw(), rightMono, cv::COLOR_BGR2GRAY); } else { diff --git a/guilib/src/GuiUtil.h b/guilib/src/GuiUtil.h new file mode 100644 index 00000000..6b128261 --- /dev/null +++ b/guilib/src/GuiUtil.h @@ -0,0 +1,59 @@ +/* +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 RTABMAP_GUILIB_SRC_GUIUTIL_H_ +#define RTABMAP_GUILIB_SRC_GUIUTIL_H_ + +#include +#include +#include +#include +#include + +namespace rtabmap { + +// Show a widget and block until it is actually painted on screen. show() only +// schedules window mapping + painting +inline void showAndWaitExposed(QWidget * widget) +{ + widget->show(); + if(widget->windowHandle()) + { + QElapsedTimer timer; + timer.start(); + while(!widget->windowHandle()->isExposed() && timer.elapsed() < 2000) + { + QApplication::processEvents(QEventLoop::ExcludeUserInputEvents | QEventLoop::WaitForMoreEvents, 50); + } + } + QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); + widget->repaint(); // synchronous, unlike update() +} + +} // namespace rtabmap + +#endif /* RTABMAP_GUILIB_SRC_GUIUTIL_H_ */ diff --git a/guilib/src/MainWindow.cpp b/guilib/src/MainWindow.cpp index 10b94fda..ef408832 100644 --- a/guilib/src/MainWindow.cpp +++ b/guilib/src/MainWindow.cpp @@ -28,6 +28,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/gui/MainWindow.h" #include "ui_mainWindow.h" +#include "GuiUtil.h" #include "rtabmap/core/CameraRGB.h" #include "rtabmap/core/CameraStereo.h" @@ -93,6 +94,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include +#if CV_MAJOR_VERSION >= 5 +#include +#endif + //RGB-D stuff #include "rtabmap/core/CameraRGBD.h" #include "rtabmap/core/Odometry.h" @@ -125,10 +130,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #endif -#ifdef HAVE_OPENCV_ARUCO -#include -#endif - #define LOG_FILE_NAME "LogRtabmap.txt" #define SHARE_SHOW_LOG_FILE "share/rtabmap/showlogs.m" #define SHARE_GET_PRECISION_RECALL_FILE "share/rtabmap/getPrecisionRecall.m" @@ -5959,9 +5960,7 @@ void MainWindow::startDetection() progress.setCancelButton(0); progress.setMinimumDuration(0); progress.setValue(0); - progress.show(); - QApplication::processEvents(); - QApplication::processEvents(); // make sure it is drawn + showAndWaitExposed(&progress); if(_preferencesDialog->getLidarSourceDriver() != PreferencesDialog::kSrcUndef) { @@ -6275,9 +6274,7 @@ void MainWindow::stopDetection() progress.setCancelButton(0); progress.setMinimumDuration(0); progress.setValue(0); - progress.show(); - QApplication::processEvents(); - QApplication::processEvents(); // make sure it is drawn + showAndWaitExposed(&progress); } // kill the processes diff --git a/guilib/src/PreferencesDialog.cpp b/guilib/src/PreferencesDialog.cpp index 3be2f4d0..babed150 100644 --- a/guilib/src/PreferencesDialog.cpp +++ b/guilib/src/PreferencesDialog.cpp @@ -61,6 +61,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include +#include +#include #include #include #include @@ -70,6 +73,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include "ui_preferencesDialog.h" +#include "GuiUtil.h" #include "rtabmap/core/Version.h" #include "rtabmap/core/Parameters.h" @@ -506,10 +510,10 @@ PreferencesDialog::PreferencesDialog(QWidget * parent) : _ui->checkBox_showOdomFrustums->setChecked(false); #endif -#if !defined(HAVE_OPENCV_ARUCO) && !defined(RTABMAP_APRILTAG) +#if !((CV_MAJOR_VERSION > 4 || (CV_MAJOR_VERSION==4 && CV_MINOR_VERSION >=7)) && defined(HAVE_OPENCV_OBJDETECT)) && !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 +#if !(((CV_MAJOR_VERSION > 4 || (CV_MAJOR_VERSION==4 && CV_MINOR_VERSION >=7)) && defined(HAVE_OPENCV_OBJDETECT)) || defined(HAVE_OPENCV_ARUCO)) _ui->MarkerStrategy->setItemData(0, 0, Qt::UserRole - 1); #endif #ifndef RTABMAP_APRILTAG @@ -2639,19 +2643,19 @@ void PreferencesDialog::restoreConfigOwnership(const QString & filePath) gid_t gid = (gid_t)atoi(sudoGid); // Restore the config file and its containing directory so the user // can still write preferences without sudo afterwards. - if(!filePath.isEmpty() && QFile::exists(filePath)) + auto restoreOwnership = [uid, gid](const QString & path) { - if(chown(filePath.toStdString().c_str(), uid, gid) != 0) + if(!path.isEmpty() && QFile::exists(path)) { - UWARN("Could not restore ownership of \"%s\" to uid=%d (%s).", - filePath.toStdString().c_str(), (int)uid, strerror(errno)); + if(chown(path.toStdString().c_str(), uid, gid) != 0) + { + UWARN("Could not restore ownership of \"%s\" to uid=%d (%s).", + path.toStdString().c_str(), (int)uid, strerror(errno)); + } } - } - QString dir = QFileInfo(filePath).absolutePath(); - if(!dir.isEmpty()) - { - chown(dir.toStdString().c_str(), uid, gid); - } + }; + restoreOwnership(filePath); + restoreOwnership(QFileInfo(filePath).absolutePath()); } } #else @@ -7846,9 +7850,7 @@ void PreferencesDialog::testOdometry() progress.setCancelButton(0); progress.setMinimumDuration(0); progress.setValue(0); - progress.show(); - QApplication::processEvents(); - QApplication::processEvents(); // make sure it is drawn + showAndWaitExposed(&progress); Camera * camera = this->createCamera(); progress.hide(); @@ -7989,9 +7991,7 @@ void PreferencesDialog::testOdometry() // at function scope end (not in join()), so 'progress' stays visible across it. On Windows // the first 2-3 RealSense closes per launch stall ~20s in the Motion Module stop(). progress.setLabelText(tr("Closing camera...")); - progress.show(); - QApplication::processEvents(); - QApplication::processEvents(); // make sure it is drawn + showAndWaitExposed(&progress); cameraThread.join(true); odomThread.join(true); @@ -8033,9 +8033,7 @@ void PreferencesDialog::testCamera() progress.setCancelButton(0); progress.setMinimumDuration(0); progress.setValue(0); - progress.show(); - QApplication::processEvents(); - QApplication::processEvents(); // make sure it is drawn + showAndWaitExposed(&progress); // createCamera() init()s the device on the GUI thread (required by ZED) and takes a few seconds. Camera * camera = this->createCamera(); @@ -8092,9 +8090,7 @@ void PreferencesDialog::testCamera() // stays visible across it. On Windows the first 2-3 RealSense closes per launch stall // ~20s in the Motion Module stop() (librealsense warm-up); this keeps the user informed. progress.setLabelText(tr("Closing camera...")); - progress.show(); - QApplication::processEvents(); - QApplication::processEvents(); // make sure it is drawn + showAndWaitExposed(&progress); cameraThread.join(true); // cameraThread's destructor (scope end) closes the device // deleteLater() (not delete): defer destruction to the event loop so Qt finishes // tearing down the OpenGL widget's context and window-proc subclass and drains @@ -8582,9 +8578,7 @@ void PreferencesDialog::testLidar() progress.setCancelButton(0); progress.setMinimumDuration(0); progress.setValue(0); - progress.show(); - QApplication::processEvents(); - QApplication::processEvents(); // make sure it is drawn + showAndWaitExposed(&progress); Lidar * lidar = this->createLidar(); progress.hide(); @@ -8613,9 +8607,7 @@ void PreferencesDialog::testLidar() // destructor at scope end (not in join()), so 'progress' - declared in the outer // scope - stays visible across it. progress.setLabelText(tr("Closing sensor...")); - progress.show(); - QApplication::processEvents(); - QApplication::processEvents(); // make sure it is drawn + showAndWaitExposed(&progress); lidarThread.join(true); // lidarThread's destructor (scope end) closes the device // deleteLater() (not delete): see testCamera() - avoids a dangling OpenGL platform // window that crashes in QWindowsWindow::alertWindow when Preferences later closes. diff --git a/guilib/src/ui/calibrationDialog.ui b/guilib/src/ui/calibrationDialog.ui index d4292528..96a5d27a 100644 --- a/guilib/src/ui/calibrationDialog.ui +++ b/guilib/src/ui/calibrationDialog.ui @@ -345,10 +345,10 @@ - + Number of inner squares on the board - Square Size + Board Size @@ -385,10 +385,10 @@ - Number of inner squares on the board + - Board Size + Square Size diff --git a/tools/Camera/main.cpp b/tools/Camera/main.cpp index e3370304..3bf320c7 100644 --- a/tools/Camera/main.cpp +++ b/tools/Camera/main.cpp @@ -32,7 +32,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/utilite/UDirectory.h" #include "rtabmap/utilite/UConversion.h" #include -#include #include void showUsage() @@ -178,7 +177,7 @@ int main(int argc, char * argv[]) cv::Mat rgb; rgb = camera->takeImage().imageRaw(); - cv::namedWindow("Video", CV_WINDOW_AUTOSIZE); // create window + cv::namedWindow("Video", cv::WINDOW_AUTOSIZE); // create window while(!rgb.empty()) { cv::imshow("Video", rgb); // show frame diff --git a/tools/CameraRGBD/main.cpp b/tools/CameraRGBD/main.cpp index 1d701b39..9f36bc43 100644 --- a/tools/CameraRGBD/main.cpp +++ b/tools/CameraRGBD/main.cpp @@ -40,10 +40,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap/utilite/UEventsManager.h" #include #include -#include -#if CV_MAJOR_VERSION >= 3 -#include -#endif #include #include #include @@ -470,7 +466,7 @@ int main(int argc, char * argv[]) { if(right.channels() == 3) { - cv::cvtColor(right, right, CV_BGR2GRAY); + cv::cvtColor(right, right, cv::COLOR_BGR2GRAY); } pcl::PointCloud::Ptr cloud = rtabmap::util3d::cloudFromStereoImages( rgb, right, diff --git a/tools/EpipolarGeometry/main.cpp b/tools/EpipolarGeometry/main.cpp index aa309fa9..22e60163 100644 --- a/tools/EpipolarGeometry/main.cpp +++ b/tools/EpipolarGeometry/main.cpp @@ -26,7 +26,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include -#include #include #include #include @@ -36,7 +35,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#if CV_MAJOR_VERSION < 5 #include +#else +#include +#endif #include "rtabmap/core/Features2d.h" #include "rtabmap/core/EpipolarGeometry.h" #include "rtabmap/core/VWDictionary.h" diff --git a/tools/StereoEval/main.cpp b/tools/StereoEval/main.cpp index bd3313f8..9c15e34b 100644 --- a/tools/StereoEval/main.cpp +++ b/tools/StereoEval/main.cpp @@ -37,7 +37,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include -#include #include #include @@ -222,7 +221,7 @@ int main(int argc, char * argv[]) cv::Mat leftMono; if(left.channels() == 3) { - cv::cvtColor(left, leftMono, CV_BGR2GRAY); + cv::cvtColor(left, leftMono, cv::COLOR_BGR2GRAY); } else { @@ -231,7 +230,7 @@ int main(int argc, char * argv[]) cv::Mat rightMono; if(right.channels() == 3) { - cv::cvtColor(right, rightMono, CV_BGR2GRAY); + cv::cvtColor(right, rightMono, cv::COLOR_BGR2GRAY); } else { @@ -266,7 +265,7 @@ int main(int argc, char * argv[]) cv::cornerSubPix(leftMono, leftCorners, cv::Size( subPixWinSize, subPixWinSize ), cv::Size( -1, -1 ), - cv::TermCriteria( CV_TERMCRIT_ITER | CV_TERMCRIT_EPS, subPixIterations, subPixEps ) ); + cv::TermCriteria( cv::TermCriteria::MAX_ITER | cv::TermCriteria::EPS, subPixIterations, subPixEps ) ); UDEBUG("cv::cornerSubPix() end"); } From bef4eaf6794cd58648475b316238373ec298001d Mon Sep 17 00:00:00 2001 From: Pierre ELIS Date: Sat, 1 Aug 2026 17:33:53 +0200 Subject: [PATCH 14/51] Multi component view (#1729) * Multi component view implemented except for rotations. Rotations can cause segfaults. * Implemented: handling rotations and applied rotations on optimized poses. Addition of a notion of main component (for saves in db) abd a node selection tool. * New feature: merge component using selected nodes * Refactored with suggestions. * Fixing QSet error on older Qt versions --------- Co-authored-by: matlabbe --- guilib/include/rtabmap/gui/DatabaseViewer.h | 42 +- guilib/include/rtabmap/gui/ExportDialog.h | 2 + guilib/include/rtabmap/gui/GraphViewer.h | 9 + .../include/rtabmap/gui/LinkRefiningDialog.h | 4 + guilib/src/DatabaseViewer.cpp | 896 ++++++++++++++++-- guilib/src/ExportDialog.cpp | 17 + guilib/src/GraphViewer.cpp | 72 +- guilib/src/LinkRefiningDialog.cpp | 24 + guilib/src/ui/DatabaseViewer.ui | 507 ++++++---- guilib/src/ui/exportDialog.ui | 339 +++---- guilib/src/ui/linkRefiningDialog.ui | 54 +- 11 files changed, 1525 insertions(+), 441 deletions(-) diff --git a/guilib/include/rtabmap/gui/DatabaseViewer.h b/guilib/include/rtabmap/gui/DatabaseViewer.h index bcd6735b..e6e97f62 100644 --- a/guilib/include/rtabmap/gui/DatabaseViewer.h +++ b/guilib/include/rtabmap/gui/DatabaseViewer.h @@ -33,6 +33,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include #include #include #include @@ -71,6 +72,24 @@ class EditMapArea; class LinkRefiningDialog; class Registration; class RegistrationIcp; +class ExportDialog; + +class GraphComponent +{ +public: + GraphComponent() : id(0), rootId(0), firstNodeId(0), lastNodeId(0), rotationValue(0), viewScale(-1.0f), optimizationFlavor(0) {} + + int id; + int rootId; + int firstNodeId; + int lastNodeId; + int rotationValue; + QPointF rootCoordinates; + float viewScale; + std::set nodeIds; + std::set selectedNodeIds; + int optimizationFlavor; +}; class RTABMAP_GUI_EXPORT DatabaseViewer : public QMainWindow { @@ -85,6 +104,10 @@ public: protected: virtual void showEvent(QShowEvent* anEvent); + +private: + void backupCurrentComponent(); + virtual void moveEvent(QMoveEvent* anEvent); virtual void resizeEvent(QResizeEvent* anEvent); virtual void keyPressEvent(QKeyEvent *event); @@ -92,6 +115,7 @@ protected: virtual bool eventFilter(QObject *obj, QEvent *event); private Q_SLOTS: + void updateGraphInteractionMode(); void writeSettings(); void restoreDefaultSettings(); void configModified(); @@ -131,6 +155,7 @@ private Q_SLOTS: void view3DMap(); void generate3DMap(); void detectMoreLoopClosures(); + void mergeComponents(); void updateAllNeighborCovariances(); void updateAllLoopClosureCovariances(); void updateAllLandmarkCovariances(); @@ -138,6 +163,7 @@ private Q_SLOTS: void resetAllChanges(); void graphNodeSelected(int); void graphLinkSelected(int, int); + void zoomToNode(); void sliderAValueChanged(int); void sliderBValueChanged(int); void sliderAMoved(int); @@ -146,6 +172,7 @@ private Q_SLOTS: void sliderNeighborValueChanged(int); void sliderLoopValueChanged(int); void sliderIterationsValueChanged(int); + void updateGraphViewRootId(); void editConstraint(); void updateGrid(); void updateOctomapView(); @@ -161,6 +188,12 @@ private Q_SLOTS: void notifyParametersChanged(const QStringList &); void setupMainLayout(bool vertical); void updateConstraintButtons(); + void optimizationIgnoredStateChanged(); + void fitInViewClicked(); + void tabBarComponentsValueChanged(int); + void regenerateGraphComponents(); + void updateComponentSelectedNodes(); + void clearAllSelectedNodes(); private: QString getIniFilePath() const; @@ -175,7 +208,7 @@ private: QLabel * label, QLabel * stamp, rtabmap::ImageView * view, - QLabel * labelId, + QToolButton * buttonNodeId, QLabel * labelMapId, QLabel * labelPose, QLabel * labelOptPose, @@ -205,7 +238,8 @@ private: std::multimap updateLinksWithModifications( const std::multimap & edgeConstraints); void updateNeighborsSlider(int from = 0, int to = 0); - void updateLoopClosuresSlider(int from = 0, int to = 0); + void updateLoopClosuresSlider(int from, int to); + void updateLoopClosuresSlider(); void updateCovariances(const QList & links); void refineLinks(const QList & links); void refineConstraint(int from, int to, Registration * reg, RegistrationIcp * regIcp, bool silent); @@ -234,6 +268,9 @@ private: std::string databaseFileName_; std::list > graphes_; std::multimap graphLinks_; + std::vector components_; + int activeComponentIndex_; + int lastValidNodeId_; std::map odomPoses_; std::map groundTruthPoses_; std::map gpsPoses_; @@ -253,6 +290,7 @@ private: QDialog * editMapDialog_; EditMapArea * editMapArea_; LinkRefiningDialog * linkRefiningDialog_; + ExportDialog * exportDataDialog_; bool savedMaximized_; bool firstCall_; diff --git a/guilib/include/rtabmap/gui/ExportDialog.h b/guilib/include/rtabmap/gui/ExportDialog.h index 2db6f992..28a036af 100644 --- a/guilib/include/rtabmap/gui/ExportDialog.h +++ b/guilib/include/rtabmap/gui/ExportDialog.h @@ -53,6 +53,8 @@ public: int framesIgnored() const; double targetFramerate() const; int sessionExported() const; + bool currentComponentOnly() const; + bool selectedNodesOnly() const; bool isRgbExported() const; bool isDepthExported() const; bool isDepth2dExported() const; diff --git a/guilib/include/rtabmap/gui/GraphViewer.h b/guilib/include/rtabmap/gui/GraphViewer.h index 75d35135..8ce6746f 100644 --- a/guilib/include/rtabmap/gui/GraphViewer.h +++ b/guilib/include/rtabmap/gui/GraphViewer.h @@ -57,6 +57,7 @@ class RTABMAP_GUI_EXPORT GraphViewer : public QGraphicsView { public: enum ViewPlane {XY, XZ, YZ}; + enum InteractionMode {HandMode, SelectionMode}; public: GraphViewer(QWidget * parent = 0); @@ -172,14 +173,21 @@ public: void setOdomCacheOverlayVisible(bool visible); void setOrientationENU(bool enabled); void setViewPlane(ViewPlane plane); + void setInteractionMode(InteractionMode mode); + InteractionMode getInteractionMode() const {return _interactionMode;} + std::set getSelectedNodeIds(); + void selectNodesFromIds(const std::set & ids); void setEnsureFrameVisible(bool visible); void setFastZoomMinNodes(int value); + QPointF getNodeScenePosition(int id) const; + void centerOnNode(int id); Q_SIGNALS: void configChanged(); void mapShownRequested(); void nodeSelected(int); void linkSelected(int, int); + void nodesSelected(); // Nodes selected by rubber band, call getSelectedNodeIds() to get ids selected public Q_SLOTS: void restoreDefaults(); @@ -253,6 +261,7 @@ private: bool _orientationENU; bool _mouseTracking; ViewPlane _viewPlane; + InteractionMode _interactionMode; bool _ensureFrameVisible; QPoint _previousMousePos; QPoint _initialMousePos; diff --git a/guilib/include/rtabmap/gui/LinkRefiningDialog.h b/guilib/include/rtabmap/gui/LinkRefiningDialog.h index 67afc4dc..548ce702 100644 --- a/guilib/include/rtabmap/gui/LinkRefiningDialog.h +++ b/guilib/include/rtabmap/gui/LinkRefiningDialog.h @@ -52,9 +52,13 @@ public: int nodeIdMax, int mapIdMin, int mapIdMax); + + void setRangesEnabled(bool selectedNodes, bool currentComponent); Link::Type getLinkType() const; void getIntraInterSessions(bool & intra, bool & inter) const; + bool isRangeByCurrentComponent() const; + bool isRangeBySelectedNodeIds() const; bool isRangeByNodeId() const; bool isRangeByMapId() const; void getRangeNodeId(int & from, int & to) const; diff --git a/guilib/src/DatabaseViewer.cpp b/guilib/src/DatabaseViewer.cpp index 63ec3694..b1475d3e 100644 --- a/guilib/src/DatabaseViewer.cpp +++ b/guilib/src/DatabaseViewer.cpp @@ -110,11 +110,14 @@ namespace rtabmap { DatabaseViewer::DatabaseViewer(const QString & ini, QWidget * parent) : QMainWindow(parent), dbDriver_(0), + activeComponentIndex_(0), + lastValidNodeId_(0), octomap_(0), exportDialog_(new ExportCloudsDialog(this)), editDepthDialog_(new QDialog(this)), editMapDialog_(new QDialog(this)), linkRefiningDialog_(new LinkRefiningDialog(this)), + exportDataDialog_(new ExportDialog(this)), savedMaximized_(false), firstCall_(true), iniFilePath_(ini), @@ -243,8 +246,10 @@ DatabaseViewer::DatabaseViewer(const QString & ini, QWidget * parent) : parameters.insert(*Parameters::getDefaultParameters().find(Parameters::kMemSTMSize())); ui_->parameters_toolbox->setupUi(parameters); exportDialog_->setObjectName("ExportCloudsDialog"); + exportDataDialog_->setObjectName("ExportDataDialog"); restoreDefaultSettings(); this->readSettings(); + updateGraphInteractionMode(); setupMainLayout(ui_->actionVertical_Layout->isChecked()); ui_->comboBox_octomap_rendering_type->setVisible(ui_->checkBox_octomap->isChecked()); @@ -272,6 +277,9 @@ DatabaseViewer::DatabaseViewer(const QString & ini, QWidget * parent) : connect(ui_->graphViewer, SIGNAL(nodeSelected(int)), this , SLOT(graphNodeSelected(int))); connect(ui_->graphViewer, SIGNAL(linkSelected(int,int)), this , SLOT(graphLinkSelected(int,int))); + connect(ui_->graphViewer, SIGNAL(nodesSelected()), this , SLOT(updateComponentSelectedNodes())); + connect(ui_->radioButton_graphHand, SIGNAL(toggled(bool)), this, SLOT(updateGraphInteractionMode())); + connect(ui_->radioButton_graphSelection, SIGNAL(toggled(bool)), this, SLOT(updateGraphInteractionMode())); connect(ui_->parameters_toolbox, SIGNAL(parametersChanged(const QStringList &)), this, SLOT(notifyParametersChanged(const QStringList &))); @@ -311,12 +319,14 @@ DatabaseViewer::DatabaseViewer(const QString & ini, QWidget * parent) : connect(ui_->actionView_3D_map, SIGNAL(triggered()), this, SLOT(view3DMap())); connect(ui_->actionGenerate_3D_map_pcd, SIGNAL(triggered()), this, SLOT(generate3DMap())); connect(ui_->actionDetect_more_loop_closures, SIGNAL(triggered()), this, SLOT(detectMoreLoopClosures())); + connect(ui_->actionMerge_components_using_selected_nodes, SIGNAL(triggered()), this, SLOT(mergeComponents())); connect(ui_->actionUpdate_all_neighbor_covariances, SIGNAL(triggered()), this, SLOT(updateAllNeighborCovariances())); connect(ui_->actionUpdate_all_loop_closure_covariances, SIGNAL(triggered()), this, SLOT(updateAllLoopClosureCovariances())); connect(ui_->actionUpdate_all_landmark_covariances, SIGNAL(triggered()), this, SLOT(updateAllLandmarkCovariances())); connect(ui_->actionRefine_links, SIGNAL(triggered()), this, SLOT(refineLinks())); connect(ui_->actionRegenerate_local_grid_maps, SIGNAL(triggered()), this, SLOT(regenerateLocalMaps())); connect(ui_->actionRegenerate_local_grid_maps_selected, SIGNAL(triggered()), this, SLOT(regenerateCurrentLocalMaps())); + connect(ui_->actionClear_all_selected_nodes, SIGNAL(triggered()), this, SLOT(clearAllSelectedNodes())); connect(ui_->actionReset_all_changes, SIGNAL(triggered()), this, SLOT(resetAllChanges())); connect(ui_->actionRestore_default_GUI_settings, SIGNAL(triggered()), this, SLOT(restoreDefaultSettings())); @@ -357,6 +367,8 @@ DatabaseViewer::DatabaseViewer(const QString & ini, QWidget * parent) : ui_->spinBox_indexB->setEnabled(false); connect(ui_->spinBox_indexA, SIGNAL(valueChanged(int)), ui_->horizontalSlider_A, SLOT(setValue(int))); connect(ui_->spinBox_indexB, SIGNAL(valueChanged(int)), ui_->horizontalSlider_B, SLOT(setValue(int))); + connect(ui_->button_nodeIdA, SIGNAL(clicked(bool)), this, SLOT(zoomToNode())); + connect(ui_->button_nodeIdB, SIGNAL(clicked(bool)), this, SLOT(zoomToNode())); connect(ui_->toolButton_edit_priorA, SIGNAL(clicked(bool)), this, SLOT(editConstraint())); connect(ui_->toolButton_edit_priorB, SIGNAL(clicked(bool)), this, SLOT(editConstraint())); @@ -395,13 +407,15 @@ DatabaseViewer::DatabaseViewer(const QString & ini, QWidget * parent) : ui_->checkBox_showOptimized->setEnabled(false); connect(ui_->toolButton_constraint, SIGNAL(clicked(bool)), this, SLOT(editConstraint())); connect(ui_->checkBox_enableForAll, SIGNAL(stateChanged(int)), this, SLOT(updateConstraintButtons())); + connect(ui_->checkBox_optimize_on_all_changes, SIGNAL(stateChanged(int)), this, SLOT(optimizationIgnoredStateChanged())); 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_->spinBox_optimizationsFrom, SIGNAL(editingFinished()), this, SLOT(updateGraphView())); + connect(ui_->spinBox_optimizationsFrom, SIGNAL(editingFinished()), this, SLOT(updateGraphViewRootId())); connect(ui_->comboBox_optimizationFlavor, SIGNAL(activated(int)), this, SLOT(updateGraphView())); + connect(ui_->tabBar_components, SIGNAL(currentChanged(int)), this, SLOT(tabBarComponentsValueChanged(int))); connect(ui_->checkBox_spanAllMaps, SIGNAL(stateChanged(int)), this, SLOT(updateGraphView())); connect(ui_->checkBox_wmState, SIGNAL(stateChanged(int)), this, SLOT(updateGraphView())); connect(ui_->graphViewer, SIGNAL(mapShownRequested()), this, SLOT(updateGraphView())); @@ -471,6 +485,8 @@ DatabaseViewer::DatabaseViewer(const QString & ini, QWidget * parent) : connect(ui_->doubleSpinBox_posefilteringAngle, SIGNAL(valueChanged(double)), this, SLOT(configModified())); connect(ui_->horizontalSlider_rotation, SIGNAL(valueChanged(int)), this, SLOT(updateGraphRotation())); connect(ui_->pushButton_applyRotation, SIGNAL(clicked()), this, SLOT(updateGraphView())); + connect(ui_->checkBox_fit_in_view, SIGNAL(stateChanged(int)), this, SLOT(configModified())); + connect(ui_->checkBox_fit_in_view, SIGNAL(stateChanged(int)), this, SLOT(fitInViewClicked())); connect(ui_->spinBox_icp_decimation, SIGNAL(valueChanged(int)), this, SLOT(configModified())); connect(ui_->doubleSpinBox_icp_maxDepth, SIGNAL(valueChanged(double)), this, SLOT(configModified())); @@ -503,6 +519,7 @@ DatabaseViewer::DatabaseViewer(const QString & ini, QWidget * parent) : connect(ui_->checkBox_grid_showProbMap, SIGNAL(stateChanged(int)), this, SLOT(updateGraphView())); connect(exportDialog_, SIGNAL(configChanged()), this, SLOT(configModified())); + connect(exportDataDialog_, SIGNAL(configChanged()), this, SLOT(configModified())); // dockwidget QList dockWidgets = this->findChildren(); @@ -578,6 +595,18 @@ void DatabaseViewer::configModified() this->setWindowModified(true); } +void DatabaseViewer::updateGraphInteractionMode() +{ + if(ui_->radioButton_graphSelection->isChecked()) + { + ui_->graphViewer->setInteractionMode(GraphViewer::SelectionMode); + } + else + { + ui_->graphViewer->setInteractionMode(GraphViewer::HandMode); + } +} + QString DatabaseViewer::getIniFilePath() const { if(!iniFilePath_.isEmpty()) @@ -637,6 +666,7 @@ void DatabaseViewer::readSettings() ui_->actionVertical_Layout->setChecked(settings.value("verticalLayout", ui_->actionVertical_Layout->isChecked()).toBool()); ui_->actionConcise_Layout->setChecked(settings.value("conciseLayout", ui_->actionConcise_Layout->isChecked()).toBool()); ui_->checkBox_ignoreIntermediateNodes->setChecked(settings.value("ignoreIntermediateNodes", ui_->checkBox_ignoreIntermediateNodes->isChecked()).toBool()); + ui_->checkBox_fit_in_view->setChecked(settings.value("fitInView", ui_->checkBox_fit_in_view->isChecked()).toBool()); ui_->checkBox_timeStats->setChecked(settings.value("timeStats", ui_->checkBox_timeStats->isChecked()).toBool()); // GraphViewer settings @@ -694,6 +724,7 @@ void DatabaseViewer::readSettings() // Use same parameters used by RTAB-Map settings.beginGroup("Gui"); exportDialog_->loadSettings(settings, exportDialog_->objectName()); + exportDataDialog_->loadSettings(settings, exportDataDialog_->objectName()); settings.beginGroup("PostProcessingDialog"); ui_->doubleSpinBox_detectMore_radius->setValue(settings.value("cluster_radius", ui_->doubleSpinBox_detectMore_radius->value()).toDouble()); ui_->doubleSpinBox_detectMore_radiusMin->setValue(settings.value("cluster_radius_min", ui_->doubleSpinBox_detectMore_radiusMin->value()).toDouble()); @@ -734,6 +765,7 @@ void DatabaseViewer::writeSettings() settings.setValue("verticalLayout", ui_->actionVertical_Layout->isChecked()); settings.setValue("conciseLayout", ui_->actionConcise_Layout->isChecked()); settings.setValue("ignoreIntermediateNodes", ui_->checkBox_ignoreIntermediateNodes->isChecked()); + settings.setValue("fitInView", ui_->checkBox_fit_in_view->isChecked()); settings.setValue("timeStats", ui_->checkBox_timeStats->isChecked()); // save GraphViewer settings @@ -792,6 +824,7 @@ void DatabaseViewer::writeSettings() // Use same parameters used by RTAB-Map settings.beginGroup("Gui"); exportDialog_->saveSettings(settings, exportDialog_->objectName()); + exportDataDialog_->saveSettings(settings, exportDataDialog_->objectName()); settings.beginGroup("PostProcessingDialog"); settings.setValue("cluster_radius", ui_->doubleSpinBox_detectMore_radius->value()); settings.setValue("cluster_radius_min", ui_->doubleSpinBox_detectMore_radiusMin->value()); @@ -829,6 +862,7 @@ void DatabaseViewer::restoreDefaultSettings() ui_->checkBox_alignPosesWithGroundTruth->setChecked(true); ui_->checkBox_alignScansCloudsWithGroundTruth->setChecked(false); ui_->checkBox_ignoreIntermediateNodes->setChecked(false); + ui_->checkBox_fit_in_view->setChecked(true); ui_->checkBox_timeStats->setChecked(true); ui_->comboBox_optimizationFlavor->setCurrentIndex(0); @@ -883,6 +917,8 @@ void DatabaseViewer::restoreDefaultSettings() ui_->checkBox_opt_graph_as_guess->setChecked(true); ui_->spinBox_fromToMapId->setValue(-1); ui_->spinBox_minGraphDistance->setValue(10); + ui_->radioButton_graphHand->setChecked(true); + ui_->checkBox_optimize_on_all_changes->setChecked(true); } void DatabaseViewer::openDatabase() @@ -1166,7 +1202,6 @@ bool DatabaseViewer::closeDatabase() localMaps_.clear(); generatedLocalMaps_.clear(); modifiedLaserScans_.clear(); - ui_->graphViewer->clearAll(); occupancyGridViewer_->clear(); ui_->menuEdit->setEnabled(false); ui_->actionGenerate_3D_map_pcd->setEnabled(false); @@ -1214,6 +1249,9 @@ bool DatabaseViewer::closeDatabase() ui_->horizontalSlider_iterations->setMaximum(0); ui_->horizontalSlider_neighbors->setEnabled(false); ui_->horizontalSlider_neighbors->setMaximum(0); + components_.clear(); // should be called before ui_->graphViewer->clearAll() to ignore selection events + activeComponentIndex_ = 0; + ui_->radioButton_graphSelection->setText(tr("Selection")); // clear count ui_->label_constraint->clear(); ui_->label_constraint_opt->clear(); ui_->label_variance->clear(); @@ -1226,8 +1264,8 @@ bool DatabaseViewer::closeDatabase() ui_->horizontalSlider_A->setMaximum(0); ui_->horizontalSlider_B->setEnabled(false); ui_->horizontalSlider_B->setMaximum(0); - ui_->label_idA->setText("NaN"); - ui_->label_idB->setText("NaN"); + ui_->button_nodeIdA->setText("NaN"); + ui_->button_nodeIdB->setText("NaN"); sliderAValueChanged(0); sliderBValueChanged(0); @@ -1404,16 +1442,58 @@ void DatabaseViewer::exportDatabase() return; } - rtabmap::ExportDialog dialog; - - if(dialog.exec()) + if(exportDataDialog_->exec()) { - if(!dialog.outputPath().isEmpty()) + if(!exportDataDialog_->outputPath().isEmpty()) { - int framesIgnored = dialog.framesIgnored(); - double frameRate = dialog.targetFramerate(); - int sessionExported = dialog.sessionExported(); - QString path = dialog.outputPath(); + int framesIgnored = exportDataDialog_->framesIgnored(); + double frameRate = exportDataDialog_->targetFramerate(); + int sessionExported = exportDataDialog_->sessionExported(); + QString path = exportDataDialog_->outputPath(); + std::set selectedIds; + if(exportDataDialog_->currentComponentOnly()) { + if(exportDataDialog_->selectedNodesOnly()) { + selectedIds = ui_->graphViewer->getSelectedNodeIds(); + if(selectedIds.empty()) + { + QMessageBox::warning(this, + tr("Cannot export database"), + tr("Exporting only selected nodes is requested but none is selected for the current component of the Graph View.")); + return; + } + } + else { + if(components_.empty()) + { + QMessageBox::warning(this, + tr("Cannot export database"), + tr("Exporting only the current component of the Graph View is requested, but the graph is not initialized. Open the Graph View to initialize the components." )); + return; + } + UASSERT(activeComponentIndex_>=0 && activeComponentIndex_<(int)components_.size()); + selectedIds = components_[activeComponentIndex_].nodeIds; + if(selectedIds.empty()) + { + QMessageBox::warning(this, + tr("Cannot export database"), + tr("Exporting only current component of the Graph View, but the current component is empty?!" )); + return; + } + } + } + else if(exportDataDialog_->selectedNodesOnly()) + { + for(const auto & comp: components_) + { + selectedIds.insert(comp.selectedNodeIds.begin(), comp.selectedNodeIds.end()); + } + if(selectedIds.empty()) + { + QMessageBox::warning(this, tr("Cannot export database"), tr("Exporting only selected nodes is requested but none is selected in the Graph View.")); + return; + } + } + rtabmap::DataRecorder recorder; QList ids; @@ -1442,7 +1522,8 @@ void DatabaseViewer::exportDatabase() stamp == 0 || stamp - previousStamp >= 1.0/frameRate) { - if(sessionExported < 0 || sessionExported == mapId) + if((sessionExported < 0 || sessionExported == mapId) && + (selectedIds.empty() || selectedIds.find(ids_[i]) != selectedIds.end())) { ids.push_back(ids_[i]); @@ -1481,10 +1562,10 @@ void DatabaseViewer::exportDatabase() progressDialog->show(); progressDialog->setCancelButtonVisible(true); UINFO("Decompress: rgb=%d depth=%d scan=%d userData=%d", - dialog.isRgbExported()?1:0, - dialog.isDepthExported()?1:0, - dialog.isDepth2dExported()?1:0, - dialog.isUserDataExported()?1:0); + exportDataDialog_->isRgbExported()?1:0, + exportDataDialog_->isDepthExported()?1:0, + exportDataDialog_->isDepth2dExported()?1:0, + exportDataDialog_->isUserDataExported()?1:0); for(int i=0; iisCanceled(); ++i) { @@ -1495,12 +1576,12 @@ void DatabaseViewer::exportDatabase() cv::Mat depth, rgb, userData; LaserScan scan; data.uncompressDataConst( - !dialog.isRgbExported()?0:&rgb, - !dialog.isDepthExported()?0:&depth, - !dialog.isDepth2dExported()?0:&scan, - !dialog.isUserDataExported()?0:&userData); + !exportDataDialog_->isRgbExported()?0:&rgb, + !exportDataDialog_->isDepthExported()?0:&depth, + !exportDataDialog_->isDepth2dExported()?0:&scan, + !exportDataDialog_->isUserDataExported()?0:&userData); cv::Mat covariance = cv::Mat::eye(6,6,CV_64FC1); - if(dialog.isOdomExported()) + if(exportDataDialog_->isOdomExported()) { std::multimap links; dbDriver_->loadLinks(id, links, Link::kNeighbor); @@ -1546,7 +1627,7 @@ void DatabaseViewer::exportDatabase() sensorData.setEnvSensors(sensorsValues.at(id)); } - recorder.addData(sensorData, dialog.isOdomExported()?poses.at(id):Transform(), covariance); + recorder.addData(sensorData, exportDataDialog_->isOdomExported()?poses.at(id):Transform(), covariance); progressDialog->appendText(tr("Exported node %1").arg(id)); progressDialog->incrementStep(); @@ -1876,6 +1957,9 @@ void DatabaseViewer::updateIds() ui_->actionView_optimized_mesh->setEnabled(false); ui_->actionExport_optimized_mesh->setEnabled(false); ui_->actionUpdate_optimized_mesh->setEnabled(uStrNumCmp(dbDriver_->getDatabaseVersion(), "0.13.0") >= 0); + ui_->pushButton_applyRotation->setEnabled(true); + Qt::ItemFlags enableFlags = Qt::ItemFlags(Qt::ItemIsEnabled) | Qt::ItemIsSelectable; + ui_->comboBox_optimizationFlavor->setItemData(2, QVariant(static_cast(enableFlags)), Qt::UserRole - 1); links_.clear(); linksAdded_.clear(); linksRefined_.clear(); @@ -1926,7 +2010,7 @@ void DatabaseViewer::updateIds() uSleep(100); QApplication::processEvents(); - int lastValidNodeId = 0; + lastValidNodeId_ = 0; for(int i=0; isecond.type() == Link::kNeighbor || iter->second.type() == Link::kNeighborMerged) { - lastValidNodeId = ids_[i]; + lastValidNodeId_ = ids_[i]; } } } @@ -2066,17 +2150,14 @@ void DatabaseViewer::updateIds() } float xMin, yMin, cellSize; - bool hasMap = !dbDriver_->load2DMap(xMin, yMin, cellSize).empty(); + bool hasMap = !dbDriver_->load2DMap(xMin, yMin, cellSize).empty(); + bool hasMesh = !dbDriver_->loadOptimizedMesh().empty(); ui_->actionEdit_optimized_2D_map->setEnabled(hasMap); ui_->actionExport_saved_2D_map->setEnabled(hasMap); ui_->actionImport_2D_map->setEnabled(hasMap); ui_->actionRegenerate_optimized_2D_map->setEnabled(uStrNumCmp(dbDriver_->getDatabaseVersion(), "0.17.0") >= 0); - - if(!dbDriver_->loadOptimizedMesh().empty()) - { - ui_->actionView_optimized_mesh->setEnabled(true); - ui_->actionExport_optimized_mesh->setEnabled(true); - } + ui_->actionView_optimized_mesh->setEnabled(hasMesh); + ui_->actionExport_optimized_mesh->setEnabled(hasMesh); UINFO("Loaded %d ids, %d poses and %d links", (int)ids_.size(), (int)odomPoses_.size(), (int)links_.size()); @@ -2155,16 +2236,16 @@ void DatabaseViewer::updateIds() } } - if(lastValidNodeId>0) + if(lastValidNodeId_>0) { // find full connected graph from last node in working memory Optimizer * optimizer = Optimizer::create(ui_->parameters_toolbox->getParameters()); std::map posesOut; std::multimap linksOut; - UINFO("Get connected graph from %d (%d poses, %d links)", lastValidNodeId, (int)odomPoses_.size(), (int)links_.size()); + UINFO("Get connected graph from %d (%d poses, %d links)", lastValidNodeId_, (int)odomPoses_.size(), (int)links_.size()); optimizer->getConnectedGraph( - lastValidNodeId, + lastValidNodeId_, odomPoses_, links_, posesOut, @@ -2220,8 +2301,8 @@ void DatabaseViewer::updateIds() ui_->spinBox_indexA->setEnabled(false); ui_->spinBox_indexB->setEnabled(false); - ui_->label_idA->setText("NaN"); - ui_->label_idB->setText("NaN"); + ui_->button_nodeIdA->setText("NaN"); + ui_->button_nodeIdB->setText("NaN"); } if(ids_.size()) @@ -2487,6 +2568,11 @@ void DatabaseViewer::editDepthImage() this->update3dView(); } } + else + { + QMessageBox::warning(this, tr("Cannot edit depth"), tr("Node %1 has no depth image!").arg(id)); + return; + } } } @@ -3336,6 +3422,10 @@ void DatabaseViewer::import2DMap() void DatabaseViewer::regenerateSavedMap() { + // Make sure this action is disabled if + // current active component is not main + UASSERT(activeComponentIndex_ == 0); + if(!dbDriver_) { QMessageBox::warning(this, tr("Cannot import 2D map"), tr("A database must must loaded first...\nUse File->Open database.")); @@ -3602,6 +3692,10 @@ void DatabaseViewer::exportOptimizedMesh() void DatabaseViewer::updateOptimizedMesh() { + // Make sure this action is disabled if + // current active component is not main + UASSERT(activeComponentIndex_ == 0); + if(!ids_.size() || !dbDriver_) { QMessageBox::warning(this, tr("Cannot generate a graph"), tr("The database is empty...")); @@ -4000,9 +4094,18 @@ void DatabaseViewer::regenerateCurrentLocalMaps() return; } + std::set selectedIds = ui_->graphViewer->getSelectedNodeIds(); QSet idsSet; - idsSet.insert(ids_.at(ui_->horizontalSlider_A->value())); - idsSet.insert(ids_.at(ui_->horizontalSlider_B->value())); + for(auto id : selectedIds) + { + idsSet.insert(id); + } + if(idsSet.empty()) + { + // legacy behavior: used currently shown nodes + idsSet.insert(ids_.at(ui_->horizontalSlider_A->value())); + idsSet.insert(ids_.at(ui_->horizontalSlider_B->value())); + } #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 3) QList ids(idsSet.begin(), idsSet.end()); #else @@ -4153,6 +4256,21 @@ void DatabaseViewer::view3DMap() } if(optimizedPoses.size() > 0) { + std::set selectedIds = ui_->graphViewer->getSelectedNodeIds(); + if(!selectedIds.empty()) + { + UINFO("Viewing %ld/%ld selected nodes of the current component.", selectedIds.size(), optimizedPoses.size()); + for(std::map::iterator iter = optimizedPoses.begin(); iter != optimizedPoses.end();) + { + if(selectedIds.find(iter->first) == selectedIds.end()) { + iter = optimizedPoses.erase(iter); + } + else { + ++iter; + } + } + } + exportDialog_->setDBDriver(dbDriver_); exportDialog_->viewClouds(optimizedPoses, updateLinksWithModifications(links_), @@ -4272,6 +4390,21 @@ void DatabaseViewer::generate3DMap() } if(optimizedPoses.size() > 0) { + std::set selectedIds = ui_->graphViewer->getSelectedNodeIds(); + if(!selectedIds.empty()) + { + UINFO("Exporting %ld/%ld selected nodes of the current component.", selectedIds.size(), optimizedPoses.size()); + for(std::map::iterator iter = optimizedPoses.begin(); iter != optimizedPoses.end();) + { + if(selectedIds.find(iter->first) == selectedIds.end()) { + iter = optimizedPoses.erase(iter); + } + else { + ++iter; + } + } + } + exportDialog_->setDBDriver(dbDriver_); exportDialog_->exportClouds(optimizedPoses, updateLinksWithModifications(links_), @@ -4327,11 +4460,14 @@ 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(); + std::set selectedIds = ui_->graphViewer->getSelectedNodeIds(); int fromToMapId = ui_->spinBox_fromToMapId->value(); int minimumGraphDistance = ui_->spinBox_minGraphDistance->value(); - if(!interSession && !intraSession) + if(!interSession && !intraSession && selectedIds.empty()) { - QMessageBox::warning(this, tr("Cannot detect more loop closures"), tr("Intra and inter session parameters are disabled! Enable one or both.")); + QMessageBox::warning(this, + tr("Cannot detect more loop closures"), + tr("Intra and inter session parameters are disabled! Enable one or both. You can also select specific nodes of the current component instead.")); return; } @@ -4351,6 +4487,21 @@ void DatabaseViewer::detectMoreLoopClosures() break; } + if(!selectedIds.empty()) + { + progressDialog->appendText(tr("Looking for more loop closures for %1/%2 selected nodes of the current component.").arg(selectedIds.size()).arg(optimizedPoses.size())); + for(std::map::iterator iter = optimizedPoses.begin(); iter != optimizedPoses.end();) + { + if(selectedIds.find(iter->first) == selectedIds.end()) { + iter = optimizedPoses.erase(iter); + } + else { + ++iter; + } + } + } + + progressDialog->appendText(tr("Looking for more loop closures: %1 clusters found.").arg(clusters.size())); if(fromToMapId >=0) { @@ -4422,8 +4573,7 @@ void DatabaseViewer::detectMoreLoopClosures() int mapIdFrom = uValue(mapIds_, from, 0); int mapIdTo = uValue(mapIds_, to, 0); - if((interSession && mapIdFrom != mapIdTo) || - (intraSession && mapIdFrom == mapIdTo)) + if((((interSession && mapIdFrom != mapIdTo) || (intraSession && mapIdFrom == mapIdTo)) && selectedIds.empty()) || !selectedIds.empty()) { // only add new links and one per cluster per iteration if(rtabmap::graph::findLink(checkedLoopClosures, from, to) == checkedLoopClosures.end()) @@ -4457,7 +4607,15 @@ void DatabaseViewer::detectMoreLoopClosures() } } progressDialog->incrementStep(); - if(i%100) + if(clusters.size() <= 100) + { + QApplication::processEvents(); + } + else if(clusters.size() > 100 && i%10 == 0) + { + QApplication::processEvents(); + } + else if(clusters.size() > 1000 && i%100 == 0) { QApplication::processEvents(); } @@ -4489,6 +4647,127 @@ void DatabaseViewer::detectMoreLoopClosures() progressDialog->setValue(progressDialog->maximumSteps()); } +void DatabaseViewer::mergeComponents() +{ + std::shared_ptr reg(Registration::create(ui_->parameters_toolbox->getParameters())); + if(!reg->isImageRequired()) + { + QMessageBox::warning(this, + tr("Merge Components"), + tr("Registration is done without vision (see %1 parameter), so we cannot compute a " + "transformation guess between the nodes belonging to different components." + "\n\nIf the database has images, it is recommended to use %2 = 0 or 2 " + "instead so that the guess can be found visually. Open the \"Core Parameters\" panel " + "and the parameter is under Reg group.") + .arg(Parameters::kRegStrategy().c_str()).arg(Parameters::kRegStrategy().c_str())); + return; + } + + if(components_.size() < 2) + { + QMessageBox::warning(this, + tr("Merge Components"), + tr("A minimum of two components are required to merge them. Make sure Graph View " + "shows two disjoint components and select nodes from both components that should " + "be close by when merged.")); + return; + } + + int fromCompIndex = activeComponentIndex_; + int toCompIndex = -1; + + std::set activeSelection = ui_->graphViewer->getSelectedNodeIds(); + if(activeSelection.empty()) + { + QMessageBox::warning(this, tr("Merge Components"), tr("Please select nodes in the active component.")); + return; + } + + for(size_t i = 0; i < components_.size(); ++i) + { + if((int)i != fromCompIndex && !components_[i].selectedNodeIds.empty()) + { + if(toCompIndex == -1) + { + toCompIndex = (int)i; + } + else + { + QMessageBox::warning(this, tr("Merge Components"), tr("Please select nodes from exactly two different components.")); + return; + } + } + } + + if(toCompIndex == -1) + { + QMessageBox::warning(this, tr("Merge Components"), tr("Please select nodes in exactly one other component to merge with the active one.")); + return; + } + + std::vector N1(activeSelection.begin(), activeSelection.end()); + std::vector N2(components_[toCompIndex].selectedNodeIds.begin(), components_[toCompIndex].selectedNodeIds.end()); + + int totalPairs = (int)(N1.size() * N2.size()); + + rtabmap::ProgressDialog * progressDialog = new rtabmap::ProgressDialog(this); + progressDialog->setAttribute(Qt::WA_DeleteOnClose); + progressDialog->setMaximumSteps(totalPairs); + progressDialog->setCancelButtonVisible(true); + progressDialog->setMinimumWidth(800); + progressDialog->show(); + progressDialog->appendText(tr("Looking for loop closure between %1 possible pairs...").arg(totalPairs)); + + bool loopFound = false; + int foundFrom = 0; + int foundTo = 0; + int step = 0; + + for(size_t i = 0; i < N1.size() && !loopFound && !progressDialog->isCanceled(); ++i) + { + for(size_t j = 0; j < N2.size() && !loopFound && !progressDialog->isCanceled(); ++j) + { + int from = N1[i]; + int to = N2[j]; + progressDialog->appendText(tr("Testing pair %1 and %2... (%3/%4)").arg(from).arg(to).arg(step+1).arg(totalPairs)); + + QApplication::processEvents(); + + if(addConstraint(from, to, reg.get(), true, false)) + { + loopFound = true; + foundFrom = from; + foundTo = to; + progressDialog->appendText(tr("Loop closure found between %1 and %2!").arg(from).arg(to)); + } + + ++step; + progressDialog->incrementStep(); + QApplication::processEvents(); + } + } + + progressDialog->setValue(progressDialog->maximumSteps()); + + if(loopFound) + { + QMessageBox::information(this, tr("Merge Components"), tr("Successfully merged components! Loop closure found between %1 and %2.").arg(foundFrom).arg(foundTo)); + + regenerateGraphComponents(); + this->updateGraphView(); + + // Combine both selections so the user can easily run detectMoreLoopClosures if they want + std::set finalSelection = activeSelection; + finalSelection.insert(N2.begin(), N2.end()); + ui_->graphViewer->selectNodesFromIds(finalSelection); + ui_->graphViewer->centerOnNode(foundFrom); + } + else if (!progressDialog->isCanceled()) + { + QMessageBox::information(this, tr("Merge Components"), tr("No loop closure found between the selected nodes of the two components.")); + } +} + void DatabaseViewer::updateAllNeighborCovariances() { std::multimap allLinks = updateLinksWithModifications(links_); @@ -4638,6 +4917,11 @@ void DatabaseViewer::refineLinks() maxNodeId, minMapId, maxMapId); + + std::set selectedNodeIds = ui_->graphViewer->getSelectedNodeIds(); + linkRefiningDialog_->setRangesEnabled( + !selectedNodeIds.empty(), + activeComponentIndex_>=0 && activeComponentIndex_<(int)components_.size() && !components_[activeComponentIndex_].nodeIds.empty()); if(linkRefiningDialog_->exec() == QDialog::Accepted) { @@ -4647,6 +4931,10 @@ void DatabaseViewer::refineLinks() linkRefiningDialog_->getRangeNodeId(minMapId, maxMapId); bool intra, inter; linkRefiningDialog_->getIntraInterSessions(intra, inter); + if(linkRefiningDialog_->isRangeByCurrentComponent()) + { + selectedNodeIds = components_[activeComponentIndex_].nodeIds; + } for(std::multimap::iterator iter=allLinks.begin(); iter!=allLinks.end(); ++iter) { if(iter->second.type() < Link::kPosePrior && @@ -4656,7 +4944,10 @@ void DatabaseViewer::refineLinks() int to = iter->second.to(); int mapFrom = uValue(mapIds_, from, 0); int mapTo = uValue(mapIds_, to, 0); - if(((linkRefiningDialog_->isRangeByNodeId() && + if((((linkRefiningDialog_->isRangeBySelectedNodeIds() || linkRefiningDialog_->isRangeByCurrentComponent()) && + (selectedNodeIds.find(from) != selectedNodeIds.end() && + selectedNodeIds.find(to) != selectedNodeIds.end())) || + (linkRefiningDialog_->isRangeByNodeId() && ((from >= minNodeId && from <= maxNodeId) || (to >= minNodeId && to <= maxNodeId))) || (linkRefiningDialog_->isRangeByMapId() && @@ -4745,6 +5036,42 @@ void DatabaseViewer::resetAllChanges() } } +void DatabaseViewer::zoomToNode() +{ + int value = -1; + if(sender() == ui_->button_nodeIdA) + { + value = ui_->spinBox_indexA->value(); + } + else if(sender() == ui_->button_nodeIdB) + { + value = ui_->spinBox_indexB->value(); + } + if(value >= 0 && value < ids_.size()) + { + int nodeId = ids_.at(value); + int componentIndex = -1; + for(int i = 0; i < (int)components_.size(); ++i) + { + if(components_[i].nodeIds.find(nodeId) != components_[i].nodeIds.end()) + { + componentIndex = i; + break; + } + } + if(componentIndex >= 0) + { + if(componentIndex != activeComponentIndex_) { + ui_->tabBar_components->setCurrentIndex(componentIndex); + } + else { + ui_->graphViewer->highlightNode(nodeId, sender()==ui_->button_nodeIdB?1:0); + } + ui_->graphViewer->centerOnNode(nodeId); + } + } +} + void DatabaseViewer::graphNodeSelected(int id) { if(id>0 && idToIndex_.contains(id)) @@ -4783,7 +5110,7 @@ void DatabaseViewer::sliderAValueChanged(int value) ui_->label_labelA, ui_->label_stampA, ui_->graphicsView_A, - ui_->label_idA, + ui_->button_nodeIdA, ui_->label_mapA, ui_->label_poseA, ui_->label_optposeA, @@ -4810,7 +5137,7 @@ void DatabaseViewer::sliderBValueChanged(int value) ui_->label_labelB, ui_->label_stampB, ui_->graphicsView_B, - ui_->label_idB, + ui_->button_nodeIdB, ui_->label_mapB, ui_->label_poseB, ui_->label_optposeB, @@ -4835,7 +5162,7 @@ void DatabaseViewer::update(int value, QLabel * label, QLabel * stamp, rtabmap::ImageView * view, - QLabel * labelId, + QToolButton * buttonNodeId, QLabel * labelMapId, QLabel * labelPose, QLabel * labelOptPose, @@ -4859,6 +5186,7 @@ void DatabaseViewer::update(int value, spinBoxIndex->blockSignals(false); labelParents->clear(); labelChildren->clear(); + buttonNodeId->setEnabled(false); weight->clear(); label->clear(); labelMapId->clear(); @@ -4880,7 +5208,7 @@ void DatabaseViewer::update(int value, view->clear(); int id = ids_.at(value); int mapId = -1; - labelId->setText(QString::number(id)); + buttonNodeId->setText(QString::number(id)); if(id>0) { if(ui_->dockWidget_graphView->isVisible()) { @@ -5001,6 +5329,7 @@ void DatabaseViewer::update(int value, weight->setNum(w); label->setText(l.c_str()); + buttonNodeId->setEnabled(w>=0 || w==-1); float x,y,z,roll,pitch,yaw; odomPose.getTranslationAndEulerAngles(x,y,z,roll, pitch,yaw); labelPose->setText(QString("%1xyz=(%2,%3,%4)\nrpy=(%5,%6,%7)").arg(odomPose.isIdentity()?"* ":"").arg(x).arg(y).arg(z).arg(roll).arg(pitch).arg(yaw)); @@ -6252,7 +6581,7 @@ void DatabaseViewer::sliderAMoved(int value) ui_->spinBox_indexA->blockSignals(false); if(value>=0 && value < ids_.size()) { - ui_->label_idA->setText(QString::number(ids_.at(value))); + ui_->button_nodeIdA->setText(QString::number(ids_.at(value))); } else { @@ -6267,7 +6596,7 @@ void DatabaseViewer::sliderBMoved(int value) ui_->spinBox_indexB->blockSignals(false); if(value>=0 && value < ids_.size()) { - ui_->label_idB->setText(QString::number(ids_.at(value))); + ui_->button_nodeIdB->setText(QString::number(ids_.at(value))); } else { @@ -6371,6 +6700,7 @@ void DatabaseViewer::editConstraint() } if(priorId==0) { + regenerateGraphComponents(); this->updateGraphView(); updateConstraintView(); } @@ -6399,6 +6729,7 @@ void DatabaseViewer::editConstraint() updated = true; if(priorId==0) { + regenerateGraphComponents(); this->updateGraphView(); updateLoopClosuresSlider(from, to); } @@ -6421,20 +6752,12 @@ void DatabaseViewer::editConstraint() ui_->parameters_toolbox->updateParameter(Parameters::kOptimizerPriorsIgnored(), "false"); } } - int indexA = ui_->horizontalSlider_A->value(); - int indexB = ui_->horizontalSlider_B->value(); + if(!priorsIgnored) { + regenerateGraphComponents(); this->updateGraphView(); } - if(ui_->horizontalSlider_A->value() != indexA) - ui_->horizontalSlider_A->setValue(indexA); - else - sliderAValueChanged(indexA); - if(ui_->horizontalSlider_B->value() != indexB) - ui_->horizontalSlider_B->setValue(indexB); - else - sliderBValueChanged(indexB); } } } @@ -6585,7 +6908,7 @@ void DatabaseViewer::updateConstraintView( ui_->label_labelA, ui_->label_stampA, ui_->graphicsView_A, - ui_->label_idA, + ui_->button_nodeIdA, ui_->label_mapA, ui_->label_poseA, ui_->label_optposeA, @@ -6610,7 +6933,7 @@ void DatabaseViewer::updateConstraintView( ui_->label_labelB, ui_->label_stampB, ui_->graphicsView_B, - ui_->label_idB, + ui_->button_nodeIdB, ui_->label_mapB, ui_->label_poseB, ui_->label_optposeB, @@ -7202,6 +7525,22 @@ void DatabaseViewer::updateConstraintButtons() } } +void DatabaseViewer::optimizationIgnoredStateChanged() +{ + if(ui_->checkBox_optimize_on_all_changes->isChecked()) + { + this->updateGraphView(); + } +} + +void DatabaseViewer::fitInViewClicked() +{ + if(ui_->checkBox_fit_in_view->isChecked()) + { + sliderIterationsValueChanged(ui_->horizontalSlider_iterations->value()); + } +} + void DatabaseViewer::sliderIterationsValueChanged(int value) { UDEBUG("sender=%s value=%d currentValue = %d", sender()?sender()->objectName().toStdString().c_str():"NA", value, ui_->horizontalSlider_iterations->value()); @@ -7522,7 +7861,8 @@ void DatabaseViewer::sliderIterationsValueChanged(int value) } } // Only zoom if there are significant unknowns - if( (xLast > xFirst && yLast > yFirst) && + if( ui_->checkBox_fit_in_view->isChecked() && + (xLast > xFirst && yLast > yFirst) && (xFirst > 50 || xLast < map.cols-50 || yFirst > 50 || @@ -7619,7 +7959,7 @@ void DatabaseViewer::sliderIterationsValueChanged(int value) ui_->graphViewer->scene()->removeItem(rectScaleItem); delete rectScaleItem; } - else { + else if(ui_->checkBox_fit_in_view->isChecked()) { ui_->graphViewer->fitInView(ui_->graphViewer->sceneRect(), Qt::KeepAspectRatio); } @@ -7667,8 +8007,241 @@ void DatabaseViewer::updateGraphRotation() } } +void DatabaseViewer::backupCurrentComponent() +{ + UDEBUG(""); + if(activeComponentIndex_ < 0 || activeComponentIndex_ >= (int)components_.size()) return; + + GraphComponent & component = components_[activeComponentIndex_]; + + if(!ui_->graphViewer || component.rootId <= 0 || component.nodeIds.find(component.rootId) == component.nodeIds.end()) + { + component.viewScale = -1.0f; + component.rootCoordinates = QPointF(); + return; + } + + if(ui_->horizontalSlider_rotation->isEnabled()) + { + component.rotationValue = ui_->horizontalSlider_rotation->value(); + } + + component.viewScale = ui_->graphViewer->transform().m11(); + component.rootCoordinates = ui_->graphViewer->mapFromScene(ui_->graphViewer->getNodeScenePosition(component.rootId)); + component.selectedNodeIds = ui_->graphViewer->getSelectedNodeIds(); + component.optimizationFlavor = ui_->comboBox_optimizationFlavor->currentIndex(); +} + +void DatabaseViewer::regenerateGraphComponents() +{ + UDEBUG(""); + if(odomPoses_.empty()) { + components_.clear(); + ui_->radioButton_graphSelection->setText(tr("Selection")); + ui_->tabBar_components->setEnabled(false); + return; + } + + std::multimap links = updateLinksWithModifications(links_); + + backupCurrentComponent(); + + std::vector oldComponents = components_; + components_.clear(); + + std::map remainingPoses = odomPoses_; + + std::shared_ptr optimizer(Optimizer::create(ui_->parameters_toolbox->getParameters())); + bool optimizeFromGraphEnd = Parameters::defaultRGBDOptimizeFromGraphEnd(); + Parameters::parse(dbDriver_->getLastParameters(), Parameters::kRGBDOptimizeFromGraphEnd(), optimizeFromGraphEnd); + + /* Extracting new components */ + while(!remainingPoses.empty()) + { + int rootId = remainingPoses.begin()->first; + + // Ignore nodes with weight < 0 + if(weights_.find(rootId) == weights_.end() || weights_.at(rootId) < 0) + { + remainingPoses.erase(rootId); + continue; + } + + if( components_.empty() && + lastValidNodeId_>0 && + remainingPoses.find(lastValidNodeId_)!=remainingPoses.end() && + weights_.find(lastValidNodeId_) != weights_.end() && + weights_.at(lastValidNodeId_) >= 0) + { + // First component is the main one (the one linked to latest valid id in working memory) + rootId = lastValidNodeId_; + } + + std::map posesOut; + std::multimap linksOut; + optimizer->getConnectedGraph( + rootId, + remainingPoses, + links, + posesOut, + linksOut); + + if(!posesOut.empty()) + { + GraphComponent comp; + + if(posesOut.find(ui_->spinBox_optimizationsFrom->value()) != posesOut.end()) + { + comp.rootId = ui_->spinBox_optimizationsFrom->value(); + } + else if(optimizeFromGraphEnd) + { + comp.rootId = posesOut.rbegin()->first; + } + else + { + comp.rootId = posesOut.lower_bound(1)->first; + } + for(std::map::const_iterator iter=posesOut.begin(); iter!=posesOut.end(); ++iter) + { + comp.nodeIds.insert(comp.nodeIds.end(), iter->first); + remainingPoses.erase(iter->first); + } + comp.firstNodeId = posesOut.lower_bound(1)->first; + comp.lastNodeId = posesOut.rbegin()->first; + + components_.push_back(comp); + } + else { + //Failed, just remove it + remainingPoses.erase(rootId); + } + } + + /* Mapping old components and new ones */ + int newActiveIndex = -1; + if (!oldComponents.empty()) + { + for(size_t i = 0; i < oldComponents.size(); ++i) + { + const GraphComponent & oldComp = oldComponents[i]; + // Check if the old component still exist, + // if so, check if it has been modified + bool found = false; + for(size_t j = 0; j < components_.size() && !found; ++j) + { + GraphComponent & newComp = components_[j]; + for(const auto & id: oldComp.nodeIds) + { + if(newComp.nodeIds.find(id) != newComp.nodeIds.end()) + { + // If same size, copy old component's state + if(oldComp.nodeIds.size() == newComp.nodeIds.size()) + { + if(activeComponentIndex_ == (int)i) + { + newActiveIndex = (int)j; + } + newComp.rotationValue = oldComp.rotationValue; + newComp.viewScale = oldComp.viewScale; + newComp.rootId = oldComp.rootId; + newComp.rootCoordinates = oldComp.rootCoordinates; + newComp.selectedNodeIds = oldComp.selectedNodeIds; + } + found = true; + break; + } + } + } + } + } + + if (newActiveIndex != -1) + { + activeComponentIndex_ = newActiveIndex; + } + else + { + activeComponentIndex_ = 0; + for(size_t i = 0; i < components_.size(); ++i) { + if (ui_->spinBox_optimizationsFrom->value() == components_[i].rootId) { + activeComponentIndex_ = (int)i; + break; + } + } + } + + ui_->tabBar_components->blockSignals(true); + + while(ui_->tabBar_components->count() > 0) { + ui_->tabBar_components->removeTab(0); + } + + int total = 0; + for(size_t i = 0; i < components_.size(); ++i) { + QString tabLabel = QString("%1%2 (%3)%4") + .arg(i==0 ? "Main" : "") + .arg(i>0?char('A'+char(i-1)):'\0') + .arg(components_[i].nodeIds.size()) + .arg(components_[i].selectedNodeIds.empty()?"":QString(" [%1]").arg(components_[i].selectedNodeIds.size())); + ui_->tabBar_components->addTab(tabLabel); + total+=components_[i].selectedNodeIds.size(); + } + ui_->radioButton_graphSelection->setText(total==0?tr("Selection"):tr("Selection [%1]").arg(total)); + + ui_->tabBar_components->setCurrentIndex(activeComponentIndex_); + ui_->tabBar_components->setEnabled(components_.size() > 1); + ui_->tabBar_components->setVisible(components_.size() > 1); + ui_->graphComponentText->setVisible(components_.size() > 1); + ui_->graphComponentText->setText(components_.size() == 1 ? QString("") : QString("Components:")); + ui_->tabBar_components->blockSignals(false); +} + +void DatabaseViewer::updateComponentSelectedNodes() +{ + UDEBUG(""); + if(activeComponentIndex_>=0 && activeComponentIndex_<(int)components_.size()) + { + UASSERT(ui_->tabBar_components->count() == (int)components_.size()); + UDEBUG("Active component=%d selected before=%ld after=%ld", + activeComponentIndex_, + components_[activeComponentIndex_].selectedNodeIds.size(), + ui_->graphViewer->getSelectedNodeIds().size()); + components_[activeComponentIndex_].selectedNodeIds = ui_->graphViewer->getSelectedNodeIds(); + QString tabLabel = QString("%1%2 (%3)%4") + .arg(activeComponentIndex_==0 ? "Main" : "") + .arg(activeComponentIndex_>0?char('A'+char(activeComponentIndex_-1)):'\0') + .arg(components_[activeComponentIndex_].nodeIds.size()) + .arg(components_[activeComponentIndex_].selectedNodeIds.empty()?"":QString(" [%1]").arg(components_[activeComponentIndex_].selectedNodeIds.size())); + ui_->tabBar_components->setTabText(activeComponentIndex_, tabLabel); + } + + int total = 0; + for(auto & component: components_) + { + total += component.selectedNodeIds.size(); + } + ui_->radioButton_graphSelection->setText(total==0?tr("Selection"):tr("Selection [%1]").arg(total)); +} + +void DatabaseViewer::clearAllSelectedNodes() +{ + UASSERT(ui_->tabBar_components->count() == (int)components_.size()); + for(size_t i = 0; i < components_.size(); ++i) { + QString tabLabel = QString("%1%2 (%3)") + .arg(i==0 ? "Main" : "") + .arg(i>0?char('A'+char(i-1)):'\0') + .arg(components_[i].nodeIds.size()); + components_[i].selectedNodeIds.clear(); + ui_->tabBar_components->setTabText(i, tabLabel); + } + ui_->radioButton_graphSelection->setText(tr("Selection")); + ui_->graphViewer->scene()->clearSelection(); +} + void DatabaseViewer::updateGraphView() { + UDEBUG(""); ui_->label_loopClosures->clear(); ui_->label_poses->clear(); ui_->label_rmse->clear(); @@ -7684,6 +8257,8 @@ void DatabaseViewer::updateGraphView() if(odomPoses_.size()) { + if (components_.size() == 0) regenerateGraphComponents(); + int fromId = ui_->spinBox_optimizationsFrom->value(); if(!uContains(odomPoses_, fromId)) { @@ -8142,7 +8717,7 @@ void DatabaseViewer::updateGraphView() else { // Global map re-optimized - ui_->pushButton_applyRotation->setEnabled(true); + ui_->pushButton_applyRotation->setEnabled(activeComponentIndex_==0); ui_->horizontalSlider_rotation->setEnabled(true); ui_->spinBox_optimizationsFrom->setEnabled(true); ui_->checkBox_spanAllMaps->setEnabled(true); @@ -8184,6 +8759,115 @@ void DatabaseViewer::updateGraphView() } } +void DatabaseViewer::updateGraphViewRootId() +{ + int id = ui_->spinBox_optimizationsFrom->value(); + if(uContains(odomPoses_, id)) + { + int fromIdComponentIndex = -1; + for(int i = 0; i < (int)components_.size(); ++i) + { + if(components_[i].nodeIds.find(id) != components_[i].nodeIds.end()) + { + fromIdComponentIndex = i; + if(components_[i].rootId != id) + { + components_[i].rootCoordinates = QPointF(); + } + components_[i].rootId = id; + break; + } + } + + if (fromIdComponentIndex >= 0 && fromIdComponentIndex != activeComponentIndex_) { + // updateGraphView() is called inside tabBarComponentsValueChanged() slot + ui_->tabBar_components->setCurrentIndex(fromIdComponentIndex); + } + else { + updateGraphView(); + } + } + else + { + QMessageBox::warning(this, tr(""), tr("Graph optimization from id (%1) for which node is not linked to graph.\n Minimum=%2, Maximum=%3") + .arg(id) + .arg(odomPoses_.begin()->first) + .arg(odomPoses_.rbegin()->first)); + return; + } +} + +void DatabaseViewer::tabBarComponentsValueChanged(int value) +{ + UDEBUG(""); + if(value < 0 || value >= (int)components_.size() || value == activeComponentIndex_) { + return; + } + backupCurrentComponent(); + activeComponentIndex_ = value; + GraphComponent & comp = components_[activeComponentIndex_]; + + ui_->horizontalSlider_rotation->blockSignals(true); + ui_->horizontalSlider_rotation->setValue(comp.rotationValue); + ui_->label_rotation->setText(QString::number(float(-ui_->horizontalSlider_rotation->value())/10.0f, 'f', 1) + " deg"); + ui_->horizontalSlider_rotation->blockSignals(false); + + ui_->pushButton_applyRotation->setEnabled(value==0); + if(value==0) + { + Qt::ItemFlags enableFlags = Qt::ItemFlags(Qt::ItemIsEnabled) | Qt::ItemIsSelectable; + ui_->comboBox_optimizationFlavor->setItemData(2, QVariant(static_cast(enableFlags)), Qt::UserRole - 1); + } + else + { + ui_->comboBox_optimizationFlavor->setItemData(2, 0, Qt::UserRole - 1); + } + ui_->comboBox_optimizationFlavor->setCurrentIndex(value==0 || comp.optimizationFlavor<=1?comp.optimizationFlavor:0); + ui_->spinBox_optimizationsFrom->setValue(comp.rootId); + ui_->label_optimizeFrom->setText(tr("Root [%1, %2]").arg(comp.firstNodeId).arg(comp.lastNodeId)); + + // File menu + ui_->actionRegenerate_optimized_2D_map->setEnabled(value==0 && uStrNumCmp(dbDriver_->getDatabaseVersion(), "0.17.0") >= 0); + ui_->actionUpdate_optimized_mesh->setEnabled(value==0 && uStrNumCmp(dbDriver_->getDatabaseVersion(), "0.13.0") >= 0); + + disconnect(ui_->graphViewer, SIGNAL(nodesSelected()), this , SLOT(updateComponentSelectedNodes())); + updateGraphView(); + connect(ui_->graphViewer, SIGNAL(nodesSelected()), this , SLOT(updateComponentSelectedNodes())); + + if(!ui_->radioButton_graphSelection->isChecked()) + { + comp.selectedNodeIds.clear(); + } + + // restore component's view + if(comp.viewScale > 0.0f && + comp.rootId > 0 && + comp.rootCoordinates != QPointF() && + comp.nodeIds.find(comp.rootId) != comp.nodeIds.end()) + { + QPointF rootScenePos = ui_->graphViewer->getNodeScenePosition(comp.rootId); + + ui_->graphViewer->resetTransform(); + ui_->graphViewer->scale(comp.viewScale, comp.viewScale); + + QPointF viewportCenter = ui_->graphViewer->viewport()->rect().center(); + QPointF viewportDelta = viewportCenter - comp.rootCoordinates; + QPointF sceneDelta = viewportDelta / comp.viewScale; + + updateGraphRotation(); + + ui_->graphViewer->centerOn(rootScenePos + sceneDelta); + ui_->graphViewer->selectNodesFromIds(comp.selectedNodeIds); + ui_->graphViewer->update(); + } + + updateComponentSelectedNodes(); // To update selection count in labels + + // Update info of the nodes currently shown + sliderAValueChanged(ui_->spinBox_indexA->value()); + sliderBValueChanged(ui_->spinBox_indexB->value()); +} + void DatabaseViewer::updateGrid() { if(sender() == ui_->checkBox_grid_2d && !ui_->checkBox_grid_2d->isChecked()) @@ -8796,7 +9480,7 @@ void DatabaseViewer::refineConstraint(int from, int to, Registration * reg, Regi updated = true; } - if(updated && !silent) + if(updated && !silent && ui_->checkBox_optimize_on_all_changes->isChecked()) { this->updateGraphView(); } @@ -9279,8 +9963,24 @@ bool DatabaseViewer::addConstraint(int from, int to, Registration * reg, bool si if(updateConstraints) { - updateLoopClosuresSlider(fromS->id(), toS->id()); - this->updateGraphView(); + int oldRootId = !components_.empty()?components_[activeComponentIndex_].rootId:0; + regenerateGraphComponents(); + std::map currentGraphNodes = uValueAt(graphes_, ui_->horizontalSlider_iterations->value()); + std::set currentComponentNodes = !components_.empty() ? components_[activeComponentIndex_].nodeIds : std::set(); + bool componentsChanged = currentGraphNodes.empty() || oldRootId == 0 || currentGraphNodes.size() != currentComponentNodes.size() || oldRootId != components_[activeComponentIndex_].rootId; + if(!ui_->checkBox_optimize_on_all_changes->isChecked() && !componentsChanged) { + // just add the link and update graph viewer + graphLinks_.insert(std::make_pair(newLink.from(), newLink)); + ui_->graphViewer->updateGraph(currentGraphNodes, graphLinks_, mapIds_, weights_); + updateLoopClosuresSlider(fromS->id(), toS->id()); + } + else { + this->updateGraphView(); + // graph would have been updated, re-highlight the nodes + ui_->graphViewer->highlightNode(ids_.at(ui_->horizontalSlider_A->value()), 0); + ui_->graphViewer->highlightNode(ids_.at(ui_->horizontalSlider_B->value()), 1); + } + this->updateConstraintView(newLink, false, *fromS, *toS); } @@ -9307,6 +10007,7 @@ bool DatabaseViewer::addConstraint(int from, int to, Registration * reg, bool si else if(updateConstraints) { updateLoopClosuresSlider(from, to); + regenerateGraphComponents(); this->updateGraphView(); } } @@ -9349,7 +10050,9 @@ void DatabaseViewer::resetConstraint() if(iter != linksRefined_.end()) { linksRefined_.erase(iter); - this->updateGraphView(); + if(ui_->checkBox_optimize_on_all_changes->isChecked()) { + this->updateGraphView(); + } } updateConstraintView(); @@ -9421,26 +10124,34 @@ void DatabaseViewer::rejectConstraint() { if(priorId==0) { - this->updateGraphView(); + int oldRootId = !components_.empty()?components_[activeComponentIndex_].rootId:0; + regenerateGraphComponents(); + std::map currentGraphNodes = uValueAt(graphes_, ui_->horizontalSlider_iterations->value()); + std::set currentComponentNodes = !components_.empty() ? components_[activeComponentIndex_].nodeIds : std::set(); + bool componentsChanged = currentGraphNodes.empty() || oldRootId == 0 || currentGraphNodes.size() != currentComponentNodes.size() || oldRootId != components_[activeComponentIndex_].rootId; + if(!ui_->checkBox_optimize_on_all_changes->isChecked() && !componentsChanged) { + // just remove the link and update graph viewer + graphLinks_.erase(graph::findLink(graphLinks_, from, to)); + ui_->graphViewer->updateGraph(currentGraphNodes, graphLinks_, mapIds_, weights_); + updateLoopClosuresSlider(); + } + else { + this->updateGraphView(); + // graph would have been updated, re-highlight the nodes + ui_->graphViewer->highlightNode(ids_.at(ui_->horizontalSlider_A->value()), 0); + ui_->graphViewer->highlightNode(ids_.at(ui_->horizontalSlider_B->value()), 1); + } } - else + else if(ui_->checkBox_optimize_on_all_changes->isChecked()) // removed prior links would not change anything visually if we don't re-optimize { bool priorsIgnored = Parameters::defaultOptimizerPriorsIgnored(); Parameters::parse(ui_->parameters_toolbox->getParameters(), Parameters::kOptimizerPriorsIgnored(), priorsIgnored); - int indexA = ui_->horizontalSlider_A->value(); - int indexB = ui_->horizontalSlider_B->value(); + if(!priorsIgnored) { + regenerateGraphComponents(); this->updateGraphView(); } - if(ui_->horizontalSlider_A->value() != indexA) - ui_->horizontalSlider_A->setValue(indexA); - else - sliderAValueChanged(indexA); - if(ui_->horizontalSlider_B->value() != indexB) - ui_->horizontalSlider_B->setValue(indexB); - else - sliderBValueChanged(indexB); } } } @@ -9580,6 +10291,7 @@ void DatabaseViewer::updateLoopClosuresSlider(int from, int to) loopLinks_.clear(); std::multimap links = updateLinksWithModifications(links_); int position = ui_->horizontalSlider_loops->value(); + int orgSliderMaximum = ui_->horizontalSlider_loops->maximum(); std::multimap linksSortedByParents; for(std::multimap::iterator iter = links.begin(); iter!=links.end(); ++iter) { @@ -9623,13 +10335,16 @@ void DatabaseViewer::updateLoopClosuresSlider(int from, int to) ui_->horizontalSlider_loops->setMinimum(0); ui_->horizontalSlider_loops->setMaximum(loopLinks_.size()-1); ui_->horizontalSlider_loops->setEnabled(true); - if(position != ui_->horizontalSlider_loops->value()) + if(position>=0) { - ui_->horizontalSlider_loops->setValue(position); - } - else - { - this->updateConstraintView(loopLinks_.at(position)); + if(position != ui_->horizontalSlider_loops->value()) + { + ui_->horizontalSlider_loops->setValue(position); + } + else if(orgSliderMaximum != ui_->horizontalSlider_loops->maximum()) + { + this->updateConstraintView(loopLinks_.at(position)); + } } } else @@ -9641,6 +10356,11 @@ void DatabaseViewer::updateLoopClosuresSlider(int from, int to) } } +void DatabaseViewer::updateLoopClosuresSlider() +{ + updateLoopClosuresSlider(ids_.at(ui_->horizontalSlider_A->value()), ids_.at(ui_->horizontalSlider_B->value())); +} + void DatabaseViewer::notifyParametersChanged(const QStringList & parametersChanged) { bool updateStereo = false; diff --git a/guilib/src/ExportDialog.cpp b/guilib/src/ExportDialog.cpp index 2ffc38d2..b00e10d6 100644 --- a/guilib/src/ExportDialog.cpp +++ b/guilib/src/ExportDialog.cpp @@ -47,6 +47,7 @@ ExportDialog::ExportDialog(QWidget * parent) : connect(_ui->spinBox_ignored, SIGNAL(valueChanged(int)), this, SIGNAL(configChanged())); connect(_ui->doubleSpinBox_framerate, SIGNAL(valueChanged(double)), this, SIGNAL(configChanged())); connect(_ui->spinBox_session, SIGNAL(valueChanged(int)), this, SIGNAL(configChanged())); + connect(_ui->checkBox_selected_nodes, SIGNAL(stateChanged(int)), this, SIGNAL(configChanged())); connect(_ui->checkBox_rgb, SIGNAL(stateChanged(int)), this, SIGNAL(configChanged())); connect(_ui->checkBox_depth, SIGNAL(stateChanged(int)), this, SIGNAL(configChanged())); connect(_ui->checkBox_depth2d, SIGNAL(stateChanged(int)), this, SIGNAL(configChanged())); @@ -70,6 +71,8 @@ void ExportDialog::saveSettings(QSettings & settings, const QString & group) con settings.setValue("framesIgnored", this->framesIgnored()); settings.setValue("targetFramerate", this->targetFramerate()); settings.setValue("sessionExported", this->sessionExported()); + settings.setValue("currentComponentOnly", this->currentComponentOnly()); + settings.setValue("selectedNodesOnly", this->selectedNodesOnly()); settings.setValue("rgbExported", this->isRgbExported()); settings.setValue("depthExported", this->isDepthExported()); settings.setValue("depth2dExported", this->isDepth2dExported()); @@ -90,6 +93,8 @@ void ExportDialog::loadSettings(QSettings & settings, const QString & group) _ui->spinBox_ignored->setValue(settings.value("framesIgnored", this->framesIgnored()).toInt()); _ui->doubleSpinBox_framerate->setValue(settings.value("targetFramerate", this->targetFramerate()).toDouble()); _ui->spinBox_session->setValue(settings.value("sessionExported", this->sessionExported()).toInt()); + _ui->checkBox_current_component->setChecked(settings.value("currentComponentOnly", this->currentComponentOnly()).toBool()); + _ui->checkBox_selected_nodes->setChecked(settings.value("selectedNodesOnly", this->selectedNodesOnly()).toBool()); _ui->checkBox_rgb->setChecked(settings.value("rgbExported", this->isRgbExported()).toBool()); _ui->checkBox_depth->setChecked(settings.value("depthExported", this->isDepthExported()).toBool()); _ui->checkBox_depth2d->setChecked(settings.value("depth2dExported", this->isDepth2dExported()).toBool()); @@ -106,6 +111,8 @@ void ExportDialog::restoreDefaults() _ui->spinBox_ignored->setValue(0); _ui->doubleSpinBox_framerate->setValue(0); _ui->spinBox_session->setValue(-1); + _ui->checkBox_current_component->setChecked(false); + _ui->checkBox_selected_nodes->setChecked(false); _ui->checkBox_rgb->setChecked(true); _ui->checkBox_depth->setChecked(true); _ui->checkBox_depth2d->setChecked(true); @@ -142,6 +149,16 @@ int ExportDialog::sessionExported() const return _ui->spinBox_session->value(); } +bool ExportDialog::currentComponentOnly() const +{ + return _ui->checkBox_current_component->isChecked(); +} + +bool ExportDialog::selectedNodesOnly() const +{ + return _ui->checkBox_selected_nodes->isChecked(); +} + bool ExportDialog::isRgbExported() const { return _ui->checkBox_rgb->isChecked(); diff --git a/guilib/src/GraphViewer.cpp b/guilib/src/GraphViewer.cpp index c0a9a8de..21cf7b1c 100644 --- a/guilib/src/GraphViewer.cpp +++ b/guilib/src/GraphViewer.cpp @@ -342,6 +342,7 @@ GraphViewer::GraphViewer(QWidget * parent) : _orientationENU(false), _mouseTracking(false), _viewPlane(XY), + _interactionMode(HandMode), _ensureFrameVisible(true), _previousMousePos(-1,-1), _initialMousePos(-1,-1), @@ -388,6 +389,7 @@ void GraphViewer::setupGraphicsScene() _zoomOverlayItem->setVisible(false); _zoomOverlayItem->setAcceptedMouseButtons(Qt::NoButton); _zoomOverlayItem->setAcceptHoverEvents(false); + connect(this->scene(), &QGraphicsScene::selectionChanged, this, &GraphViewer::nodesSelected); _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)); @@ -664,6 +666,7 @@ void GraphViewer::updateGraph(const std::map & poses, item->setZValue(iter->first<0?21:20); item->setColor(color); item->setParentItem(_graphRoot); + item->setFlag(QGraphicsItem::ItemIsSelectable, iter->first>0 && _interactionMode == SelectionMode); item->show(); _nodeItems.insert(iter->first, item); ++iter; @@ -674,6 +677,7 @@ void GraphViewer::updateGraph(const std::map & poses, // NodeItem exists for the pose, copy data and increase both iterators UASSERT(iter->first == nter.key()); nter.value()->setColor(color); // reset color + nter.value()->setFlag(QGraphicsItem::ItemIsSelectable, iter->first > 0 && _interactionMode == SelectionMode); nter.value()->setToolTipInfo(QString()); nter.value()->setZValue(iter->first<0?21:20); nter.value()->setPose(iter->second, _viewPlane); @@ -937,6 +941,7 @@ void GraphViewer::updateGTGraph(const std::map & poses) item->setZValue(20); item->setColor(_gtPathColor); item->setParentItem(_gtGraphRoot); + item->setFlag(QGraphicsItem::ItemIsSelectable, false); item->setVisible(_nodeVisible); _gtNodeItems.insert(iter->first, item); } @@ -1071,6 +1076,7 @@ void GraphViewer::updateGPSGraph( item->setZValue(20); item->setColor(_gpsPathColor); item->setParentItem(_gpsGraphRoot); + item->setFlag(QGraphicsItem::ItemIsSelectable, false); item->setVisible(_nodeVisible); _gpsNodeItems.insert(iter->first, item); } @@ -1453,6 +1459,48 @@ void GraphViewer::highlightNode(int nodeId, int highlightIndex) } } +std::set GraphViewer::getSelectedNodeIds() +{ + std::set ids; + for(QMap::iterator iter = _nodeItems.begin(); iter != _nodeItems.end(); iter++) + { + if(iter.value() && iter.value()->isSelected()) + ids.insert(iter.key()); + } + return ids; +} + +void GraphViewer::selectNodesFromIds(const std::set & ids) +{ + this->scene()->blockSignals(true); + + for (QMap::iterator iter = _nodeItems.begin(); iter != _nodeItems.end(); ++iter) + { + if(iter.value() && iter.key()>0) + iter.value()->setSelected(ids.find(iter.key()) != ids.end()); + } + + this->scene()->blockSignals(false); +} + +void GraphViewer::setInteractionMode(InteractionMode mode) +{ + if(_interactionMode == mode) + { + return; + } + _interactionMode = mode; + this->setDragMode(mode == HandMode ? QGraphicsView::ScrollHandDrag : QGraphicsView::RubberBandDrag); + this->setRubberBandSelectionMode(Qt::IntersectsItemShape); + for(QMap::iterator iter = _nodeItems.begin(); iter != _nodeItems.end(); iter++) + { + if(iter.key()>0 && iter.value()) + { + iter.value()->setFlag(QGraphicsItem::ItemIsSelectable, mode == SelectionMode); + } + } +} + void GraphViewer::clearGraph() { qDeleteAll(_nodeItems); @@ -1566,6 +1614,7 @@ void GraphViewer::saveSettings(QSettings & settings, const QString & group) cons settings.setValue("odom_cache_overlay", this->isOdomCacheOverlayVisible()); settings.setValue("orientation_ENU", this->isOrientationENU()); settings.setValue("view_plane", (int)this->getViewPlane()); + settings.setValue("interaction_mode", (int)this->getInteractionMode()); settings.setValue("ensure_frame_visible", (int)this->isEnsureFrameVisible()); settings.setValue("fast_zoom_min_nodes", this->getFastZoomMinNodes()); if(!group.isEmpty()) @@ -1618,6 +1667,7 @@ void GraphViewer::loadSettings(QSettings & settings, const QString & group) this->setOdomCacheOverlayVisible(settings.value("odom_cache_overlay", this->isOdomCacheOverlayVisible()).toBool()); this->setOrientationENU(settings.value("orientation_ENU", this->isOrientationENU()).toBool()); this->setViewPlane((ViewPlane)settings.value("view_plane", (int)this->getViewPlane()).toInt()); + this->setInteractionMode((InteractionMode)settings.value("interaction_mode", (int)this->getInteractionMode()).toInt()); this->setEnsureFrameVisible(settings.value("ensure_frame_visible", this->isEnsureFrameVisible()).toBool()); this->setFastZoomMinNodes(settings.value("fast_zoom_min_nodes", this->getFastZoomMinNodes()).toInt()); if(!group.isEmpty()) @@ -2087,6 +2137,7 @@ void GraphViewer::restoreDefaults() setLocalPathVisible(true); setGtGraphVisible(true); setFastZoomMinNodes(0); + setInteractionMode(HandMode); } void GraphViewer::wheelEvent(QWheelEvent * event) @@ -2123,7 +2174,7 @@ void GraphViewer::mouseMoveEvent(QMouseEvent * event) { QToolTip::hideText(); } - if (event->modifiers() & (Qt::ShiftModifier | Qt::ControlModifier) && event->buttons() & Qt::LeftButton) { + if (_interactionMode == HandMode && event->modifiers() & (Qt::ShiftModifier | Qt::ControlModifier) && event->buttons() & Qt::LeftButton) { // same modifiers than 3D view, change zoom if(_previousMousePos.y()!=0) { qreal factor = (event->pos().y() - _previousMousePos.y() > 0) ? 0.98 : 1.02; @@ -2200,6 +2251,25 @@ QIcon createIcon(const QColor & color) return QIcon(pixmap); } +QPointF GraphViewer::getNodeScenePosition(int id) const +{ + NodeItem * item = _nodeItems.value(id, 0); + if(item) + { + return item->sceneBoundingRect().center(); + } + return QPointF(); +} + +void GraphViewer::centerOnNode(int id) +{ + QPointF center = this->getNodeScenePosition(id); + if(!center.isNull()) + { + this->centerOn(center); + } +} + void GraphViewer::contextMenuEvent(QContextMenuEvent * event) { QMenu menu; diff --git a/guilib/src/LinkRefiningDialog.cpp b/guilib/src/LinkRefiningDialog.cpp index 5763aebf..7181344b 100644 --- a/guilib/src/LinkRefiningDialog.cpp +++ b/guilib/src/LinkRefiningDialog.cpp @@ -87,6 +87,20 @@ void LinkRefiningDialog::setMinMax( restoreDefaults(); } } +void LinkRefiningDialog::setRangesEnabled(bool selectedNodes, bool currentComponent) +{ + ui_->radioButton_selected_nodes->setEnabled(selectedNodes); + if(!selectedNodes && ui_->radioButton_selected_nodes->isChecked()) + { + ui_->radioButton_nodes->setChecked(true); + } + + ui_->radioButton_current_component->setEnabled(currentComponent); + if(!currentComponent && ui_->radioButton_current_component->isChecked()) + { + ui_->radioButton_nodes->setChecked(true); + } +} Link::Type LinkRefiningDialog::getLinkType() const { @@ -103,6 +117,16 @@ void LinkRefiningDialog::getIntraInterSessions(bool & intra, bool & inter) const inter = ui_->comboBox_link_inter_intra->currentIndex() == 0 || ui_->comboBox_link_inter_intra->currentIndex() == 2; } +bool LinkRefiningDialog::isRangeByCurrentComponent() const +{ + return ui_->radioButton_current_component->isChecked(); +} + +bool LinkRefiningDialog::isRangeBySelectedNodeIds() const +{ + return ui_->radioButton_selected_nodes->isChecked(); +} + bool LinkRefiningDialog::isRangeByNodeId() const { return ui_->radioButton_nodes->isChecked(); diff --git a/guilib/src/ui/DatabaseViewer.ui b/guilib/src/ui/DatabaseViewer.ui index cc7918fb..8b4b95de 100644 --- a/guilib/src/ui/DatabaseViewer.ui +++ b/guilib/src/ui/DatabaseViewer.ui @@ -61,7 +61,7 @@ 0 0 - 364 + 344 389 @@ -392,7 +392,7 @@ 0 0 - 364 + 343 389 @@ -752,10 +752,19 @@ - + + + Click to locate in Graph View + idA + + Qt::ToolButtonTextOnly + + + true + @@ -835,10 +844,19 @@ - + + + Click to locate in Graph View + idB + + Qt::ToolButtonTextOnly + + + true + @@ -914,10 +932,10 @@ - + @@ -937,6 +955,7 @@ + @@ -948,6 +967,7 @@ + @@ -1198,7 +1218,17 @@ - + + + + + Re-optimize the graph right away after using any of these actions + + + + + + @@ -1217,20 +1247,44 @@ - - - Add + + + QFrame::StyledPanel - - - - - - Enable "Add" for all nodes (even not in graph) - - - + + QFrame::Raised + + + 0 + + + 0 + + + 6 + + + 0 + + + + + Add + + + + + + + Enable "Add" for all nodes (even not in graph) + + + + + + + @@ -1247,7 +1301,7 @@ - 0 + 13 20 @@ -1267,9 +1321,74 @@ + + + + + + Interaction mode: + + + + + + + Hand / Move + + + true + + + + + + + Selection + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + + + Graph component + + + + + + + true + + + true + + + false + + + + + @@ -1292,6 +1411,9 @@ + + 6 + @@ -1337,7 +1459,13 @@ - + + + + + + 6 + @@ -1380,10 +1508,23 @@ + + + + After any graph changes, re-zoom to fit the whole map in the view + + + Fit in view + + + + + 6 + @@ -1467,6 +1608,9 @@ + + 6 + @@ -1722,7 +1866,7 @@ - 2 + 0 @@ -1737,8 +1881,25 @@ Graph optimization - - + + + + Ignore pose correction + + + + + + + + + + false + + + + + @@ -1754,16 +1915,6 @@ - - - - - - - false - - - @@ -1771,15 +1922,8 @@ - - - - Ignore pose correction - - - - - + + @@ -1788,13 +1932,10 @@ - - + + - - - - false + Ignore landmarks @@ -1811,8 +1952,15 @@ - - + + + + Ignore local loop closures (space) + + + + + @@ -1828,20 +1976,6 @@ - - - - Ignore local loop closures (space) - - - - - - - Scale - - - @@ -1861,15 +1995,25 @@ - - + + - Ignore landmarks + + + + false - - + + + + Scale + + + + + @@ -2604,7 +2748,7 @@ 0 - -175 + 0 403 319 @@ -2613,54 +2757,13 @@ Detect more loop closures - - + + - Angle + Iterations - - - - Inter-session - - - - - - - - - - - - - - Radius Max - - - - - - - Intra-session - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - @@ -2680,10 +2783,29 @@ - - - - Radius Min + + + + -1 + + + 9999 + + + -1 + + + + + + + 1 + + + 9999 + + + 10 @@ -2700,6 +2822,39 @@ + + + + + + + + + + + m + + + 2 + + + 10.000000000000000 + + + 0.100000000000000 + + + 1.000000000000000 + + + + + + + Inter-session + + + @@ -2707,20 +2862,23 @@ - - - - Iterations + + + + Qt::Vertical - + + + 20 + 40 + + + - - + + - - - - true + Radius Max @@ -2750,6 +2908,16 @@ + + + + + + + true + + + @@ -2760,38 +2928,6 @@ - - - - m - - - 2 - - - 10.000000000000000 - - - 0.100000000000000 - - - 1.000000000000000 - - - - - - - -1 - - - 9999 - - - -1 - - - @@ -2799,16 +2935,24 @@ - - - - 1 + + + + Angle - - 9999 + + + + + + Intra-session - - 10 + + + + + + Radius Min @@ -3021,7 +3165,7 @@ - 40 + 0 20 @@ -3391,7 +3535,7 @@ - Export 3D map (*.ply *.pcd) ... + Export 3D map (*.ply *.pcd)... @@ -3557,6 +3701,9 @@ Update optimized mesh... + + Available only for the Main component + @@ -3585,6 +3732,9 @@ Regenerate optimized 2D map... + + Available only for the Main component + @@ -3596,6 +3746,16 @@ Update all landmark covariances... + + + Merge components... + + + + + Clear all selected nodes + + @@ -3620,6 +3780,11 @@
../include/rtabmap/gui/ParametersToolBox.h
1
+ + QTabBar + QWidget +
qtabbar.h
+
diff --git a/guilib/src/ui/exportDialog.ui b/guilib/src/ui/exportDialog.ui index 1f034c37..1987896c 100644 --- a/guilib/src/ui/exportDialog.ui +++ b/guilib/src/ui/exportDialog.ui @@ -6,8 +6,8 @@ 0 0 - 382 - 331 + 333 + 520 @@ -15,178 +15,199 @@ - - - - - - - - - - - - Frames ignored - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - Hz - - - 1 - - - - - - - Target frame rate - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - - - -1 - - - 999 - - - -1 - - - - - - - Mapping session (-1 = all) - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - RGB images - - - true + + + Selection + + + + + + + + -1 + + + 999 + + + -1 + + + + + + + Mapping session (-1 = all) + + + + + + + + + + false + + + + + + + Selected nodes + + + true + + + + + + + Current component only + + + true + + + + + + + + + + false + + + + - - - Depth images - - - true + + + Filtering + + + + + + + + + + + + Nodes ignored + + + + + + + Hz + + + 1 + + + + + + + Target frame rate + + + + - - - Laser scans - - - true + + + Data + + + + + RGB images + + + true + + + + + + + Depth images + + + true + + + + + + + Laser scans + + + true + + + + + + + Odometry + + + true + + + + + + + User data (planned goals) + + + false + + + + - - - Odometry - - - true + + + Output Path + + + + + true + + + + + + + ... + + + + - - - - User data (planned goals) - - - false - - - - - - - - - true - - - - - - - ... - - - - - diff --git a/guilib/src/ui/linkRefiningDialog.ui b/guilib/src/ui/linkRefiningDialog.ui index 3bc36249..e6d843aa 100644 --- a/guilib/src/ui/linkRefiningDialog.ui +++ b/guilib/src/ui/linkRefiningDialog.ui @@ -6,8 +6,8 @@ 0 0 - 333 - 286 + 369 + 344 @@ -51,28 +51,21 @@ Range - + From - - + + - To + Maps: - - - - Nodes: - - - - + 1 @@ -82,7 +75,7 @@ - + 1 @@ -92,14 +85,21 @@ - - + + - Maps: + Nodes selected - + + + + To + + + + 0 @@ -112,7 +112,7 @@ - + 0 @@ -125,6 +125,20 @@ + + + + Nodes: + + + + + + + Current component + + +
From bcdb4b454683efc651a36f044cc85d8f2f5f4ac3 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Sun, 2 Aug 2026 20:54:07 -0700 Subject: [PATCH 15/51] hotfix opencv5 compressed data compatibility --- corelib/src/Compression.cpp | 45 ++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/corelib/src/Compression.cpp b/corelib/src/Compression.cpp index 5f9fb46c..b346817e 100644 --- a/corelib/src/Compression.cpp +++ b/corelib/src/Compression.cpp @@ -34,6 +34,41 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. namespace rtabmap { +namespace { + +// The compressed blob trailer stores the cv::Mat type as a raw int, and the +// database keeps those blobs forever. OpenCV 5 changed CV_CN_SHIFT from 3 to 5, +// so the very same type has a different numeric value across major versions +// (CV_32FC2 is 13 under OpenCV 4 but 37 under OpenCV 5). Reading an OpenCV 4 +// database with an OpenCV 5 build would decode 13 as a 1-channel Mat of a +// nonsense depth. +// +// Persist the OpenCV 4 encoding in both cases: existing databases stay +// readable, and databases written by an OpenCV 5 build stay readable by +// OpenCV 4 builds. Under OpenCV 4 both helpers are the identity. +const int kSerializedCnShift = 3; +const int kSerializedDepthMask = (1 << kSerializedCnShift) - 1; + +int serializeMatType(int type) +{ + const int depth = CV_MAT_DEPTH(type); + // Only the 8 original depths (CV_8U..CV_16F) fit the on-disk encoding; + // rtabmap never persists the types OpenCV 5 added beyond them. + UASSERT_MSG(depth <= kSerializedDepthMask, + uFormat("Cannot serialize cv::Mat depth %d (type %d): the database " + "format only supports depths 0-%d.", + depth, type, kSerializedDepthMask).c_str()); + return depth + ((CV_MAT_CN(type) - 1) << kSerializedCnShift); +} + +int deserializeMatType(int serializedType) +{ + return CV_MAKETYPE(serializedType & kSerializedDepthMask, + ((serializedType >> kSerializedCnShift) & 511) + 1); +} + +} // namespace + // format : ".jpg" ".png" ".rvl" "" (empty is general) CompressionThread::CompressionThread(const cv::Mat & mat, const std::string & format) : uncompressedData_(mat), @@ -222,7 +257,7 @@ std::vector compressData(const cv::Mat & data) bytes.resize(destLen+3*sizeof(int)); *((int*)&bytes[destLen]) = data.rows; *((int*)&bytes[destLen+sizeof(int)]) = data.cols; - *((int*)&bytes[destLen+2*sizeof(int)]) = data.type(); + *((int*)&bytes[destLen+2*sizeof(int)]) = serializeMatType(data.type()); if(errCode == Z_MEM_ERROR) { @@ -252,7 +287,7 @@ cv::Mat compressData2(const cv::Mat & data) bytes = cv::Mat(bytes, cv::Rect(0,0, destLen+3*sizeof(int), 1)); *((int*)&bytes.data[destLen]) = data.rows; *((int*)&bytes.data[destLen+sizeof(int)]) = data.cols; - *((int*)&bytes.data[destLen+2*sizeof(int)]) = data.type(); + *((int*)&bytes.data[destLen+2*sizeof(int)]) = serializeMatType(data.type()); if(errCode == Z_MEM_ERROR) { @@ -285,7 +320,7 @@ cv::Mat uncompressData(const unsigned char * bytes, unsigned long size) //last 3 int elements are matrix size and type int height = *((int*)&bytes[size-3*sizeof(int)]); int width = *((int*)&bytes[size-2*sizeof(int)]); - int type = *((int*)&bytes[size-1*sizeof(int)]); + int type = deserializeMatType(*((int*)&bytes[size-1*sizeof(int)])); data = cv::Mat(height, width, type); uLongf totalUncompressed = uLongf(data.total())*uLongf(data.elemSize()); @@ -331,6 +366,10 @@ std::string uncompressString(const cv::Mat & bytes) std::string compressedDepthFormat(const cv::Mat & bytes) { + if(bytes.empty()) + { + return std::string(); + } return compressedDepthFormat(bytes.data, bytes.rows * bytes.cols * bytes.elemSize()); } std::string compressedDepthFormat(const std::vector & bytes) From ee49beaf4f9c970f54300786bd068ca541983d6e Mon Sep 17 00:00:00 2001 From: matlabbe Date: Thu, 6 Aug 2026 13:32:20 -0700 Subject: [PATCH 16/51] Adding doc and tests (#1492) * added doc and tests for util2d.h * updated cmake-ros ci * Added util3d.h doc and tests * util3d_transforms.h: Added doc and tests * util3d_filtering.h: started doc and test * util3d_filtering.h: more tests and doc * Added more doc/tests * finished util3d_filtering doc and tests * added test for util2d::depthBleedingFiltering * Added util3d_registration tests * Added util3d_features.h doc/tests * added doc/tests for util3d_correspondences.h * added doc/gtest for util3d_mapping.h (missing hpp functions) * finished testing util3d_mapping.hpp * Added util3d_motion_estimation.h tests (2D->3D done) * finished util3d_motion_estimation.h tests * minimal util3d_surface.h * Added Transform and VisualWord tests * Added doc for CameraModel and StereoCameraModel * Added more logs in ros ci * Passing tests on fical * improved all devcontainer * added devcontainer kilted, fixed source setup.bash, removed ldconfig in ros-cmake workflow * cleanup * source ros * Added utilite tests * Added testing to appveyor, github actions cancellable on re-commit on same branch * appveyor testing without all targets * appveyor: specifying ALL_BUILD target * Fixed Util2dTest.NMSImageBoundsRespected test * Fixing PCL Indices error on old pcl * Added VWDictionary tests and doc. Fixed LSH not working (fix from https://github.com/flann-lib/flann/pull/472 * fixing some appveyor CI errors, added test to check dictionary serialization against all type * Added StereoDense, StereoBM and StereoSGBM doc and tests * Added Stereo tests * Added CameraModel and StereoCameraModel tests * Added doc and test for Statistics * Added doc/tests for Signature * Added doc/test for SensorEvent, added doc for SensorCaptureInfo * Added doc to SensorData * Added SensorData tests * Added SensorCapture and SensorCaptureThread doc and tests * fixed sensordata test * updated SSC test and doc * Added doc and tests for BayesFilter class * Enabled testing on mac, updated windows testing like on linux * added test_link * fixed unresolved on windows * fixed ThreadHandle error on macos ci * Added GPS and GeodeticCoords tests * Added tests for compression * Added Odometry tests (base class only) * Added DBDriver tests * Added coverage report * uniformized test names * fixing concurancy and coverage ci * dont built tools, examples and app for coverage build * fixed report tool rebuilt without qt compilation error * updated coverage option * updated coverage config * added doc CI job * fixing windows and mac ci errors * Added DBDriverSqlite3 tests * Added IMU tests * Added Graph tests * fixing flaky macos test * Added IMUThread and IMUFilter tests * Added Landmarks tests * Added LASWriter tests * fixing seed flaky test * fixing flaky macos timing tests * Added LocalGrid tests * Added LocalGridMaker tests * fixing ci errors * Added GlobalMap tests * Added doc for EnvSensor * Added Features2D tests * Added Registration tests * Added RegistrationVis tests * Added doc for Rtabmap and Memory classes * Added Memory and Rtabmap tests * making some tests less flaky * lcov 1.14 support * updated compatible tool arguments * Added integration tests (RGB-D, Stereo, Lidar2d, Lidar3d) * More octomap checks * Refactored how/when python interpretor is created to simplify library usage * Added python tests * fixed some flaky tests * suppressed some third party related warnings * fixed ceres tests * more flaky fixes * Fixing tests without libpointmatcher * Added RANSAC rejection filter to PCL ICP * fixing multi platform flakiness * Added test to detect regression * Fixing windows pcl link error * fixed some macos flakiness * bigger 2D2D registration error on opencv 4.6.0 * flakiness * fixing flaky tests on windows and mac * flaky thread test on slow mac VM * windows slow test * fixing more ci erros * fxing temp dir on windows * Added Optimizer tests and discovered some bugs (fixed) * fixing flaky tests in mac and windows * Added Optimizer doc * Added GTSAM BA, updated Ceres to use g2o ba parameters. Renamed g2o's ba related parameters to Optimizer group and used by both gtsam and ceres. * fixing build without gtsam * fixing home dir * fixing python ci isssues * Added multicam ba tests * Added Ceres multicam BA support * Aligned BundleAdjustment parameters with Optimizer/Strategy to avoid confusion in the code * Added BA integration test * Added robust graph optimization integration test * Added loop3it test * Added stereo20Hz test * Added smartfactor gtsam * Fixed bugged check and warn if python didn't return any descriptors * Fixing gtsam version build issues * fixing tilt on windows ci * loosing ceres integration test for ci * mac ci flakiness * updating missing param in gui * updating test bound for mac * added appearance-based tests, set min gftt quality to quality level * testing more stuff * improving features2d tests * ci flakiness * fixing flaky ci * ci fixes * flaky fixes * Added RegistrationIcp tests * Added icp integration test with real-worl corridor like env * intermediate nodes * fixing enum * Updated test to catch #1714 * Fixed 2d corridor failing on pcl * flaky pnp test * flaky brisk test * Set rtabmap_integration test as long * updating loop closure test * flaky ci tests * TEsting roundtrip g2o/toro save/load * loosing test bound * fixed cuda capable checks * flaky tests * Debugging test hanging * more debugging stuff * updating limit * windows: disabled cuda on ci to avoid incompatible driver issue. Fixing a bad test mem allocation * trying fixing cuda hanging issue * fixing ci flakyness * flaky tests * Updated BOW flaky tests by checking min precision/recall instead of recall@100precision. Fixed signature test * CameraModel::load() test initRectificationMap param * test dbdriver load dictionary idsOnly * Memory: test keepLinkedInDb param * added dummyDictionary tests * test intermediate nodes count * Added MarkerDetector tests * reverted breaking change of UMutex and USemaphore * Features2d: fixed compiltion warnings with clang about override * clang warnings * fixing test build with pcl 1.8 * g2o and gtsam build errors on android * opencv5 test fixes * disabled testing for ios and android builds * normalized endline characters for easier diff * added LF CRLF rule * bump 0.23.10. fixing doc version * Publish rtabmap website doc from ci * fixing MSCVC build error * macos icp flaky test * fixing ceres macos test bound * ficing more flaky tests * fixing opencv5 related test errors. Also fixed an actual bug in ENU_WGS84ToGeocentric_WGS84() * added comment about mrpt change * removed rosdoc2 (will add it for rtabmap_ros later) * fixing website style * updated download links * locally deployable website with api * sweep doxygen issues * improved/revised doxygen main pages * removed examples empty page * Updated doxygen style * more concise doxygen groups * added api link on main readme * fixing utilite test error * fixing CommonFilteringGroundNormalsUp test * updated precisionRecall test bounds for Freak and brief descriptors * fixing scale check in ba tests * disabled tests on windows cuda build (missing dlls amd runner cannot test cuda anyway) * ceres: missing suitesparse dep in windows ci * adjusting recall thr for fast/freak * ficing more flaky tests * fixing flaky tests * disabled coverage in ros ci * Enable integration tests for ros ci jobs * loosing up some threshold for failing tests * trigger cache * fixing test data in ros ci. Updated flaky test for mac * slaking some test limit * Fixed rtabmap-detectMoreLoopClosures inverted output value * loosing up sift recall on mac * optimizer re-ordered distribution for reproducible results (mac g2o) * macos dump test crash log * combining all tests to save time on shared library reload. Also fixed Logs with missing arguments. * Added ENABLE_FORMAT_ERRORS cmake option * do test only one time * fixed all format warnings * format security android build errors * less verbose tests * updated ImuUThread test * fixed a log * Fixed libpointmatcher 2d normals eigen issue * Fixing libpointmatcher conversion issues * fixing libpointmatcher test on windows ci * cleanup comments, relax some test thr * disabled sequoia-intel ci build (too flaky, would need extensive testing directly on that machine) --- .devcontainer/focal/Dockerfile | 4 +- .devcontainer/jammy/Dockerfile | 4 +- .../latest-deps-from-source/Dockerfile | 1 + .devcontainer/noble-kilted/Dockerfile | 20 + .devcontainer/noble-kilted/devcontainer.json | 17 + .devcontainer/noble/Dockerfile | 4 +- .devcontainer/rolling/Dockerfile | 2 +- .gitattributes | 53 + .../install-windows-cuda-deps/action.yml | 2 +- .../actions/install-windows-deps/action.yml | 2 +- .github/workflows/cmake-linux.yml | 35 +- .github/workflows/cmake-macos.yml | 73 +- .github/workflows/cmake-ros.yml | 45 +- .github/workflows/cmake-windows.yml | 61 +- .github/workflows/coverage.yml | 125 + .github/workflows/docs-preview-cleanup.yml | 54 + .github/workflows/docs.yml | 211 + .gitignore | 14 + CMakeLists.txt | 69 +- Doxyfile.in | 2824 ++++++++++++ README.md | 1 + app/android/jni/CameraTango.cpp | 2 +- app/android/jni/RTABMapApp.cpp | 2 +- cmake_modules/RTABMapDocumentation.cmake | 112 + corelib/CMakeLists.txt | 6 +- corelib/include/rtabmap/core/BayesFilter.h | 135 +- corelib/include/rtabmap/core/CameraModel.h | 488 +- corelib/include/rtabmap/core/Compression.h | 65 +- corelib/include/rtabmap/core/DBDriver.h | 168 +- .../include/rtabmap/core/DBDriverSqlite3.h | 105 +- corelib/include/rtabmap/core/EnvSensor.h | 67 +- corelib/include/rtabmap/core/Features2d.h | 215 +- corelib/include/rtabmap/core/GPS.h | 30 + corelib/include/rtabmap/core/GeodeticCoords.h | 34 + corelib/include/rtabmap/core/GlobalMap.h | 54 +- corelib/include/rtabmap/core/Graph.h | 518 ++- corelib/include/rtabmap/core/IMU.h | 122 +- corelib/include/rtabmap/core/IMUFilter.h | 89 +- corelib/include/rtabmap/core/IMUThread.h | 47 +- corelib/include/rtabmap/core/LASWriter.h | 49 +- corelib/include/rtabmap/core/Landmark.h | 106 +- corelib/include/rtabmap/core/LaserScan.h | 112 +- corelib/include/rtabmap/core/Link.h | 101 +- corelib/include/rtabmap/core/LocalGrid.h | 65 +- corelib/include/rtabmap/core/LocalGridMaker.h | 75 +- corelib/include/rtabmap/core/MarkerDetector.h | 10 - corelib/include/rtabmap/core/Memory.h | 535 ++- corelib/include/rtabmap/core/Odometry.h | 103 +- corelib/include/rtabmap/core/OdometryEvent.h | 49 + corelib/include/rtabmap/core/OdometryInfo.h | 96 +- corelib/include/rtabmap/core/OdometryThread.h | 56 +- corelib/include/rtabmap/core/Optimizer.h | 213 +- corelib/include/rtabmap/core/Parameters.h | 63 +- corelib/include/rtabmap/core/ParticleFilter.h | 2 +- .../include/rtabmap/core/PythonInterface.h | 16 +- corelib/include/rtabmap/core/Registration.h | 57 +- .../include/rtabmap/core/RegistrationIcp.h | 28 +- .../include/rtabmap/core/RegistrationInfo.h | 61 +- .../include/rtabmap/core/RegistrationVis.h | 27 +- corelib/include/rtabmap/core/Rtabmap.h | 795 +++- corelib/include/rtabmap/core/RtabmapEvent.h | 49 +- corelib/include/rtabmap/core/RtabmapThread.h | 106 +- corelib/include/rtabmap/core/SensorCapture.h | 207 +- .../include/rtabmap/core/SensorCaptureInfo.h | 89 +- .../rtabmap/core/SensorCaptureThread.h | 459 +- corelib/include/rtabmap/core/SensorData.h | 772 +++- corelib/include/rtabmap/core/SensorEvent.h | 127 +- corelib/include/rtabmap/core/Signature.h | 529 ++- corelib/include/rtabmap/core/Statistics.h | 516 ++- corelib/include/rtabmap/core/Stereo.h | 248 +- .../include/rtabmap/core/StereoCameraModel.h | 451 +- corelib/include/rtabmap/core/StereoDense.h | 77 +- corelib/include/rtabmap/core/Transform.h | 211 +- corelib/include/rtabmap/core/VWDictionary.h | 433 +- corelib/include/rtabmap/core/VisualWord.h | 81 +- .../rtabmap/core/camera/CameraOpenni.h | 4 +- .../rtabmap/core/impl/LocalMapMaker.hpp | 12 + .../rtabmap/core/impl/util3d_mapping.hpp | 7 +- .../rtabmap/core/optimizer/OptimizerCeres.h | 17 +- .../rtabmap/core/optimizer/OptimizerG2O.h | 1 + .../rtabmap/core/optimizer/OptimizerGTSAM.h | 13 + .../include/rtabmap/core/stereo/StereoBM.h | 80 +- .../include/rtabmap/core/stereo/StereoSGBM.h | 80 +- corelib/include/rtabmap/core/util2d.h | 638 ++- corelib/include/rtabmap/core/util3d.h | 1055 ++++- .../rtabmap/core/util3d_correspondences.h | 220 +- .../include/rtabmap/core/util3d_features.h | 113 +- .../include/rtabmap/core/util3d_filtering.h | 1217 +++-- corelib/include/rtabmap/core/util3d_mapping.h | 325 +- .../rtabmap/core/util3d_motion_estimation.h | 191 +- .../rtabmap/core/util3d_registration.h | 163 +- corelib/include/rtabmap/core/util3d_surface.h | 57 +- .../include/rtabmap/core/util3d_transforms.h | 76 + corelib/src/BayesFilter.cpp | 14 +- corelib/src/CMakeLists.txt | 44 +- corelib/src/CameraModel.cpp | 2 +- corelib/src/DBDriver.cpp | 6 +- corelib/src/DBDriverSqlite3.cpp | 27 +- corelib/src/DBReader.cpp | 12 +- corelib/src/EpipolarGeometry.cpp | 4 +- corelib/src/Features2d.cpp | 175 +- corelib/src/FlannIndex.cpp | 38 +- corelib/src/GeodeticCoords.cpp | 48 +- corelib/src/Graph.cpp | 73 +- corelib/src/IMU.cpp | 10 +- corelib/src/IMUThread.cpp | 2 +- corelib/src/LASWriter.cpp | 3 +- corelib/src/LaserScan.cpp | 16 + corelib/src/MarkerDetector.cpp | 31 +- corelib/src/Memory.cpp | 62 +- corelib/src/Optimizer.cpp | 4 +- corelib/src/Parameters.cpp | 7 + corelib/src/Recovery.cpp | 2 +- corelib/src/RegistrationIcp.cpp | 259 +- corelib/src/RegistrationVis.cpp | 39 +- corelib/src/Rtabmap.cpp | 153 +- corelib/src/SensorData.cpp | 68 +- corelib/src/Stereo.cpp | 14 + corelib/src/StereoCameraModel.cpp | 2 +- corelib/src/Transform.cpp | 30 +- corelib/src/VWDictionary.cpp | 89 +- corelib/src/VisualWord.cpp | 1 - corelib/src/camera/CameraImages.cpp | 2 +- corelib/src/camera/CameraK4A.cpp | 2 +- corelib/src/camera/CameraRealSense2.cpp | 4 +- corelib/src/camera/CameraStereoZed.cpp | 4 +- .../clams/discrete_depth_distortion_model.cpp | 12 +- corelib/src/global_map/OctoMap.cpp | 2 +- corelib/src/icp/cccorelib.h | 17 +- corelib/src/icp/libpointmatcher.h | 95 +- corelib/src/odometry/OdometryF2F.cpp | 2 +- corelib/src/odometry/OdometryF2M.cpp | 10 +- corelib/src/opencv/ORBextractor.cc | 13 +- corelib/src/optimizer/OptimizerCeres.cpp | 443 +- corelib/src/optimizer/OptimizerG2O.cpp | 52 +- corelib/src/optimizer/OptimizerGTSAM.cpp | 536 +++ .../ceres/bundle/between_cameras_error.h | 114 + .../ceres/bundle/planar_constraint_error.h | 68 + .../snavely_stereo_reprojection_error.h | 104 + corelib/src/optimizer/g2o/edge_sbacam_prior.h | 8 +- .../src/optimizer/gtsam/PlanarBodyZFactor.h | 59 + corelib/src/optimizer/gtsam/XYZFactor.h | 4 + .../vertigo/gtsam/betweenFactorSwitchable.h | 13 +- .../pcl18/surface/impl/texture_mapping.hpp | 6 +- corelib/src/python/PyDescriptor.cpp | 24 +- corelib/src/python/PyDescriptor.h | 3 +- corelib/src/python/PyDetector.cpp | 14 +- corelib/src/python/PyDetector.h | 8 +- corelib/src/python/PyMatcher.cpp | 18 +- corelib/src/python/PyMatcher.h | 3 +- corelib/src/python/PythonInterface.cpp | 60 +- .../src/python/rtabmap_trace_superpoint.py | 51 +- corelib/src/rtflann/util/result_set.h | 7 +- .../SuperpointRpautrat.cpp | 2 +- .../superpoint_to_torchscript.py | 31 +- corelib/src/util2d.cpp | 148 +- corelib/src/util3d.cpp | 119 +- corelib/src/util3d_correspondences.cpp | 48 +- corelib/src/util3d_features.cpp | 27 +- corelib/src/util3d_filtering.cpp | 365 +- corelib/src/util3d_mapping.cpp | 42 +- corelib/src/util3d_motion_estimation.cpp | 67 +- corelib/src/util3d_registration.cpp | 115 +- corelib/src/util3d_surface.cpp | 151 +- corelib/test/CMakeLists.txt | 154 + corelib/test/TestUtils.h | 48 + corelib/test/test_bayesfilter.cpp | 992 ++++ corelib/test/test_cameramodel.cpp | 841 ++++ corelib/test/test_compression.cpp | 185 + corelib/test/test_dbdriver.cpp | 1243 ++++++ corelib/test/test_dbdriversqlite3.cpp | 176 + corelib/test/test_features2d.cpp | 632 +++ corelib/test/test_geodeticcoords.cpp | 151 + corelib/test/test_globalmap.cpp | 220 + corelib/test/test_gps.cpp | 77 + corelib/test/test_graph.cpp | 1262 ++++++ corelib/test/test_imu.cpp | 156 + corelib/test/test_imufilter.cpp | 174 + corelib/test/test_imuthread.cpp | 362 ++ corelib/test/test_landmark.cpp | 95 + corelib/test/test_laswriter.cpp | 174 + corelib/test/test_link.cpp | 197 + corelib/test/test_localgrid.cpp | 152 + corelib/test/test_localgridmaker.cpp | 1111 +++++ corelib/test/test_markerdetector.cpp | 493 ++ corelib/test/test_memory.cpp | 3754 ++++++++++++++++ corelib/test/test_odometry.cpp | 181 + corelib/test/test_optimizer.cpp | 2889 ++++++++++++ corelib/test/test_pydescriptor.cpp | 200 + corelib/test/test_pydetector.cpp | 328 ++ corelib/test/test_pymatcher.cpp | 234 + corelib/test/test_python_interface.cpp | 100 + corelib/test/test_registration.cpp | 266 ++ corelib/test/test_registrationicp.cpp | 1473 ++++++ corelib/test/test_registrationvis.cpp | 607 +++ corelib/test/test_rtabmap.cpp | 3974 +++++++++++++++++ corelib/test/test_rtabmap_integration.cpp | 2435 ++++++++++ corelib/test/test_sensorcapture.cpp | 447 ++ corelib/test/test_sensorcaptureinfo.cpp | 231 + corelib/test/test_sensorcapturethread.cpp | 572 +++ corelib/test/test_sensordata.cpp | 917 ++++ corelib/test/test_sensorevent.cpp | 323 ++ corelib/test/test_signature.cpp | 798 ++++ corelib/test/test_statistics.cpp | 672 +++ corelib/test/test_stereo.cpp | 319 ++ corelib/test/test_stereo_dense.cpp | 276 ++ corelib/test/test_stereocameramodel.cpp | 715 +++ corelib/test/test_transform.cpp | 220 + corelib/test/test_util2d.cpp | 1418 ++++++ corelib/test/test_util3d.cpp | 2075 +++++++++ corelib/test/test_util3d_correspondences.cpp | 469 ++ corelib/test/test_util3d_features.cpp | 290 ++ corelib/test/test_util3d_filtering.cpp | 1998 +++++++++ corelib/test/test_util3d_mapping.cpp | 682 +++ .../test/test_util3d_motion_estimation.cpp | 638 +++ corelib/test/test_util3d_registration.cpp | 439 ++ corelib/test/test_util3d_surface.cpp | 124 + corelib/test/test_util3d_transforms.cpp | 376 ++ corelib/test/test_visualword.cpp | 82 + corelib/test/test_vwdictionary.cpp | 847 ++++ coverage-report.sh | 148 + data/rgbd/calib/154.yaml | 17 + data/rgbd/calib/17.yaml | 17 + data/rgbd/depth/154.png | Bin 0 -> 123097 bytes data/rgbd/depth/17.png | Bin 0 -> 116855 bytes data/rgbd/rgb/154.jpg | Bin 0 -> 78853 bytes data/rgbd/rgb/17.jpg | Bin 0 -> 82283 bytes data/samples_GT.bmp | Bin 8134 -> 8218 bytes data/stereo_rect/left/50.jpg | Bin 0 -> 68515 bytes data/stereo_rect/left/60.jpg | Bin 0 -> 67197 bytes data/stereo_rect/right/50.jpg | Bin 0 -> 67018 bytes data/stereo_rect/right/60.jpg | Bin 0 -> 65240 bytes data/stereo_rect/stereo_left.yaml | 25 + data/stereo_rect/stereo_right.yaml | 25 + data/tests/.gitignore | 7 + data/tests/2loops_workspace_3IT_gt.g2o | 594 +++ data/tests/loop_3it_gps_gt.g2o | 313 ++ data/tests/manifest.txt | 25 + .../robust_graph_optimization_stereo_gt.g2o | 112 + data/tests/stereo_20Hz_gt.g2o | 256 ++ docs-report.sh | 221 + doxygen/custom.css | 55 + doxygen/generate_parameters_page.py | 338 ++ doxygen/mainpage.md | 330 ++ doxygen/tools.md | 66 + doxygen/version-switcher.js | 74 + doxygen/versions.js | 17 + examples/RGBDMapping/main.cpp | 8 - examples/WifiMapping/main.cpp | 8 - guilib/src/CloudViewer.cpp | 2 +- guilib/src/DatabaseViewer.cpp | 30 +- guilib/src/ExportBundlerDialog.cpp | 102 +- guilib/src/ExportCloudsDialog.cpp | 16 +- guilib/src/LoopClosureViewer.cpp | 5 +- guilib/src/MainWindow.cpp | 57 +- guilib/src/PostProcessingDialog.cpp | 58 +- guilib/src/PreferencesDialog.cpp | 12 +- guilib/src/ui/exportBundlerDialog.ui | 15 + guilib/src/ui/postProcessingDialog.ui | 14 +- guilib/src/ui/preferencesDialog.ui | 258 +- package.xml | 2 +- scripts/fetch_test_data.sh | 129 + tools/DatabaseViewer/main.cpp | 7 - tools/DetectMoreLoopClosures/main.cpp | 4 +- tools/EpipolarGeometry/main.cpp | 14 +- tools/Info/main.cpp | 30 +- tools/Matcher/main.cpp | 7 - tools/ReduceGraph/main.cpp | 7 - tools/Report/CMakeLists.txt | 14 +- tools/RgbdDataset/main.cpp | 2 +- tools/StereoEval/main.cpp | 8 +- utilite/CMakeLists.txt | 4 + utilite/include/rtabmap/utilite/UConversion.h | 84 +- utilite/include/rtabmap/utilite/ULogger.h | 28 +- utilite/include/rtabmap/utilite/UMath.h | 2 +- utilite/include/rtabmap/utilite/UMutex.h | 7 + .../include/rtabmap/utilite/UProcessInfo.h | 37 +- utilite/include/rtabmap/utilite/USemaphore.h | 52 +- utilite/include/rtabmap/utilite/UThreadNode.h | 7 +- utilite/include/rtabmap/utilite/UTimer.h | 8 - utilite/include/rtabmap/utilite/UVariant.h | 377 +- utilite/include/rtabmap/utilite/UtiLite.h | 48 +- utilite/src/UDirectory.cpp | 5 + utilite/src/UEventsManager.cpp | 11 +- utilite/src/UFile.cpp | 2 +- utilite/src/ULogger.cpp | 64 +- utilite/src/UThread.cpp | 2 +- utilite/src/UTimer.cpp | 24 - utilite/test/CMakeLists.txt | 66 + utilite/test/test_uconversion.cpp | 203 + utilite/test/test_udirectory.cpp | 242 + utilite/test/test_uevents.cpp | 607 +++ utilite/test/test_ufile.cpp | 145 + utilite/test/test_ulogger.cpp | 796 ++++ utilite/test/test_umath.cpp | 284 ++ utilite/test/test_umutex.cpp | 178 + utilite/test/test_uprocessinfo.cpp | 30 + utilite/test/test_usemaphore.cpp | 341 ++ utilite/test/test_ustl.cpp | 347 ++ utilite/test/test_uthread.cpp | 362 ++ utilite/test/test_utimer.cpp | 129 + utilite/test/test_uvariant.cpp | 213 + vcpkg.json | 8 +- website/_config.yml | 12 + website/index.md | 240 + website/oauth2_redirect.html | 9 + website/stylesheets/github-light.css | 124 + website/stylesheets/pygment_trac.css | 70 + website/stylesheets/stylesheet.css | 425 ++ 309 files changed, 67468 insertions(+), 3069 deletions(-) create mode 100644 .devcontainer/noble-kilted/Dockerfile create mode 100644 .devcontainer/noble-kilted/devcontainer.json create mode 100644 .gitattributes create mode 100644 .github/workflows/coverage.yml create mode 100644 .github/workflows/docs-preview-cleanup.yml create mode 100644 .github/workflows/docs.yml create mode 100644 Doxyfile.in create mode 100644 cmake_modules/RTABMapDocumentation.cmake create mode 100644 corelib/src/optimizer/ceres/bundle/between_cameras_error.h create mode 100644 corelib/src/optimizer/ceres/bundle/planar_constraint_error.h create mode 100644 corelib/src/optimizer/ceres/bundle/snavely_stereo_reprojection_error.h create mode 100644 corelib/src/optimizer/gtsam/PlanarBodyZFactor.h create mode 100644 corelib/test/CMakeLists.txt create mode 100644 corelib/test/TestUtils.h create mode 100644 corelib/test/test_bayesfilter.cpp create mode 100644 corelib/test/test_cameramodel.cpp create mode 100644 corelib/test/test_compression.cpp create mode 100644 corelib/test/test_dbdriver.cpp create mode 100644 corelib/test/test_dbdriversqlite3.cpp create mode 100644 corelib/test/test_features2d.cpp create mode 100644 corelib/test/test_geodeticcoords.cpp create mode 100644 corelib/test/test_globalmap.cpp create mode 100644 corelib/test/test_gps.cpp create mode 100644 corelib/test/test_graph.cpp create mode 100644 corelib/test/test_imu.cpp create mode 100644 corelib/test/test_imufilter.cpp create mode 100644 corelib/test/test_imuthread.cpp create mode 100644 corelib/test/test_landmark.cpp create mode 100644 corelib/test/test_laswriter.cpp create mode 100644 corelib/test/test_link.cpp create mode 100644 corelib/test/test_localgrid.cpp create mode 100644 corelib/test/test_localgridmaker.cpp create mode 100644 corelib/test/test_markerdetector.cpp create mode 100644 corelib/test/test_memory.cpp create mode 100644 corelib/test/test_odometry.cpp create mode 100644 corelib/test/test_optimizer.cpp create mode 100644 corelib/test/test_pydescriptor.cpp create mode 100644 corelib/test/test_pydetector.cpp create mode 100644 corelib/test/test_pymatcher.cpp create mode 100644 corelib/test/test_python_interface.cpp create mode 100644 corelib/test/test_registration.cpp create mode 100644 corelib/test/test_registrationicp.cpp create mode 100644 corelib/test/test_registrationvis.cpp create mode 100644 corelib/test/test_rtabmap.cpp create mode 100644 corelib/test/test_rtabmap_integration.cpp create mode 100644 corelib/test/test_sensorcapture.cpp create mode 100644 corelib/test/test_sensorcaptureinfo.cpp create mode 100644 corelib/test/test_sensorcapturethread.cpp create mode 100644 corelib/test/test_sensordata.cpp create mode 100644 corelib/test/test_sensorevent.cpp create mode 100644 corelib/test/test_signature.cpp create mode 100644 corelib/test/test_statistics.cpp create mode 100644 corelib/test/test_stereo.cpp create mode 100644 corelib/test/test_stereo_dense.cpp create mode 100644 corelib/test/test_stereocameramodel.cpp create mode 100644 corelib/test/test_transform.cpp create mode 100644 corelib/test/test_util2d.cpp create mode 100644 corelib/test/test_util3d.cpp create mode 100644 corelib/test/test_util3d_correspondences.cpp create mode 100644 corelib/test/test_util3d_features.cpp create mode 100644 corelib/test/test_util3d_filtering.cpp create mode 100644 corelib/test/test_util3d_mapping.cpp create mode 100644 corelib/test/test_util3d_motion_estimation.cpp create mode 100644 corelib/test/test_util3d_registration.cpp create mode 100644 corelib/test/test_util3d_surface.cpp create mode 100644 corelib/test/test_util3d_transforms.cpp create mode 100644 corelib/test/test_visualword.cpp create mode 100644 corelib/test/test_vwdictionary.cpp create mode 100755 coverage-report.sh create mode 100644 data/rgbd/calib/154.yaml create mode 100644 data/rgbd/calib/17.yaml create mode 100644 data/rgbd/depth/154.png create mode 100644 data/rgbd/depth/17.png create mode 100644 data/rgbd/rgb/154.jpg create mode 100644 data/rgbd/rgb/17.jpg create mode 100644 data/stereo_rect/left/50.jpg create mode 100644 data/stereo_rect/left/60.jpg create mode 100644 data/stereo_rect/right/50.jpg create mode 100644 data/stereo_rect/right/60.jpg create mode 100644 data/stereo_rect/stereo_left.yaml create mode 100644 data/stereo_rect/stereo_right.yaml create mode 100644 data/tests/.gitignore create mode 100644 data/tests/2loops_workspace_3IT_gt.g2o create mode 100644 data/tests/loop_3it_gps_gt.g2o create mode 100644 data/tests/manifest.txt create mode 100644 data/tests/robust_graph_optimization_stereo_gt.g2o create mode 100644 data/tests/stereo_20Hz_gt.g2o create mode 100755 docs-report.sh create mode 100644 doxygen/custom.css create mode 100644 doxygen/generate_parameters_page.py create mode 100644 doxygen/mainpage.md create mode 100644 doxygen/tools.md create mode 100644 doxygen/version-switcher.js create mode 100644 doxygen/versions.js create mode 100755 scripts/fetch_test_data.sh create mode 100644 utilite/test/CMakeLists.txt create mode 100644 utilite/test/test_uconversion.cpp create mode 100644 utilite/test/test_udirectory.cpp create mode 100644 utilite/test/test_uevents.cpp create mode 100644 utilite/test/test_ufile.cpp create mode 100644 utilite/test/test_ulogger.cpp create mode 100644 utilite/test/test_umath.cpp create mode 100644 utilite/test/test_umutex.cpp create mode 100644 utilite/test/test_uprocessinfo.cpp create mode 100644 utilite/test/test_usemaphore.cpp create mode 100644 utilite/test/test_ustl.cpp create mode 100644 utilite/test/test_uthread.cpp create mode 100644 utilite/test/test_utimer.cpp create mode 100644 utilite/test/test_uvariant.cpp create mode 100644 website/_config.yml create mode 100644 website/index.md create mode 100644 website/oauth2_redirect.html create mode 100644 website/stylesheets/github-light.css create mode 100644 website/stylesheets/pygment_trac.css create mode 100644 website/stylesheets/stylesheet.css diff --git a/.devcontainer/focal/Dockerfile b/.devcontainer/focal/Dockerfile index 841a7ff0..f0307e88 100644 --- a/.devcontainer/focal/Dockerfile +++ b/.devcontainer/focal/Dockerfile @@ -15,7 +15,5 @@ RUN set -ex && \ 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 - - +RUN echo 'source /opt/ros/$ROS_DISTRO/setup.bash' >> /home/${USERNAME}/.bashrc diff --git a/.devcontainer/jammy/Dockerfile b/.devcontainer/jammy/Dockerfile index 83d58d64..1dd140ef 100644 --- a/.devcontainer/jammy/Dockerfile +++ b/.devcontainer/jammy/Dockerfile @@ -15,7 +15,5 @@ RUN set -ex && \ 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 - - +RUN echo 'source /opt/ros/$ROS_DISTRO/setup.bash' >> /home/${USERNAME}/.bashrc diff --git a/.devcontainer/latest-deps-from-source/Dockerfile b/.devcontainer/latest-deps-from-source/Dockerfile index 941967ae..532c4d9a 100644 --- a/.devcontainer/latest-deps-from-source/Dockerfile +++ b/.devcontainer/latest-deps-from-source/Dockerfile @@ -34,6 +34,7 @@ RUN set -ex && \ RUN mkdir -p /home/${USERNAME}/Documents/RTAB-Map RUN echo "source /usr/share/bash-completion/completions/git" >> /home/${USERNAME}/.bashrc +RUN echo 'source /opt/ros/${ROS_DISTRO}/setup.bash' >> /home/${USERNAME}/.bashrc WORKDIR /home/${USERNAME}/ diff --git a/.devcontainer/noble-kilted/Dockerfile b/.devcontainer/noble-kilted/Dockerfile new file mode 100644 index 00000000..2201fb5a --- /dev/null +++ b/.devcontainer/noble-kilted/Dockerfile @@ -0,0 +1,20 @@ +FROM introlab3it/rtabmap:noble-kilted-deps +# 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 echo "source /usr/share/bash-completion/completions/git" >> /home/${USERNAME}/.bashrc +RUN echo 'source /opt/ros/kilted/setup.bash' >> /home/${USERNAME}/.bashrc diff --git a/.devcontainer/noble-kilted/devcontainer.json b/.devcontainer/noble-kilted/devcontainer.json new file mode 100644 index 00000000..724f0974 --- /dev/null +++ b/.devcontainer/noble-kilted/devcontainer.json @@ -0,0 +1,17 @@ +{ + "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", + "settings": { + "terminal.integrated.defaultProfile.linux": "bash" + }, + "remoteUser": "vscode", + "runArgs": ["--privileged"] +} diff --git a/.devcontainer/noble/Dockerfile b/.devcontainer/noble/Dockerfile index 84792a66..6f7ec5b0 100644 --- a/.devcontainer/noble/Dockerfile +++ b/.devcontainer/noble/Dockerfile @@ -19,7 +19,5 @@ RUN set -ex && \ 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 - - +RUN echo 'source /opt/ros/$ROS_DISTRO/setup.bash' >> /home/${USERNAME}/.bashrc diff --git a/.devcontainer/rolling/Dockerfile b/.devcontainer/rolling/Dockerfile index 576b75fe..c58fd852 100644 --- a/.devcontainer/rolling/Dockerfile +++ b/.devcontainer/rolling/Dockerfile @@ -77,4 +77,4 @@ RUN set -ex && \ 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 - +RUN echo 'source /opt/ros/$ROS_DISTRO/setup.bash' >> /home/${USERNAME}/.bashrc diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..834f39c7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,53 @@ +# This repository has a historical mix of CRLF and LF files. Letting git convert +# line endings (core.autocrlf, or a "text=auto" policy) would rewrite whole files +# on checkout/commit and bury real changes under thousands of ending-only diff +# lines -- and it would do so at unpredictable times, on whichever file happens +# to be touched next. Keep every file's bytes exactly as committed. +# +# This also keeps LF-only files LF on Windows checkouts, which data/tests/manifest.txt +# relies on: scripts/fetch_test_data.sh tab-splits it and a trailing CR breaks SHA +# matching. No per-file exception is needed for that. +# +# Note this does not stop an editor or script from rewriting a file's endings; it +# only stops git from doing it. +* -text + +# ----------------------------------------------------------------------------- +# Migrating to the standard LF policy (a future MR, on its own) +# ----------------------------------------------------------------------------- +# LF is what git stores natively and what essentially every C++ project on GitHub +# uses (LLVM, OpenCV, PCL, Qt). The rule above only freezes the current mix; it is +# a holding position, not the destination. To finish the job: +# +# 1. Replace the `* -text` line above with: +# +# * text=auto +# # Files that must stay LF even in a Windows working tree: +# *.sh text eol=lf +# *.bash text eol=lf +# +# (`text=auto` = store LF in the repo, check out native line endings.) +# +# 2. Rewrite the working tree and stage it, in the same commit as step 1: +# +# git add --renormalize . +# +# 3. Verify nothing but line endings changed -- this must print nothing: +# +# git diff --cached --ignore-cr-at-eol +# +# 4. Commit alone, with no other change in the MR, then record the SHA so +# `git blame` (and GitHub's blame view) skips over it: +# +# echo "" >> .git-blame-ignore-revs +# +# Add a follow-up commit for that file; also set it locally with +# `git config blame.ignoreRevsFile .git-blame-ignore-revs`. +# +# Cost, measured on the tree at the time of writing: 227 CRLF files + 7 with mixed +# endings, ~138k of ~634k lines rewritten (22%), across 234 files. The commit +# itself is mechanical and safe -- the real cost is every open branch that touches +# one of those files, which will conflict over the whole file on rebase. Merge or +# rebase those with `-X renormalize` (merge) / `--strategy-option renormalize`. +# +# Best moment: right after a release, when the branch backlog is smallest. diff --git a/.github/actions/install-windows-cuda-deps/action.yml b/.github/actions/install-windows-cuda-deps/action.yml index 0664fe70..d3cfe2cf 100644 --- a/.github/actions/install-windows-cuda-deps/action.yml +++ b/.github/actions/install-windows-cuda-deps/action.yml @@ -26,7 +26,7 @@ runs: uses: actions/cache@v4 with: path: ${{ runner.workspace }}/vcpkg_installed - key: ${{ runner.os }}-vcpkg-export-66c0373d-x64-vs2022-cuda130_v4 + key: ${{ runner.os }}-vcpkg-export-66c0373d-x64-vs2022-cuda130_v5 - name: Download and Install vcpkg if: steps.cache-vcpkg.outputs.cache-hit != 'true' diff --git a/.github/actions/install-windows-deps/action.yml b/.github/actions/install-windows-deps/action.yml index c9837afa..1b71cf18 100644 --- a/.github/actions/install-windows-deps/action.yml +++ b/.github/actions/install-windows-deps/action.yml @@ -13,7 +13,7 @@ runs: uses: actions/cache@v4 with: path: ${{ runner.workspace }}/vcpkg_installed - key: ${{ runner.os }}-vcpkg-export-66c0373d-x64-vs2022-v6 + key: ${{ runner.os }}-vcpkg-export-66c0373d-x64-vs2022-v7 - name: Download and Install vcpkg if: steps.cache-vcpkg.outputs.cache-hit != 'true' diff --git a/.github/workflows/cmake-linux.yml b/.github/workflows/cmake-linux.yml index bf2b51fb..7edcae53 100644 --- a/.github/workflows/cmake-linux.yml +++ b/.github/workflows/cmake-linux.yml @@ -19,6 +19,9 @@ jobs: build: name: ${{ matrix.build_name }} runs-on: ${{ matrix.os }} + concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ matrix.build_name }} + cancel-in-progress: true strategy: fail-fast: true matrix: @@ -44,6 +47,21 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Restore test data + id: cache-testdata + uses: actions/cache@v4 + with: + # SQLite DBs are binary-portable -> share the cache across + # linux / macos / windows. Key omits runner.os on purpose so all + # three OSes hit the same entry. + path: data/tests/*.db + key: testdata-${{ hashFiles('data/tests/manifest.txt') }} + + - name: Fetch test data + if: steps.cache-testdata.outputs.cache-hit != 'true' + shell: bash + run: bash scripts/fetch_test_data.sh + - name: Install Linux Dependencies run: | DEBIAN_FRONTEND=noninteractive @@ -72,9 +90,14 @@ jobs: 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}} - + - name: Test + working-directory: ${{github.workspace}}/build + # PYTHONNOUSERSITE=1: prevent the embedded Python interpreter from + # loading numpy / other site-packages from ~/.local that were compiled + # against a different ABI than the build-time Python (causes numpy + # 1.x/2.x mismatch crashes in test_pydetector / test_pydescriptor / + # test_pymatcher). + env: + PYTHONNOUSERSITE: 1 + run: | + ctest -C ${{env.BUILD_TYPE}} --output-on-failure \ No newline at end of file diff --git a/.github/workflows/cmake-macos.yml b/.github/workflows/cmake-macos.yml index 240395f3..773205e5 100644 --- a/.github/workflows/cmake-macos.yml +++ b/.github/workflows/cmake-macos.yml @@ -22,11 +22,12 @@ jobs: strategy: fail-fast: true matrix: - build_name: [macos-sequoia-intel, macos-sequoia-apple-silicon, macos-tahoe-intel, macos-tahoe-apple-silicon, macos-tahoe-intel-cv5, macos-tahoe-apple-silicon-cv5] + build_name: [macos-sequoia-apple-silicon, macos-tahoe-intel, macos-tahoe-apple-silicon, macos-tahoe-intel-cv5, macos-tahoe-apple-silicon-cv5] include: - - build_name: macos-sequoia-intel - os: macos-15-intel - cv: opencv@4 + # macos-sequoia-intel temporarily disabled. Had some g2o optimizer issues. + #- build_name: macos-sequoia-intel + # os: macos-15-intel + # cv: opencv@4 - build_name: macos-sequoia-apple-silicon os: macos-15 cv: opencv@4 @@ -45,6 +46,21 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Restore test data + id: cache-testdata + uses: actions/cache@v4 + with: + # SQLite DBs are binary-portable -> share the cache across + # linux / macos / windows. Key omits runner.os on purpose so all + # three OSes hit the same entry. + path: data/tests/*.db + key: testdata-${{ hashFiles('data/tests/manifest.txt') }} + + - name: Fetch test data + if: steps.cache-testdata.outputs.cache-hit != 'true' + shell: bash + run: bash scripts/fetch_test_data.sh + - name: Install Brew Dependencies run: | # Update brew and install from Brewfile if present, or specific packages @@ -66,6 +82,7 @@ jobs: LIBOMP=$(brew --prefix libomp) cmake -B ${{github.workspace}}/build \ -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} \ + -DBUILD_TESTING=ON \ -DBUILD_AS_BUNDLE=ON \ -DWITH_ORBBEC_SDK=ON \ -DWITH_DEPTHAI=ON \ @@ -98,9 +115,45 @@ jobs: 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}} - + - name: Test + working-directory: ${{github.workspace}}/build + # PYTHONNOUSERSITE=1: prevent the embedded Python interpreter from + # loading numpy / other site-packages from the user's site that were + # compiled against a different ABI than the build-time Python. + env: + PYTHONNOUSERSITE: 1 + run: | + ctest -C ${{env.BUILD_TYPE}} --output-on-failure + + # A ctest SEGFAULT is reported as just "SEGFAULT" with no backtrace, which + # makes a crash inside a long integration test invisible -- the log simply + # stops mid-test. macOS's ReportCrash writes a symbolized .ips report for + # every crashed process, so surface any that appeared during this job. + - name: Dump macOS crash reports on failure + if: failure() + run: | + # ReportCrash can lag the process death by a few seconds. + sleep 20 + found=0 + for d in "$HOME/Library/Logs/DiagnosticReports" /Library/Logs/DiagnosticReports; do + [ -d "$d" ] || continue + while IFS= read -r f; do + found=1 + echo "::group::$f" + cat "$f" + echo "::endgroup::" + done < <(find "$d" -type f \( -name '*.ips' -o -name '*.crash' \) -mtime -1 2>/dev/null | sort) + done + if [ "$found" = 0 ]; then + echo "No crash reports found -- the job may have failed without a crash." + fi + + - name: Upload ctest log on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: ctest-log-${{ matrix.build_name }} + path: build/Testing/Temporary/LastTest.log + if-no-files-found: warn + retention-days: 7 + diff --git a/.github/workflows/cmake-ros.yml b/.github/workflows/cmake-ros.yml index 942735d2..66c15a92 100644 --- a/.github/workflows/cmake-ros.yml +++ b/.github/workflows/cmake-ros.yml @@ -19,6 +19,9 @@ jobs: build: name: ${{ matrix.ros_distribution }} runs-on: ubuntu-latest + concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ matrix.ros_distribution }} + cancel-in-progress: true strategy: fail-fast: false matrix: @@ -39,6 +42,45 @@ jobs: image: osrf/ros:${{ matrix.ros_distribution }}-desktop-full steps: - uses: actions/checkout@v4 + + - name: Restore test data + id: cache-testdata + uses: actions/cache@v4 + with: + path: data/tests/*.db + key: testdata-${{ hashFiles('data/tests/manifest.txt') }} + + - name: Fetch test data + if: steps.cache-testdata.outputs.cache-hit != 'true' + shell: bash + run: | + # The ROS images are slim; fetch_test_data.sh needs curl. + command -v curl >/dev/null || (apt-get update && apt-get install -y --no-install-recommends curl) + bash scripts/fetch_test_data.sh + + # `${{ github.workspace }}` is expanded by the runner on the *host* + # (/home/runner/work/...), but this job runs in a container where the runner + # bind-mounts that directory somewhere else (/__w/...). Baking the host path + # into RTABMAP_TEST_DATA_ROOT points every data-driven test at a directory + # that does not exist in the container: the integration tests silently skip + # and the ones that imread() committed fixtures abort outright. + # + # Resolve the path inside the container instead. A `run` step's cwd is the + # checkout root -- the same assumption the relative `scripts/...` and + # `data/tests/*.db` paths in the steps above already rely on -- so derive it + # from $PWD rather than trusting a path computed outside the container. The + # existence checks then turn that into a verified precondition: a wrong + # mount point or a failed fetch fails here, with the path printed, instead + # of degrading into skipped tests. + - name: Resolve test data root in the container + id: testdata + shell: bash + run: | + root="$PWD/data" + test -f "$root/samples/17.jpg" || { echo "::error::test data root $root is missing committed fixtures"; exit 1; } + ls "$root"/tests/*.db >/dev/null || { echo "::error::no test databases in $root/tests"; exit 1; } + echo "root=$root" >> "$GITHUB_OUTPUT" + - uses: ros-tooling/setup-ros@v0.7 with: required-ros-distributions: ${{ matrix.ros_distribution }} @@ -48,4 +90,5 @@ jobs: package-name: rtabmap target-ros2-distro: ${{ matrix.ros_distribution }} rosdep-skip-keys: "${{ matrix.skip_keys }}" - + coverage-result: false + extra-cmake-args: -DTEST_DATA_ROOT=${{ steps.testdata.outputs.root }} diff --git a/.github/workflows/cmake-windows.yml b/.github/workflows/cmake-windows.yml index 87513c9b..e11159af 100644 --- a/.github/workflows/cmake-windows.yml +++ b/.github/workflows/cmake-windows.yml @@ -36,6 +36,22 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Restore test data + id: cache-testdata + if: matrix.build_name != 'windows-2022-cuda' + uses: actions/cache@v4 + with: + # SQLite DBs are binary-portable -> share the cache across + # linux / macos / windows. Key omits runner.os on purpose so all + # three OSes hit the same entry. + path: data/tests/*.db + key: testdata-${{ hashFiles('data/tests/manifest.txt') }} + + - name: Fetch test data + if: matrix.build_name != 'windows-2022-cuda' && steps.cache-testdata.outputs.cache-hit != 'true' + shell: bash + run: bash scripts/fetch_test_data.sh + - name: Install Windows Dependencies if: matrix.build_name == 'windows-2022' uses: ./.github/actions/install-windows-deps @@ -65,6 +81,7 @@ jobs: -DWITH_REALSENSE2=ON ` -DWITH_CCCORELIB=ON ` ${{ matrix.extra_cmake_def }} ` + -DBUILD_TESTING=ON ` -DVCPKG_MANIFEST_INSTALL=OFF ` -DVCPKG_TARGET_TRIPLET=x64-windows-release ` -DVCPKG_INSTALLED_DIR="${{env.VCPKG_EXPORT_PATH}}/installed" ` @@ -72,7 +89,42 @@ jobs: -DTorch_DIR=${{env.VCPKG_EXPORT_PATH}}/installed/x64-windows-release/tools/python3/Lib/site-packages/torch/share/cmake/Torch - name: Build - run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} + run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --target ALL_BUILD + + - name: Test + # Not run on the CUDA build, which is a build+package job only. + # + # The runner has no NVIDIA GPU or driver (see the driver-DLL note in + # app/src/CMakeLists.txt), so no cv::cuda / CUDASIFT path can actually + # execute there -- everything falls back to CPU and the run just repeats + # the coverage the non-CUDA job already gives, on the same sources. It + # used to cost more than it returned: the two cv::cuda-touching tests + # (test_util3d_features, test_localgrid) had to be excluded because lazy + # CUDA-init stalled them past the 300 s timeout, and once WITH_ZED=ON was + # added to this job every remaining corelib test died in the loader with + # 0xc0000135 (STATUS_DLL_NOT_FOUND) before reaching main(): rtabmap_core + # links sl_zed64.dll, which imports the driver-only nvcuvid.dll / + # nvEncodeAPI64.dll that a driver-less runner does not have. + # + # So this job compiles, links and packages the CUDA artifact; the + # non-CUDA job is what verifies behaviour. Same reason the Info step + # below is skipped here. + if: matrix.build_name != 'windows-2022-cuda' + working-directory: ${{github.workspace}}/build + # PYTHONHOME points the embedded interpreter at vcpkg's bundled + # Python install (where Lib/, DLLs/ live). Without it Python emits + # "Could not find platform independent libraries " at init + # and runs without a stdlib, breaking every numpy import. + # PYTHONNOUSERSITE=1: prevent the embedded Python interpreter from + # loading numpy / other site-packages from %APPDATA%\Python that + # were compiled against a different ABI than the build-time Python + # (numpy 1.x/2.x mismatch crashes in test_pydetector / + # test_pydescriptor / test_pymatcher). + env: + PYTHONHOME: ${{env.VCPKG_EXPORT_PATH}}/installed/x64-windows-release/tools/python3 + PYTHONNOUSERSITE: 1 + run: | + ctest -C ${{env.BUILD_TYPE}} --output-on-failure --timeout 300 - name: Info # Skipped for CUDA: the binary links ZED (sl_zed64.dll -> nvcuvid/nvEncodeAPI64), @@ -116,10 +168,3 @@ jobs: 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/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 00000000..ff521cd0 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,125 @@ +name: Coverage + +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' }} + +permissions: + contents: read + id-token: write + +env: + BUILD_TYPE: Debug + +jobs: + coverage: + name: Coverage (ubuntu-24.04) + runs-on: ubuntu-24.04 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + steps: + - uses: actions/checkout@v4 + + - name: Install Linux dependencies + run: | + DEBIAN_FRONTEND=noninteractive + sudo apt-get update + sudo apt-get install -y \ + lcov \ + libopencv-dev \ + libpcl-dev \ + git \ + cmake \ + software-properties-common \ + libyaml-cpp-dev + + - name: Configure CMake + run: | + cmake -B ${{ github.workspace }}/build \ + -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} \ + -DENABLE_COVERAGE=ON \ + -DBUILD_TESTING=ON \ + -DBUILD_APP=OFF \ + -DBUILD_TOOLS=OFF \ + -DBUILD_EXAMPLES=OFF \ + -DWITH_QT=OFF \ + -DWITH_PYTHON=OFF \ + -DWITH_CERES=OFF \ + -DWITH_G2O=OFF \ + -DWITH_GTSAM=OFF \ + -DWITH_MRPT=OFF \ + -DWITH_VERTIGO=OFF \ + -DWITH_CVSBA=OFF \ + -DWITH_POINTMATCHER=OFF \ + -DWITH_CCCORELIB=OFF \ + -DWITH_OPEN3D=OFF \ + -DWITH_LOAM=OFF \ + -DWITH_FLOAM=OFF \ + -DWITH_LIOSAM=OFF \ + -DWITH_FLYCAPTURE2=OFF \ + -DWITH_ZED=OFF \ + -DWITH_ZEDOC=OFF \ + -DWITH_REALSENSE=OFF + + - name: Build + run: cmake --build ${{ github.workspace }}/build -j$(nproc) + + - name: Test + working-directory: ${{ github.workspace }}/build + run: | + export LD_LIBRARY_PATH="${{ github.workspace }}/build/bin${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" + find ${{ github.workspace }}/build -name '*.gcda' -delete + ctest --output-on-failure + + - name: Generate LCOV report + run: | + GCOV_VER="$(gcc -dumpversion | cut -d. -f1)" + if command -v "gcov-${GCOV_VER}" >/dev/null 2>&1; then + GCOV_TOOL="gcov-${GCOV_VER}" + else + GCOV_TOOL="gcov" + fi + LCOV_IGNORE=(--ignore-errors gcov,source,graph,mismatch,unused) + + lcov --gcov-tool "$GCOV_TOOL" "${LCOV_IGNORE[@]}" \ + --capture \ + --directory ${{ github.workspace }}/build/corelib/src \ + --directory ${{ github.workspace }}/build/utilite/src \ + --output-file lcov.info + + lcov "${LCOV_IGNORE[@]}" --extract lcov.info \ + '${{ github.workspace }}/*' \ + --output-file lcov.info + lcov "${LCOV_IGNORE[@]}" --remove lcov.info \ + '*/sqlite3/*' \ + '*/rtflann/*' \ + --output-file lcov.info + + lcov --summary lcov.info + + - name: HTML coverage report + run: genhtml --ignore-errors source,mismatch lcov.info --output-directory coverage-html --legend --demangle-cpp + + - name: Upload HTML coverage artifact + uses: actions/upload-artifact@v4 + with: + name: coverage-html + path: coverage-html + retention-days: 14 + + - name: Upload to Codecov + if: ${{ env.CODECOV_TOKEN != '' }} + uses: codecov/codecov-action@v5 + with: + files: lcov.info + token: ${{ env.CODECOV_TOKEN }} + fail_ci_if_error: false diff --git a/.github/workflows/docs-preview-cleanup.yml b/.github/workflows/docs-preview-cleanup.yml new file mode 100644 index 00000000..118bec08 --- /dev/null +++ b/.github/workflows/docs-preview-cleanup.yml @@ -0,0 +1,54 @@ +name: Documentation preview cleanup + +# Removes the per-PR documentation preview published by docs.yml under +# gh-pages:preview/pr-/ once the pull request is closed (merged or not). +# +# Deliberately has no `paths` filter: docs.yml only publishes a preview when +# documentation-related files changed, but this must fire for every close so a +# preview can never be orphaned on the branch. +on: + pull_request: + types: [closed] + +permissions: + contents: read + +jobs: + cleanup: + name: Remove PR preview + # Forks never get a preview (their token is read-only), so nothing to clean. + if: github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-24.04 + + # Same group as docs.yml's publish job: both push to gh-pages, and two + # concurrent pushes would collide. + concurrency: + group: gh-pages-publish + cancel-in-progress: false + + permissions: + contents: write + + steps: + - name: Checkout gh-pages + uses: actions/checkout@v4 + with: + ref: gh-pages + fetch-depth: 1 + + - name: Remove preview directory + env: + PREVIEW_DIR: preview/pr-${{ github.event.number }} + run: | + set -eux + if [[ ! -d "${PREVIEW_DIR}" ]]; then + echo "No preview at ${PREVIEW_DIR}, nothing to do." + exit 0 + fi + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + git rm -r --quiet "${PREVIEW_DIR}" + # Drop the parent too once the last preview is gone. + rmdir preview 2>/dev/null || true + git commit -m "Remove preview for PR #${{ github.event.number }}" + git push diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..123a8fce --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,211 @@ +name: Documentation + +on: + push: + branches: + - master + tags: + - '*' + # Publishing commits to the gh-pages branch, so only run when something that + # affects the documentation actually changed. Use the manual "Run workflow" + # button if a deploy is needed outside of these paths. + paths: &docs_paths + - '.github/workflows/docs.yml' + - 'CMakeLists.txt' + - 'Doxyfile.in' + - 'docs-report.sh' + - 'website/**' + - 'doxygen/**' + - 'package.xml' + - 'corelib/include/**' + - 'utilite/include/**' + pull_request: + branches: + - '**' + paths: *docs_paths + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +permissions: + contents: read + +jobs: + build: + name: Build site (ubuntu-24.04) + runs-on: ubuntu-24.04 + + outputs: + version: ${{ steps.site.outputs.version }} + + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + DEBIAN_FRONTEND=noninteractive + sudo apt-get update + sudo apt-get install -y \ + doxygen \ + graphviz \ + cmake \ + libopencv-dev \ + libpcl-dev \ + libyaml-cpp-dev \ + git + + # Configures build-docs (CMake generates the export headers that are part + # of Doxygen's INPUT), runs Doxygen into build-docs/api/latest/. Same + # script used locally, so a local preview matches what gets published. + - name: Generate C++ API documentation (Doxygen) + run: ./docs-report.sh + + # The theme emits absolute asset links built from site.baseurl, which + # GitHub's own Jekyll infers from the repository (/rtabmap). A PR preview + # is served from a deeper path, so those links would 404 there. Pin + # baseurl to the path this particular build gets published to. + - name: Set Jekyll baseurl + run: | + set -eux + if [ "${{ github.event_name }}" = "pull_request" ]; then + baseurl="/${{ github.event.repository.name }}/preview/pr-${{ github.event.number }}" + else + baseurl="/${{ github.event.repository.name }}" + fi + echo "baseurl: ${baseurl}" >> website/_config.yml + + # Landing page: built here rather than by GitHub's own Jekyll, so that + # .nojekyll can be set below and the Doxygen output is served untouched. + - name: Build landing page (Jekyll) + uses: actions/jekyll-build-pages@v1 + with: + source: ./website + destination: ./_site + + - name: Assemble site + id: site + run: | + set -eux + # jekyll-build-pages is a container action running as root, so _site + # comes back owned by root and the steps below (running as the runner + # user) cannot write into it. + sudo chown -R "$(id -u):$(id -g)" _site + + # Serve everything verbatim: the site is already built, and Jekyll + # would otherwise skip Doxygen files/folders starting with '_'. + touch _site/.nojekyll + + mkdir -p _site/api + cp -r build-docs/api/latest _site/api/latest + + # On a release tag (plain X.Y.Z, not the ROS-distro variants such as + # 0.23.7-jazzy), also publish under the version number. Older versions + # already on the branch are preserved by keep_files when publishing. + version="" + if [[ "${GITHUB_REF_TYPE}" == "tag" && "${GITHUB_REF_NAME}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + version="${GITHUB_REF_NAME}" + cp -r build-docs/api/latest "_site/api/${version}" + fi + echo "version=${version}" >> "$GITHUB_OUTPUT" + + # The dropdown list is maintained by hand in the repository (one entry + # per release) and shared by every published version -- see the header + # comment in doxygen/versions.js. + cp doxygen/versions.js _site/api/versions.js + + # /api/ has no content of its own: send it to the current docs so a + # bare .../api/ link lands somewhere useful instead of a 404. The + # target is relative, so it works at the site root and under a + # preview prefix alike. + printf '%s\n' \ + '' \ + '' \ + 'RTAB-Map API documentation' \ + '' \ + '' \ + '

Redirecting to the latest API documentation.

' \ + > _site/api/index.html + + test -f _site/index.html + test -f _site/api/index.html + test -f _site/api/latest/index.html + + - name: Upload site artifact + uses: actions/upload-artifact@v4 + with: + name: documentation-site + path: _site + # include-hidden-files keeps .nojekyll, which the publish job needs. + include-hidden-files: true + retention-days: 14 + + publish: + name: Publish to gh-pages + needs: build + # Forks are skipped for pull requests: their GITHUB_TOKEN is read-only, and + # granting write access to a workflow that builds untrusted code would be a + # security hole. + if: >- + github.event_name != 'pull_request' || + github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-24.04 + + # Serialize every push to gh-pages, including the preview cleanup workflow, + # and never cancel one in flight: two concurrent pushes would collide. + concurrency: + group: gh-pages-publish + cancel-in-progress: false + + permissions: + contents: write + + steps: + - name: Download site + uses: actions/download-artifact@v4 + with: + name: documentation-site + path: _site + + # Pull requests go to preview/pr-/, everything else to the site + # root. keep_files preserves what this run does not carry: the published + # version folders (api/0.23.x/) and the other PRs' previews. + - name: Publish + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_branch: gh-pages + publish_dir: ./_site + destination_dir: ${{ github.event_name == 'pull_request' && format('preview/pr-{0}', github.event.number) || '' }} + keep_files: true + # Do not let the action drop a .nojekyll at the *branch root*: that + # disables Jekyll for everything on the branch, including content + # published outside this deployment. The site we publish carries its + # own .nojekyll inside destination_dir, which is the only place it + # should apply. + enable_jekyll: true + user_name: 'github-actions[bot]' + user_email: 'github-actions[bot]@users.noreply.github.com' + commit_message: >- + ${{ github.event_name == 'pull_request' + && format('Preview for PR #{0}', github.event.number) + || format('Update documentation ({0})', needs.build.outputs.version || 'latest') }} + + - name: Link the site + run: | + base="https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}" + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + url="${base}/preview/pr-${{ github.event.number }}" + note="Removed automatically when this pull request is closed." + else + url="${base}" + note="" + fi + { + echo "### Documentation" + echo "" + echo "- Landing page: ${url}/" + echo "- C++ API: ${url}/api/latest/" + [[ -n "${note}" ]] && { echo ""; echo "${note}"; } + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.gitignore b/.gitignore index 1fb83a34..62dc7222 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,20 @@ compile_flags.txt tags build_* *.bak +build-* + +# Coverage (coverage-report.sh / .github/workflows/coverage.yml) +coverage-html/ +lcov.info +*.gcda +*.gcno + +# Doxygen (docs-report.sh / .github/workflows/docs.yml) +doc/ +# Byte-code of doxygen/generate_parameters_page.py +__pycache__/ +# Generated from Doxyfile.in by CMake. +Doxyfile # Windows bundle: vcpkg tree, build output, and deps built from source (bundle_windows_deps.bat) vcpkg/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 0e971403..595aab97 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 9) +SET(RTABMAP_PATCH_VERSION 10) SET(RTABMAP_VERSION ${RTABMAP_MAJOR_VERSION}.${RTABMAP_MINOR_VERSION}.${RTABMAP_PATCH_VERSION}) @@ -167,6 +167,61 @@ ENDIF(APPLE OR WIN32) OPTION(BUILD_APP "Build main application" ON) OPTION(BUILD_TOOLS "Build tools" ON) OPTION(BUILD_EXAMPLES "Build examples" ON) +# Off by default: it downloads the Doxygen theme at configure time, which an +# ordinary build has no reason to need. Set by docs-report.sh. +OPTION(BUILD_DOCUMENTATION "Prepare the API documentation build (downloads the Doxygen theme)" OFF) + +# The unit tests are host executables run by ctest, which can't be done from a +# cross-compiled Android/iOS build. +if(ANDROID OR IOS OR CMAKE_SYSTEM_NAME STREQUAL "iOS") + set(BUILD_TESTING OFF CACHE BOOL "Build the testing tree." FORCE) +endif() + +include(CTest) +if(BUILD_TESTING) + # Add GTest using FetchContent + include(FetchContent) + FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/refs/tags/v1.16.0.zip + ) + FetchContent_GetProperties(googletest) + if(NOT googletest_POPULATED) + FetchContent_Populate(googletest) + add_subdirectory(${googletest_SOURCE_DIR} ${googletest_BINARY_DIR}) + endif() + include(GoogleTest) + set(TEST_DATA_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/data" + CACHE PATH "Root directory holding the test fixtures (data/tests/*.db)") +endif() + +OPTION(ENABLE_COVERAGE "Build with gcov/lcov coverage (GCC/Clang, Debug only; see coverage-report.sh)" OFF) +IF(ENABLE_COVERAGE) + IF(NOT CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + MESSAGE(FATAL_ERROR "ENABLE_COVERAGE requires GCC or Clang with gcov/lcov (current: ${CMAKE_CXX_COMPILER_ID}).") + ENDIF() + IF(NOT CMAKE_BUILD_TYPE STREQUAL "Debug") + MESSAGE(FATAL_ERROR "ENABLE_COVERAGE requires CMAKE_BUILD_TYPE=Debug (current: \"${CMAKE_BUILD_TYPE}\").") + ENDIF() + MESSAGE(STATUS "Coverage instrumentation enabled") + ADD_COMPILE_OPTIONS(--coverage -O0 -g) + ADD_LINK_OPTIONS(--coverage) +ENDIF() + +# Check UDEBUG/UINFO/UWARN/UERROR/uFormat format strings against their +# arguments. +# -Wno-format-zero-length: UDEBUG("") is used deliberately as a trace point +# -Wformat-security: non-literal format strings, e.g. UERROR(uFormat(...).c_str()) +OPTION(ENABLE_FORMAT_ERRORS "Treat log/uFormat format-string mismatches as errors (GCC/Clang)" OFF) +IF(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + ADD_COMPILE_OPTIONS(-Wformat -Wformat-security -Wno-format-zero-length) + IF(ENABLE_FORMAT_ERRORS) + MESSAGE(STATUS "Format-string mismatches are errors") + ADD_COMPILE_OPTIONS(-Werror=format -Werror=format-security) + ENDIF() +ELSEIF(ENABLE_FORMAT_ERRORS) + MESSAGE(WARNING "ENABLE_FORMAT_ERRORS has no effect on ${CMAKE_CXX_COMPILER_ID} (needs GCC/Clang)") +ENDIF() ####### DEPENDENCIES ####### IF(MOBILE_BUILD) @@ -1263,6 +1318,15 @@ ENDIF() CONFIGURE_FILE(Version.h.in ${CMAKE_CURRENT_BINARY_DIR}/corelib/src/include/${PROJECT_PREFIX}/core/Version.h) +# API documentation (Doxygen). Off by default: preparing it downloads the theme, +# spawns Doxygen to regenerate the HTML header and runs the parameter-page +# generator, none of which an ordinary build needs. docs-report.sh turns it on; +# see also .github/workflows/docs.yml. +IF(BUILD_DOCUMENTATION) + INCLUDE(RTABMapDocumentation) + RTABMAP_SETUP_DOCUMENTATION() +ENDIF(BUILD_DOCUMENTATION) + ADD_SUBDIRECTORY( utilite ) ADD_SUBDIRECTORY( corelib ) @@ -1495,6 +1559,9 @@ MESSAGE(STATUS " CMAKE_INSTALL_LIBDIR = ${CMAKE_INSTALL_LIBDIR}") MESSAGE(STATUS " BUILD_APP = ${BUILD_APP}") MESSAGE(STATUS " BUILD_TOOLS = ${BUILD_TOOLS}") MESSAGE(STATUS " BUILD_EXAMPLES = ${BUILD_EXAMPLES}") +MESSAGE(STATUS " BUILD_TESTING = ${BUILD_TESTING}") +MESSAGE(STATUS " ENABLE_COVERAGE = ${ENABLE_COVERAGE}") +MESSAGE(STATUS " ENABLE_FORMAT_ERRORS = ${ENABLE_FORMAT_ERRORS}") IF(NOT WIN32) # see comment above for the BUILD_SHARED_LIBS option on Windows MESSAGE(STATUS " BUILD_SHARED_LIBS = ${BUILD_SHARED_LIBS}") diff --git a/Doxyfile.in b/Doxyfile.in new file mode 100644 index 00000000..78ae7694 --- /dev/null +++ b/Doxyfile.in @@ -0,0 +1,2824 @@ +# Doxyfile 1.9.8 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. +# +# All text after a double hash (##) is considered a comment and is placed in +# front of the TAG it is preceding. +# +# All text after a single hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists, items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (\" \"). +# +# Note: +# +# Use doxygen to compare the used configuration file with the template +# configuration file: +# doxygen -x [configFile] +# Use doxygen to compare the used configuration file with the template +# configuration file without replacing the environment variables or CMake type +# replacement variables: +# doxygen -x_noenv [configFile] + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the configuration +# file that follow. The default is UTF-8 which is also the encoding used for all +# text before the first occurrence of this tag. Doxygen uses libiconv (or the +# iconv built into libc) for the transcoding. See +# https://www.gnu.org/software/libiconv/ for the list of possible encodings. +# The default value is: UTF-8. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by +# double-quotes, unless you are using Doxywizard) that should identify the +# project for which the documentation is generated. This name is used in the +# title of most generated pages and in a few other places. +# The default value is: My Project. + +PROJECT_NAME = RTAB-Map + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. This +# could be handy for archiving the generated documentation or if some version +# control system is used. + +PROJECT_NUMBER = @RTABMAP_VERSION@ + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewer a +# quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = "Real-Time Appearance-Based Mapping" + +# With the PROJECT_LOGO tag one can specify a logo or an icon that is included +# in the documentation. The maximum height of the logo should not exceed 55 +# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy +# the logo to the output directory. + +PROJECT_LOGO = + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path +# into which the generated documentation will be written. If a relative path is +# entered, it will be relative to the location where doxygen was started. If +# left blank the current directory will be used. + +OUTPUT_DIRECTORY = doc + +# If the CREATE_SUBDIRS tag is set to YES then doxygen will create up to 4096 +# sub-directories (in 2 levels) under the output directory of each output format +# and will distribute the generated files over these directories. Enabling this +# option can be useful when feeding doxygen a huge amount of source files, where +# putting all generated files in the same directory would otherwise causes +# performance problems for the file system. Adapt CREATE_SUBDIRS_LEVEL to +# control the number of sub-directories. +# The default value is: NO. + +CREATE_SUBDIRS = NO + +# Controls the number of sub-directories that will be created when +# CREATE_SUBDIRS tag is set to YES. Level 0 represents 16 directories, and every +# level increment doubles the number of directories, resulting in 4096 +# directories at level 8 which is the default and also the maximum value. The +# sub-directories are organized in 2 levels, the first level always has a fixed +# number of 16 directories. +# Minimum value: 0, maximum value: 8, default value: 8. +# This tag requires that the tag CREATE_SUBDIRS is set to YES. + +CREATE_SUBDIRS_LEVEL = 8 + +# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII +# characters to appear in the names of generated files. If set to NO, non-ASCII +# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode +# U+3044. +# The default value is: NO. + +ALLOW_UNICODE_NAMES = NO + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Bulgarian, +# Catalan, Chinese, Chinese-Traditional, Croatian, Czech, Danish, Dutch, English +# (United States), Esperanto, Farsi (Persian), Finnish, French, German, Greek, +# Hindi, Hungarian, Indonesian, Italian, Japanese, Japanese-en (Japanese with +# English messages), Korean, Korean-en (Korean with English messages), Latvian, +# Lithuanian, Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, +# Romanian, Russian, Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, +# Swedish, Turkish, Ukrainian and Vietnamese. +# The default value is: English. + +OUTPUT_LANGUAGE = English + +# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member +# descriptions after the members that are listed in the file and class +# documentation (similar to Javadoc). Set to NO to disable this. +# The default value is: YES. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief +# description of a member or function before the detailed description +# +# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. +# The default value is: YES. + +REPEAT_BRIEF = YES + +# This tag implements a quasi-intelligent brief description abbreviator that is +# used to form the text in various listings. Each string in this list, if found +# as the leading text of the brief description, will be stripped from the text +# and the result, after processing the whole list, is used as the annotated +# text. Otherwise, the brief description is used as-is. If left blank, the +# following values are used ($name is automatically replaced with the name of +# the entity):The $name class, The $name widget, The $name file, is, provides, +# specifies, contains, represents, a, an and the. + +ABBREVIATE_BRIEF = + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# doxygen will generate a detailed section even if there is only a brief +# description. +# The default value is: NO. + +ALWAYS_DETAILED_SEC = NO + +# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. +# The default value is: NO. + +INLINE_INHERITED_MEMB = NO + +# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path +# before files name in the file list and in the header files. If set to NO the +# shortest path that makes the file name unique will be used +# The default value is: YES. + +FULL_PATH_NAMES = YES + +# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. +# Stripping is only done if one of the specified strings matches the left-hand +# part of the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which doxygen is run is used as the path to +# strip. +# +# Note that you can specify absolute paths here, but also relative paths, which +# will be relative from the directory where doxygen is started. +# This tag requires that the tag FULL_PATH_NAMES is set to YES. + +STRIP_FROM_PATH = + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the +# path mentioned in the documentation of a class, which tells the reader which +# header file to include in order to use a class. If left blank only the name of +# the header file containing the class definition is used. Otherwise one should +# specify the list of include paths that are normally passed to the compiler +# using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but +# less readable) file names. This can be useful is your file systems doesn't +# support long names like on DOS, Mac, or CD-ROM. +# The default value is: NO. + +SHORT_NAMES = NO + +# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the +# first line (until the first dot) of a Javadoc-style comment as the brief +# description. If set to NO, the Javadoc-style will behave just like regular Qt- +# style comments (thus requiring an explicit @brief command for a brief +# description.) +# The default value is: NO. + +JAVADOC_AUTOBRIEF = NO + +# If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line +# such as +# /*************** +# as being the beginning of a Javadoc-style comment "banner". If set to NO, the +# Javadoc-style will behave just like regular comments and it will not be +# interpreted by doxygen. +# The default value is: NO. + +JAVADOC_BANNER = NO + +# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first +# line (until the first dot) of a Qt-style comment as the brief description. If +# set to NO, the Qt-style will behave just like regular Qt-style comments (thus +# requiring an explicit \brief command for a brief description.) +# The default value is: NO. + +QT_AUTOBRIEF = NO + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a +# multi-line C++ special comment block (i.e. a block of //! or /// comments) as +# a brief description. This used to be the default behavior. The new default is +# to treat a multi-line C++ comment block as a detailed description. Set this +# tag to YES if you prefer the old behavior instead. +# +# Note that setting this tag to YES also means that rational rose comments are +# not recognized any more. +# The default value is: NO. + +MULTILINE_CPP_IS_BRIEF = NO + +# By default Python docstrings are displayed as preformatted text and doxygen's +# special commands cannot be used. By setting PYTHON_DOCSTRING to NO the +# doxygen's special commands can be used and the contents of the docstring +# documentation blocks is shown as doxygen documentation. +# The default value is: YES. + +PYTHON_DOCSTRING = YES + +# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the +# documentation from any documented member that it re-implements. +# The default value is: YES. + +INHERIT_DOCS = YES + +# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new +# page for each member. If set to NO, the documentation of a member will be part +# of the file/class/namespace that contains it. +# The default value is: NO. + +SEPARATE_MEMBER_PAGES = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen +# uses this value to replace tabs by spaces in code fragments. +# Minimum value: 1, maximum value: 16, default value: 4. + +TAB_SIZE = 2 + +# This tag can be used to specify a number of aliases that act as commands in +# the documentation. An alias has the form: +# name=value +# For example adding +# "sideeffect=@par Side Effects:^^" +# will allow you to put the command \sideeffect (or @sideeffect) in the +# documentation, which will result in a user-defined paragraph with heading +# "Side Effects:". Note that you cannot put \n's in the value part of an alias +# to insert newlines (in the resulting output). You can put ^^ in the value part +# of an alias to insert a newline as if a physical newline was in the original +# file. When you need a literal { or } or , in the value part of an alias you +# have to escape them by means of a backslash (\), this can lead to conflicts +# with the commands \{ and \} for these it is advised to use the version @{ and +# @} or use a double escape (\\{ and \\}) + +ALIASES = + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources +# only. Doxygen will then generate output that is more tailored for C. For +# instance, some of the names that are used will be different. The list of all +# members will be omitted, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or +# Python sources only. Doxygen will then generate output that is more tailored +# for that language. For instance, namespaces will be presented as packages, +# qualified scopes will look different, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_JAVA = NO + +# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran +# sources. Doxygen will then generate output that is tailored for Fortran. +# The default value is: NO. + +OPTIMIZE_FOR_FORTRAN = NO + +# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL +# sources. Doxygen will then generate output that is tailored for VHDL. +# The default value is: NO. + +OPTIMIZE_OUTPUT_VHDL = NO + +# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice +# sources only. Doxygen will then generate output that is more tailored for that +# language. For instance, namespaces will be presented as modules, types will be +# separated into more groups, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_SLICE = NO + +# Doxygen selects the parser to use depending on the extension of the files it +# parses. With this tag you can assign which parser to use for a given +# extension. Doxygen has a built-in mapping, but you can override or extend it +# using this tag. The format is ext=language, where ext is a file extension, and +# language is one of the parsers supported by doxygen: IDL, Java, JavaScript, +# Csharp (C#), C, C++, Lex, D, PHP, md (Markdown), Objective-C, Python, Slice, +# VHDL, Fortran (fixed format Fortran: FortranFixed, free formatted Fortran: +# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser +# tries to guess whether the code is fixed or free formatted code, this is the +# default for Fortran type files). For instance to make doxygen treat .inc files +# as Fortran files (default is PHP), and .f files as C (default is Fortran), +# use: inc=Fortran f=C. +# +# Note: For files without extension you can use no_extension as a placeholder. +# +# Note that for custom extensions you also need to set FILE_PATTERNS otherwise +# the files are not read by doxygen. When specifying no_extension you should add +# * to the FILE_PATTERNS. +# +# Note see also the list of default file extension mappings. + +EXTENSION_MAPPING = + +# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments +# according to the Markdown format, which allows for more readable +# documentation. See https://daringfireball.net/projects/markdown/ for details. +# The output of markdown processing is further processed by doxygen, so you can +# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in +# case of backward compatibilities issues. +# The default value is: YES. + +MARKDOWN_SUPPORT = YES + +# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up +# to that level are automatically included in the table of contents, even if +# they do not have an id attribute. +# Note: This feature currently applies only to Markdown headings. +# Minimum value: 0, maximum value: 99, default value: 5. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +# 1: only the top-level headings of the Markdown pages reach the navigation +# tree, which keeps it two levels deep (page, then its sections). +TOC_INCLUDE_HEADINGS = 1 + +# The MARKDOWN_ID_STYLE tag can be used to specify the algorithm used to +# generate identifiers for the Markdown headings. Note: Every identifier is +# unique. +# Possible values are: DOXYGEN use a fixed 'autotoc_md' string followed by a +# sequence number starting at 0 and GITHUB use the lower case version of title +# with any whitespace replaced by '-' and punctuation characters removed. +# The default value is: DOXYGEN. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +MARKDOWN_ID_STYLE = DOXYGEN + +# When enabled doxygen tries to link words that correspond to documented +# classes, or namespaces to their corresponding documentation. Such a link can +# be prevented in individual cases by putting a % sign in front of the word or +# globally by setting AUTOLINK_SUPPORT to NO. +# The default value is: YES. + +AUTOLINK_SUPPORT = YES + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should set this +# tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); +# versus func(std::string) {}). This also make the inheritance and collaboration +# diagrams that involve STL classes more complete and accurate. +# The default value is: NO. + +BUILTIN_STL_SUPPORT = NO + +# If you use Microsoft's C++/CLI language, you should set this option to YES to +# enable parsing support. +# The default value is: NO. + +CPP_CLI_SUPPORT = NO + +# Set the SIP_SUPPORT tag to YES if your project consists of sip (see: +# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen +# will parse them like normal C++ but will assume all classes use public instead +# of private inheritance when no explicit protection keyword is present. +# The default value is: NO. + +SIP_SUPPORT = NO + +# For Microsoft's IDL there are propget and propput attributes to indicate +# getter and setter methods for a property. Setting this option to YES will make +# doxygen to replace the get and set methods by a property in the documentation. +# This will only work if the methods are indeed getting or setting a simple +# type. If this is not the case, or you want to show the methods anyway, you +# should set this option to NO. +# The default value is: YES. + +IDL_PROPERTY_SUPPORT = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES then doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. +# The default value is: NO. + +DISTRIBUTE_GROUP_DOC = NO + +# If one adds a struct or class to a group and this option is enabled, then also +# any nested class or struct is added to the same group. By default this option +# is disabled and one has to add nested compounds explicitly via \ingroup. +# The default value is: NO. + +GROUP_NESTED_COMPOUNDS = NO + +# Set the SUBGROUPING tag to YES to allow class member groups of the same type +# (for instance a group of public functions) to be put as a subgroup of that +# type (e.g. under the Public Functions section). Set it to NO to prevent +# subgrouping. Alternatively, this can be done per class using the +# \nosubgrouping command. +# The default value is: YES. + +SUBGROUPING = YES + +# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions +# are shown inside the group in which they are included (e.g. using \ingroup) +# instead of on a separate page (for HTML and Man pages) or section (for LaTeX +# and RTF). +# +# Note that this feature does not work in combination with +# SEPARATE_MEMBER_PAGES. +# The default value is: NO. + +INLINE_GROUPED_CLASSES = NO + +# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions +# with only public data fields or simple typedef fields will be shown inline in +# the documentation of the scope in which they are defined (i.e. file, +# namespace, or group documentation), provided this scope is documented. If set +# to NO, structs, classes, and unions are shown on a separate page (for HTML and +# Man pages) or section (for LaTeX and RTF). +# The default value is: NO. + +INLINE_SIMPLE_STRUCTS = NO + +# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or +# enum is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically be +# useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. +# The default value is: NO. + +TYPEDEF_HIDES_STRUCT = NO + +# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This +# cache is used to resolve symbols given their name and scope. Since this can be +# an expensive process and often the same symbol appears multiple times in the +# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small +# doxygen will become slower. If the cache is too large, memory is wasted. The +# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range +# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 +# symbols. At the end of a run doxygen will report the cache usage and suggest +# the optimal cache size from a speed point of view. +# Minimum value: 0, maximum value: 9, default value: 0. + +LOOKUP_CACHE_SIZE = 0 + +# The NUM_PROC_THREADS specifies the number of threads doxygen is allowed to use +# during processing. When set to 0 doxygen will based this on the number of +# cores available in the system. You can set it explicitly to a value larger +# than 0 to get more control over the balance between CPU load and processing +# speed. At this moment only the input processing can be done using multiple +# threads. Since this is still an experimental feature the default is set to 1, +# which effectively disables parallel processing. Please report any issues you +# encounter. Generating dot graphs in parallel is controlled by the +# DOT_NUM_THREADS setting. +# Minimum value: 0, maximum value: 32, default value: 1. + +NUM_PROC_THREADS = 1 + +# If the TIMESTAMP tag is set different from NO then each generated page will +# contain the date or date and time when the page was generated. Setting this to +# NO can help when comparing the output of multiple runs. +# Possible values are: YES, NO, DATETIME and DATE. +# The default value is: NO. + +TIMESTAMP = NO + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in +# documentation are documented, even if no documentation was available. Private +# class members and static file members will be hidden unless the +# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. +# Note: This will also disable the warnings about undocumented members that are +# normally produced when WARNINGS is set to YES. +# The default value is: NO. + +EXTRACT_ALL = NO + +# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will +# be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIVATE = NO + +# If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual +# methods of a class will be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIV_VIRTUAL = NO + +# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal +# scope will be included in the documentation. +# The default value is: NO. + +EXTRACT_PACKAGE = NO + +# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be +# included in the documentation. +# The default value is: NO. + +EXTRACT_STATIC = NO + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined +# locally in source files will be included in the documentation. If set to NO, +# only classes defined in header files are included. Does not have any effect +# for Java sources. +# The default value is: YES. + +EXTRACT_LOCAL_CLASSES = YES + +# This flag is only useful for Objective-C code. If set to YES, local methods, +# which are defined in the implementation section but not in the interface are +# included in the documentation. If set to NO, only methods in the interface are +# included. +# The default value is: NO. + +EXTRACT_LOCAL_METHODS = NO + +# If this flag is set to YES, the members of anonymous namespaces will be +# extracted and appear in the documentation as a namespace called +# 'anonymous_namespace{file}', where file will be replaced with the base name of +# the file that contains the anonymous namespace. By default anonymous namespace +# are hidden. +# The default value is: NO. + +EXTRACT_ANON_NSPACES = NO + +# If this flag is set to YES, the name of an unnamed parameter in a declaration +# will be determined by the corresponding definition. By default unnamed +# parameters remain unnamed in the output. +# The default value is: YES. + +RESOLVE_UNNAMED_PARAMS = YES + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all +# undocumented members inside documented classes or files. If set to NO these +# members will be included in the various overviews, but no documentation +# section is generated. This option has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. If set +# to NO, these classes will be included in the various overviews. This option +# will also hide undocumented C++ concepts if enabled. This option has no effect +# if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_CLASSES = NO + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend +# declarations. If set to NO, these declarations will be included in the +# documentation. +# The default value is: NO. + +HIDE_FRIEND_COMPOUNDS = NO + +# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any +# documentation blocks found inside the body of a function. If set to NO, these +# blocks will be appended to the function's detailed documentation block. +# The default value is: NO. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation that is typed after a +# \internal command is included. If the tag is set to NO then the documentation +# will be excluded. Set it to YES to include the internal documentation. +# The default value is: NO. + +INTERNAL_DOCS = NO + +# With the correct setting of option CASE_SENSE_NAMES doxygen will better be +# able to match the capabilities of the underlying filesystem. In case the +# filesystem is case sensitive (i.e. it supports files in the same directory +# whose names only differ in casing), the option must be set to YES to properly +# deal with such files in case they appear in the input. For filesystems that +# are not case sensitive the option should be set to NO to properly deal with +# output files written for symbols that only differ in casing, such as for two +# classes, one named CLASS and the other named Class, and to also support +# references to files without having to specify the exact matching casing. On +# Windows (including Cygwin) and MacOS, users should typically set this option +# to NO, whereas on Linux or other Unix flavors it should typically be set to +# YES. +# Possible values are: SYSTEM, NO and YES. +# The default value is: SYSTEM. + +CASE_SENSE_NAMES = YES + +# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with +# their full class and namespace scopes in the documentation. If set to YES, the +# scope will be hidden. +# The default value is: NO. + +HIDE_SCOPE_NAMES = NO + +# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will +# append additional text to a page's title, such as Class Reference. If set to +# YES the compound reference will be hidden. +# The default value is: NO. + +HIDE_COMPOUND_REFERENCE= NO + +# If the SHOW_HEADERFILE tag is set to YES then the documentation for a class +# will show which file needs to be included to use the class. +# The default value is: YES. + +SHOW_HEADERFILE = YES + +# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of +# the files that are included by a file in the documentation of that file. +# The default value is: YES. + +SHOW_INCLUDE_FILES = YES + +# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each +# grouped member an include statement to the documentation, telling the reader +# which file to include in order to use the member. +# The default value is: NO. + +SHOW_GROUPED_MEMB_INC = NO + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include +# files with double quotes in the documentation rather than with sharp brackets. +# The default value is: NO. + +FORCE_LOCAL_INCLUDES = NO + +# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the +# documentation for inline members. +# The default value is: YES. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the +# (detailed) documentation of file and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. +# The default value is: YES. + +SORT_MEMBER_DOCS = NO + +# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief +# descriptions of file, namespace and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. Note that +# this will also influence the order of the classes in the class list. +# The default value is: NO. + +SORT_BRIEF_DOCS = NO + +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the +# (brief and detailed) documentation of class members so that constructors and +# destructors are listed first. If set to NO the constructors will appear in the +# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. +# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief +# member documentation. +# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting +# detailed member documentation. +# The default value is: NO. + +SORT_MEMBERS_CTORS_1ST = NO + +# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy +# of group names into alphabetical order. If set to NO the group names will +# appear in their defined order. +# The default value is: NO. + +SORT_GROUP_NAMES = NO + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by +# fully-qualified names, including namespaces. If set to NO, the class list will +# be sorted only by class name, not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the alphabetical +# list. +# The default value is: NO. + +SORT_BY_SCOPE_NAME = NO + +# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper +# type resolution of all parameters of a function it will reject a match between +# the prototype and the implementation of a member function even if there is +# only one candidate or it is obvious which candidate to choose by doing a +# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still +# accept a match between prototype and implementation in such cases. +# The default value is: NO. + +STRICT_PROTO_MATCHING = NO + +# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo +# list. This list is created by putting \todo commands in the documentation. +# The default value is: YES. + +# Off: \todo notes are for whoever works on the code, not for the published API +# reference (they are still visible in the sources). +GENERATE_TODOLIST = NO + +# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test +# list. This list is created by putting \test commands in the documentation. +# The default value is: YES. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug +# list. This list is created by putting \bug commands in the documentation. +# The default value is: YES. + +GENERATE_BUGLIST = YES + +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO) +# the deprecated list. This list is created by putting \deprecated commands in +# the documentation. +# The default value is: YES. + +GENERATE_DEPRECATEDLIST= YES + +# The ENABLED_SECTIONS tag can be used to enable conditional documentation +# sections, marked by \if ... \endif and \cond +# ... \endcond blocks. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the +# initial value of a variable or macro / define can have for it to appear in the +# documentation. If the initializer consists of more lines than specified here +# it will be hidden. Use a value of 0 to hide initializers completely. The +# appearance of the value of individual variables and macros / defines can be +# controlled using \showinitializer or \hideinitializer command in the +# documentation regardless of this setting. +# Minimum value: 0, maximum value: 10000, default value: 30. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at +# the bottom of the documentation of classes and structs. If set to YES, the +# list will mention the files that were used to generate the documentation. +# The default value is: YES. + +SHOW_USED_FILES = YES + +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This +# will remove the Files entry from the Quick Index and from the Folder Tree View +# (if specified). +# The default value is: YES. + +SHOW_FILES = YES + +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces +# page. This will remove the Namespaces entry from the Quick Index and from the +# Folder Tree View (if specified). +# The default value is: YES. + +SHOW_NAMESPACES = YES + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# doxygen should invoke to get the current version for each file (typically from +# the version control system). Doxygen will invoke the program by executing (via +# popen()) the command command input-file, where command is the value of the +# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided +# by doxygen. Whatever the program writes to standard output is used as the file +# version. For an example see the documentation. + +FILE_VERSION_FILTER = + +# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed +# by doxygen. The layout file controls the global structure of the generated +# output files in an output format independent way. To create the layout file +# that represents doxygen's defaults, run doxygen with the -l option. You can +# optionally specify a file name after the option, if omitted DoxygenLayout.xml +# will be used as the name of the layout file. See also section "Changing the +# layout of pages" for information. +# +# Note that if you run doxygen from a directory containing a file called +# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE +# tag is left empty. + +LAYOUT_FILE = + +# The CITE_BIB_FILES tag can be used to specify one or more bib files containing +# the reference definitions. This must be a list of .bib files. The .bib +# extension is automatically appended if omitted. This requires the bibtex tool +# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info. +# For LaTeX the style of the bibliography can be controlled using +# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the +# search path. See also \cite for info how to create references. + +CITE_BIB_FILES = + +#--------------------------------------------------------------------------- +# Configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated to +# standard output by doxygen. If QUIET is set to YES this implies that the +# messages are off. +# The default value is: NO. + +QUIET = NO + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES +# this implies that the warnings are on. +# +# Tip: Turn warnings on while writing the documentation. +# The default value is: YES. + +WARNINGS = YES + +# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate +# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: YES. + +WARN_IF_UNDOCUMENTED = YES + +# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for +# potential errors in the documentation, such as documenting some parameters in +# a documented function twice, or documenting parameters that don't exist or +# using markup commands wrongly. +# The default value is: YES. + +WARN_IF_DOC_ERROR = YES + +# If WARN_IF_INCOMPLETE_DOC is set to YES, doxygen will warn about incomplete +# function parameter documentation. If set to NO, doxygen will accept that some +# parameters have no documentation without warning. +# The default value is: YES. + +WARN_IF_INCOMPLETE_DOC = YES + +# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that +# are documented, but have no documentation for their parameters or return +# value. If set to NO, doxygen will only warn about wrong parameter +# documentation, but not about the absence of documentation. If EXTRACT_ALL is +# set to YES then this flag will automatically be disabled. See also +# WARN_IF_INCOMPLETE_DOC +# The default value is: NO. + +WARN_NO_PARAMDOC = NO + +# If WARN_IF_UNDOC_ENUM_VAL option is set to YES, doxygen will warn about +# undocumented enumeration values. If set to NO, doxygen will accept +# undocumented enumeration values. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: NO. + +WARN_IF_UNDOC_ENUM_VAL = NO + +# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when +# a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS +# then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but +# at the end of the doxygen process doxygen will return with a non-zero status. +# If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS_PRINT then doxygen behaves +# like FAIL_ON_WARNINGS but in case no WARN_LOGFILE is defined doxygen will not +# write the warning messages in between other messages but write them at the end +# of a run, in case a WARN_LOGFILE is defined the warning messages will be +# besides being in the defined file also be shown at the end of a run, unless +# the WARN_LOGFILE is defined as - i.e. standard output (stdout) in that case +# the behavior will remain as with the setting FAIL_ON_WARNINGS. +# Possible values are: NO, YES, FAIL_ON_WARNINGS and FAIL_ON_WARNINGS_PRINT. +# The default value is: NO. + +WARN_AS_ERROR = NO + +# The WARN_FORMAT tag determines the format of the warning messages that doxygen +# can produce. The string should contain the $file, $line, and $text tags, which +# will be replaced by the file and line number from which the warning originated +# and the warning text. Optionally the format may contain $version, which will +# be replaced by the version of the file (if it could be obtained via +# FILE_VERSION_FILTER) +# See also: WARN_LINE_FORMAT +# The default value is: $file:$line: $text. + +WARN_FORMAT = "$file:$line: $text" + +# In the $text part of the WARN_FORMAT command it is possible that a reference +# to a more specific place is given. To make it easier to jump to this place +# (outside of doxygen) the user can define a custom "cut" / "paste" string. +# Example: +# WARN_LINE_FORMAT = "'vi $file +$line'" +# See also: WARN_FORMAT +# The default value is: at line $line of file $file. + +WARN_LINE_FORMAT = "at line $line of file $file" + +# The WARN_LOGFILE tag can be used to specify a file to which warning and error +# messages should be written. If left blank the output is written to standard +# error (stderr). In case the file specified cannot be opened for writing the +# warning and error messages are written to standard error. When as file - is +# specified the warning and error messages are written to standard output +# (stdout). + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag is used to specify the files and/or directories that contain +# documented source files. You may enter file names like myfile.cpp or +# directories like /usr/src/myproject. Separate the files or directories with +# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING +# Note: If this tag is empty the current directory is searched. + +INPUT = doxygen/mainpage.md \ + doxygen/tools.md \ + @RTABMAP_DOXYGEN_PARAMETERS_PAGE@ \ + corelib/include \ + utilite/include \ + build-docs/corelib/src/include + +# This tag can be used to specify the character encoding of the source files +# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses +# libiconv (or the iconv built into libc) for the transcoding. See the libiconv +# documentation (see: +# https://www.gnu.org/software/libiconv/) for the list of possible encodings. +# See also: INPUT_FILE_ENCODING +# The default value is: UTF-8. + +INPUT_ENCODING = UTF-8 + +# This tag can be used to specify the character encoding of the source files +# that doxygen parses The INPUT_FILE_ENCODING tag can be used to specify +# character encoding on a per file pattern basis. Doxygen will compare the file +# name with each pattern and apply the encoding instead of the default +# INPUT_ENCODING) if there is a match. The character encodings are a list of the +# form: pattern=encoding (like *.php=ISO-8859-1). See cfg_input_encoding +# "INPUT_ENCODING" for further information on supported encodings. + +INPUT_FILE_ENCODING = + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and +# *.h) to filter out the source-files in the directories. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# read by doxygen. +# +# Note the list of default checked file patterns might differ from the list of +# default file extension mappings. +# +# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cxxm, +# *.cpp, *.cppm, *.c++, *.c++m, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, +# *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, *.h++, *.ixx, *.l, *.cs, *.d, *.php, +# *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be +# provided as doxygen C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, +# *.f18, *.f, *.for, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice. + +FILE_PATTERNS = + +# The RECURSIVE tag can be used to specify whether or not subdirectories should +# be searched for input files as well. +# The default value is: NO. + +RECURSIVE = YES + +# The EXCLUDE tag can be used to specify files and/or directories that should be +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. +# +# Note that relative paths are relative to the directory from which doxygen is +# run. + +EXCLUDE = + +# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or +# directories that are symbolic links (a Unix file system feature) are excluded +# from the input. +# The default value is: NO. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories for example use the pattern */test/* + +EXCLUDE_PATTERNS = + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the +# output. The symbol name can be a fully qualified name, a word, or if the +# wildcard * is used, a substring. Examples: ANamespace, AClass, +# ANamespace::AClass, ANamespace::*Test + +EXCLUDE_SYMBOLS = + +# The EXAMPLE_PATH tag can be used to specify one or more files or directories +# that contain example code fragments that are included (see the \include +# command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and +# *.h) to filter out the source-files in the directories. If left blank all +# files are included. + +EXAMPLE_PATTERNS = * + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude commands +# irrespective of the value of the RECURSIVE tag. +# The default value is: NO. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or directories +# that contain images that are to be included in the documentation (see the +# \image command). + +IMAGE_PATH = + +# The INPUT_FILTER tag can be used to specify a program that doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command: +# +# +# +# where is the value of the INPUT_FILTER tag, and is the +# name of an input file. Doxygen will then use the output that the filter +# program writes to standard output. If FILTER_PATTERNS is specified, this tag +# will be ignored. +# +# Note that the filter must not add or remove lines; it is applied before the +# code is scanned, but not when the output code is generated. If lines are added +# or removed, the anchors will not be placed correctly. +# +# Note that doxygen will use the data processed and written to standard output +# for further processing, therefore nothing else, like debug statements or used +# commands (so in case of a Windows batch file always use @echo OFF), should be +# written to standard output. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by doxygen. + +INPUT_FILTER = + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. The filters are a list of the form: pattern=filter +# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how +# filters are used. If the FILTER_PATTERNS tag is empty or if none of the +# patterns match the file name, INPUT_FILTER is applied. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by doxygen. + +FILTER_PATTERNS = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER) will also be used to filter the input files that are used for +# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). +# The default value is: NO. + +FILTER_SOURCE_FILES = NO + +# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file +# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and +# it is also possible to disable source filtering for a specific pattern using +# *.ext= (so without naming a filter). +# This tag requires that the tag FILTER_SOURCE_FILES is set to YES. + +FILTER_SOURCE_PATTERNS = + +# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that +# is part of the input, its contents will be placed on the main page +# (index.html). This can be useful if you have a project on for instance GitHub +# and want to reuse the introduction page also for the doxygen output. + +USE_MDFILE_AS_MAINPAGE = doxygen/mainpage.md + +# The Fortran standard specifies that for fixed formatted Fortran code all +# characters from position 72 are to be considered as comment. A common +# extension is to allow longer lines before the automatic comment starts. The +# setting FORTRAN_COMMENT_AFTER will also make it possible that longer lines can +# be processed before the automatic comment starts. +# Minimum value: 7, maximum value: 10000, default value: 72. + +FORTRAN_COMMENT_AFTER = 72 + +#--------------------------------------------------------------------------- +# Configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will be +# generated. Documented entities will be cross-referenced with these sources. +# +# Note: To get rid of all source code in the generated output, make sure that +# also VERBATIM_HEADERS is set to NO. +# The default value is: NO. + +SOURCE_BROWSER = YES + +# Setting the INLINE_SOURCES tag to YES will include the body of functions, +# classes and enums directly into the documentation. +# The default value is: NO. + +INLINE_SOURCES = NO + +# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any +# special comment blocks from generated source code fragments. Normal C, C++ and +# Fortran comments will always remain visible. +# The default value is: YES. + +STRIP_CODE_COMMENTS = YES + +# If the REFERENCED_BY_RELATION tag is set to YES then for each documented +# entity all documented functions referencing it will be listed. +# The default value is: NO. + +REFERENCED_BY_RELATION = NO + +# If the REFERENCES_RELATION tag is set to YES then for each documented function +# all documented entities called/used by that function will be listed. +# The default value is: NO. + +REFERENCES_RELATION = NO + +# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set +# to YES then the hyperlinks from functions in REFERENCES_RELATION and +# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will +# link to the documentation. +# The default value is: YES. + +REFERENCES_LINK_SOURCE = YES + +# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the +# source code will show a tooltip with additional information such as prototype, +# brief description and links to the definition and documentation. Since this +# will make the HTML file larger and loading of large files a bit slower, you +# can opt to disable this feature. +# The default value is: YES. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +SOURCE_TOOLTIPS = YES + +# If the USE_HTAGS tag is set to YES then the references to source code will +# point to the HTML generated by the htags(1) tool instead of doxygen built-in +# source browser. The htags tool is part of GNU's global source tagging system +# (see https://www.gnu.org/software/global/global.html). You will need version +# 4.8.6 or higher. +# +# To use it do the following: +# - Install the latest version of global +# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file +# - Make sure the INPUT points to the root of the source tree +# - Run doxygen as normal +# +# Doxygen will invoke htags (and that will in turn invoke gtags), so these +# tools must be available from the command line (i.e. in the search path). +# +# The result: instead of the source browser generated by doxygen, the links to +# source code will now point to the output of htags. +# The default value is: NO. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a +# verbatim copy of the header file for each class for which an include is +# specified. Set to NO to disable this. +# See also: Section \class. +# The default value is: YES. + +VERBATIM_HEADERS = YES + +# If the CLANG_ASSISTED_PARSING tag is set to YES then doxygen will use the +# clang parser (see: +# http://clang.llvm.org/) for more accurate parsing at the cost of reduced +# performance. This can be particularly helpful with template rich C++ code for +# which doxygen's built-in parser lacks the necessary type information. +# Note: The availability of this option depends on whether or not doxygen was +# generated with the -Duse_libclang=ON option for CMake. +# The default value is: NO. + +CLANG_ASSISTED_PARSING = NO + +# If the CLANG_ASSISTED_PARSING tag is set to YES and the CLANG_ADD_INC_PATHS +# tag is set to YES then doxygen will add the directory of each input to the +# include path. +# The default value is: YES. +# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. + +CLANG_ADD_INC_PATHS = YES + +# If clang assisted parsing is enabled you can provide the compiler with command +# line options that you would normally use when invoking the compiler. Note that +# the include paths will already be set by doxygen for the files and directories +# specified with INPUT and INCLUDE_PATH. +# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. + +CLANG_OPTIONS = + +# If clang assisted parsing is enabled you can provide the clang parser with the +# path to the directory containing a file called compile_commands.json. This +# file is the compilation database (see: +# http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html) containing the +# options used when the source files were built. This is equivalent to +# specifying the -p option to a clang tool, such as clang-check. These options +# will then be passed to the parser. Any options specified with CLANG_OPTIONS +# will be added as well. +# Note: The availability of this option depends on whether or not doxygen was +# generated with the -Duse_libclang=ON option for CMake. + +CLANG_DATABASE_PATH = + +#--------------------------------------------------------------------------- +# Configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all +# compounds will be generated. Enable this if the project contains a lot of +# classes, structs, unions or interfaces. +# The default value is: YES. + +ALPHABETICAL_INDEX = YES + +# The IGNORE_PREFIX tag can be used to specify a prefix (or a list of prefixes) +# that should be ignored while generating the index headers. The IGNORE_PREFIX +# tag works for classes, function and member names. The entity will be placed in +# the alphabetical list under the first letter of the entity name that remains +# after removing the prefix. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output +# The default value is: YES. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. +# The default directory is: html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_OUTPUT = html + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each +# generated HTML page (for example: .htm, .php, .asp). +# The default value is: .html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a user-defined HTML header file for +# each generated HTML page. If the tag is left blank doxygen will generate a +# standard header. +# +# To get valid HTML the header file that includes any scripts and style sheets +# that doxygen needs, which is dependent on the configuration options used (e.g. +# the setting GENERATE_TREEVIEW). It is highly recommended to start with a +# default header using +# doxygen -w html new_header.html new_footer.html new_stylesheet.css +# YourConfigFile +# and then modify the file new_header.html. See also section "Doxygen usage" +# for information on how to generate the default header that doxygen normally +# uses. +# Note: The header is subject to change so you typically have to regenerate the +# default header when upgrading to a newer version of doxygen. For a description +# of the possible markers and block names see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_HEADER = @RTABMAP_DOXYGEN_HEADER@ + +# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each +# generated HTML page. If the tag is left blank doxygen will generate a standard +# footer. See HTML_HEADER for more information on how to generate a default +# footer and what special commands can be used inside the footer. See also +# section "Doxygen usage" for information on how to generate the default footer +# that doxygen normally uses. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style +# sheet that is used by each HTML page. It can be used to fine-tune the look of +# the HTML output. If left blank doxygen will generate a default style sheet. +# See also section "Doxygen usage" for information on how to generate the style +# sheet that doxygen normally uses. +# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as +# it is more robust and this tag (HTML_STYLESHEET) will in the future become +# obsolete. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_STYLESHEET = + +# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined +# cascading style sheets that are included after the standard style sheets +# created by doxygen. Using this option one can overrule certain style aspects. +# This is preferred over using HTML_STYLESHEET since it does not replace the +# standard style sheet and is therefore more robust against future updates. +# Doxygen will copy the style sheet files to the output directory. +# Note: The order of the extra style sheet files is of importance (e.g. the last +# style sheet in the list overrules the setting of the previous ones in the +# list). +# Note: Since the styling of scrollbars can currently not be overruled in +# Webkit/Chromium, the styling will be left out of the default doxygen.css if +# one or more extra stylesheets have been specified. So if scrollbar +# customization is desired it has to be added explicitly. For an example see the +# documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +# Doxygen Awesome (MIT), fetched by CMake -- empty when BUILD_DOCUMENTATION is +# off. The theme requires GENERATE_TREEVIEW=YES and HTML_COLORSTYLE=LIGHT: it +# brings its own dark mode, driven by the toggle button and the browser. +HTML_EXTRA_STYLESHEET = @RTABMAP_DOXYGEN_STYLESHEETS@ + +# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the HTML output directory. Note +# that these files will be copied to the base HTML output directory. Use the +# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these +# files. In the HTML_STYLESHEET file, use the file name only. Also note that the +# files will be copied as-is; there are no commands or markers available. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_FILES = @PROJECT_SOURCE_DIR@/doxygen/version-switcher.js \ + @RTABMAP_DOXYGEN_AWESOME_TOGGLE@ + +# The HTML_COLORSTYLE tag can be used to specify if the generated HTML output +# should be rendered with a dark or light theme. +# Possible values are: LIGHT always generate light mode output, DARK always +# generate dark mode output, AUTO_LIGHT automatically set the mode according to +# the user preference, use light mode if no preference is set (the default), +# AUTO_DARK automatically set the mode according to the user preference, use +# dark mode if no preference is set and TOGGLE allow to user to switch between +# light and dark mode via a button. +# The default value is: AUTO_LIGHT. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE = LIGHT + +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen +# will adjust the colors in the style sheet and background images according to +# this color. Hue is specified as an angle on a color-wheel, see +# https://en.wikipedia.org/wiki/Hue for more information. For instance the value +# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 +# purple, and 360 is red again. +# Minimum value: 0, maximum value: 359, default value: 220. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_HUE = 220 + +# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors +# in the HTML output. For a value of 0 the output will use gray-scales only. A +# value of 255 will produce the most vivid colors. +# Minimum value: 0, maximum value: 255, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_SAT = 100 + +# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the +# luminance component of the colors in the HTML output. Values below 100 +# gradually make the output lighter, whereas values above 100 make the output +# darker. The value divided by 100 is the actual gamma applied, so 80 represents +# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not +# change the gamma. +# Minimum value: 40, maximum value: 240, default value: 80. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_GAMMA = 80 + +# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML +# documentation will contain a main index with vertical navigation menus that +# are dynamically created via JavaScript. If disabled, the navigation index will +# consists of multiple levels of tabs that are statically embedded in every HTML +# page. Disable this option to support browsers that do not have JavaScript, +# like the Qt help browser. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_MENUS = YES + +# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML +# documentation will contain sections that can be hidden and shown after the +# page has loaded. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_SECTIONS = NO + +# If the HTML_CODE_FOLDING tag is set to YES then classes and functions can be +# dynamically folded and expanded in the generated HTML source code. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_CODE_FOLDING = YES + +# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries +# shown in the various tree structured indices initially; the user can expand +# and collapse entries dynamically later on. Doxygen will expand the tree to +# such a level that at most the specified number of entries are visible (unless +# a fully collapsed tree already exceeds this amount). So setting the number of +# entries 1 will produce a full collapsed tree by default. 0 is a special value +# representing an infinite number of entries and will result in a full expanded +# tree by default. +# Minimum value: 0, maximum value: 9999, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_INDEX_NUM_ENTRIES = 100 + +# If the GENERATE_DOCSET tag is set to YES, additional index files will be +# generated that can be used as input for Apple's Xcode 3 integrated development +# environment (see: +# https://developer.apple.com/xcode/), introduced with OSX 10.5 (Leopard). To +# create a documentation set, doxygen will generate a Makefile in the HTML +# output directory. Running make will produce the docset in that directory and +# running make install will install the docset in +# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at +# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy +# genXcode/_index.html for more information. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_DOCSET = NO + +# This tag determines the name of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# The default value is: Doxygen generated docs. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDNAME = "Doxygen generated docs" + +# This tag determines the URL of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDURL = + +# This tag specifies a string that should uniquely identify the documentation +# set bundle. This should be a reverse domain-name style string, e.g. +# com.mycompany.MyDocSet. Doxygen will append .docset to the name. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_BUNDLE_ID = org.doxygen.Project + +# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify +# the documentation publisher. This should be a reverse domain-name style +# string, e.g. com.mycompany.MyDocSet.documentation. +# The default value is: org.doxygen.Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_ID = org.doxygen.Publisher + +# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. +# The default value is: Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_NAME = Publisher + +# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three +# additional HTML index files: index.hhp, index.hhc, and index.hhk. The +# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop +# on Windows. In the beginning of 2021 Microsoft took the original page, with +# a.o. the download links, offline the HTML help workshop was already many years +# in maintenance mode). You can download the HTML help workshop from the web +# archives at Installation executable (see: +# http://web.archive.org/web/20160201063255/http://download.microsoft.com/downlo +# ad/0/A/9/0A939EF6-E31C-430F-A3DF-DFAE7960D564/htmlhelp.exe). +# +# The HTML Help Workshop contains a compiler that can convert all HTML output +# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML +# files are now used as the Windows 98 help format, and will replace the old +# Windows help format (.hlp) on all Windows platforms in the future. Compressed +# HTML files also contain an index, a table of contents, and you can search for +# words in the documentation. The HTML workshop also contains a viewer for +# compressed HTML files. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_HTMLHELP = NO + +# The CHM_FILE tag can be used to specify the file name of the resulting .chm +# file. You can add a path in front of the file if the result should not be +# written to the html output directory. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_FILE = + +# The HHC_LOCATION tag can be used to specify the location (absolute path +# including file name) of the HTML help compiler (hhc.exe). If non-empty, +# doxygen will try to run the HTML help compiler on the generated index.hhp. +# The file has to be specified with full path. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +HHC_LOCATION = + +# The GENERATE_CHI flag controls if a separate .chi index file is generated +# (YES) or that it should be included in the main .chm file (NO). +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +GENERATE_CHI = NO + +# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc) +# and project file content. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_INDEX_ENCODING = + +# The BINARY_TOC flag controls whether a binary table of contents is generated +# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it +# enables the Previous and Next buttons. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members to +# the table of contents of the HTML help documentation and to the tree view. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +TOC_EXPAND = NO + +# The SITEMAP_URL tag is used to specify the full URL of the place where the +# generated documentation will be placed on the server by the user during the +# deployment of the documentation. The generated sitemap is called sitemap.xml +# and placed on the directory specified by HTML_OUTPUT. In case no SITEMAP_URL +# is specified no sitemap is generated. For information about the sitemap +# protocol see https://www.sitemaps.org +# This tag requires that the tag GENERATE_HTML is set to YES. + +SITEMAP_URL = + +# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and +# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that +# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help +# (.qch) of the generated HTML documentation. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_QHP = NO + +# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify +# the file name of the resulting .qch file. The path specified is relative to +# the HTML output folder. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QCH_FILE = + +# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help +# Project output. For more information please see Qt Help Project / Namespace +# (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace). +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_NAMESPACE = org.doxygen.Project + +# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt +# Help Project output. For more information please see Qt Help Project / Virtual +# Folders (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual-folders). +# The default value is: doc. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_VIRTUAL_FOLDER = doc + +# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom +# filter to add. For more information please see Qt Help Project / Custom +# Filters (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_NAME = + +# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the +# custom filter to add. For more information please see Qt Help Project / Custom +# Filters (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_ATTRS = + +# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this +# project's filter section matches. Qt Help Project / Filter Attributes (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_SECT_FILTER_ATTRS = + +# The QHG_LOCATION tag can be used to specify the location (absolute path +# including file name) of Qt's qhelpgenerator. If non-empty doxygen will try to +# run qhelpgenerator on the generated .qhp file. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHG_LOCATION = + +# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be +# generated, together with the HTML files, they form an Eclipse help plugin. To +# install this plugin and make it available under the help contents menu in +# Eclipse, the contents of the directory containing the HTML and XML files needs +# to be copied into the plugins directory of eclipse. The name of the directory +# within the plugins directory should be the same as the ECLIPSE_DOC_ID value. +# After copying Eclipse needs to be restarted before the help appears. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_ECLIPSEHELP = NO + +# A unique identifier for the Eclipse help plugin. When installing the plugin +# the directory name containing the HTML and XML files should also have this +# name. Each documentation set should have its own identifier. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. + +ECLIPSE_DOC_ID = org.doxygen.Project + +# If you want full control over the layout of the generated HTML pages it might +# be necessary to disable the index and replace it with your own. The +# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top +# of each HTML page. A value of NO enables the index and the value YES disables +# it. Since the tabs in the index contain the same information as the navigation +# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +DISABLE_INDEX = NO + +# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index +# structure should be generated to display hierarchical information. If the tag +# value is set to YES, a side panel will be generated containing a tree-like +# index structure (just like the one that is generated for HTML Help). For this +# to work a browser that supports JavaScript, DHTML, CSS and frames is required +# (i.e. any modern browser). Windows users are probably better off using the +# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can +# further fine tune the look of the index (see "Fine-tuning the output"). As an +# example, the default style sheet generated by doxygen has an example that +# shows how to put an image at the root of the tree instead of the PROJECT_NAME. +# Since the tree basically has the same information as the tab index, you could +# consider setting DISABLE_INDEX to YES when enabling this option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_TREEVIEW = YES + +# When both GENERATE_TREEVIEW and DISABLE_INDEX are set to YES, then the +# FULL_SIDEBAR option determines if the side bar is limited to only the treeview +# area (value NO) or if it should extend to the full height of the window (value +# YES). Setting this to YES gives a layout similar to +# https://docs.readthedocs.io with more room for contents, but less room for the +# project logo, title, and description. If either GENERATE_TREEVIEW or +# DISABLE_INDEX is set to NO, this option has no effect. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FULL_SIDEBAR = NO + +# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that +# doxygen will group on one line in the generated HTML documentation. +# +# Note that a value of 0 will completely suppress the enum values from appearing +# in the overview section. +# Minimum value: 0, maximum value: 20, default value: 4. +# This tag requires that the tag GENERATE_HTML is set to YES. + +ENUM_VALUES_PER_LINE = 4 + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used +# to set the initial width (in pixels) of the frame in which the tree is shown. +# Minimum value: 0, maximum value: 1500, default value: 250. +# This tag requires that the tag GENERATE_HTML is set to YES. + +TREEVIEW_WIDTH = @RTABMAP_DOXYGEN_TREEVIEW_WIDTH@ + +# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to +# external symbols imported via tag files in a separate window. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +EXT_LINKS_IN_WINDOW = NO + +# If the OBFUSCATE_EMAILS tag is set to YES, doxygen will obfuscate email +# addresses. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +OBFUSCATE_EMAILS = YES + +# If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg +# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see +# https://inkscape.org) to generate formulas as SVG images instead of PNGs for +# the HTML output. These images will generally look nicer at scaled resolutions. +# Possible values are: png (the default) and svg (looks nicer but requires the +# pdf2svg or inkscape tool). +# The default value is: png. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FORMULA_FORMAT = png + +# Use this tag to change the font size of LaTeX formulas included as images in +# the HTML documentation. When you change the font size after a successful +# doxygen run you need to manually remove any form_*.png images from the HTML +# output directory to force them to be regenerated. +# Minimum value: 8, maximum value: 50, default value: 10. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_FONTSIZE = 10 + +# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands +# to create new LaTeX commands to be used in formulas as building blocks. See +# the section "Including formulas" for details. + +FORMULA_MACROFILE = + +# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see +# https://www.mathjax.org) which uses client side JavaScript for the rendering +# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX +# installed or if you want to formulas look prettier in the HTML output. When +# enabled you may also need to install MathJax separately and configure the path +# to it using the MATHJAX_RELPATH option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +USE_MATHJAX = NO + +# With MATHJAX_VERSION it is possible to specify the MathJax version to be used. +# Note that the different versions of MathJax have different requirements with +# regards to the different settings, so it is possible that also other MathJax +# settings have to be changed when switching between the different MathJax +# versions. +# Possible values are: MathJax_2 and MathJax_3. +# The default value is: MathJax_2. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_VERSION = MathJax_2 + +# When MathJax is enabled you can set the default output format to be used for +# the MathJax output. For more details about the output format see MathJax +# version 2 (see: +# http://docs.mathjax.org/en/v2.7-latest/output.html) and MathJax version 3 +# (see: +# http://docs.mathjax.org/en/latest/web/components/output.html). +# Possible values are: HTML-CSS (which is slower, but has the best +# compatibility. This is the name for Mathjax version 2, for MathJax version 3 +# this will be translated into chtml), NativeMML (i.e. MathML. Only supported +# for NathJax 2. For MathJax version 3 chtml will be used instead.), chtml (This +# is the name for Mathjax version 3, for MathJax version 2 this will be +# translated into HTML-CSS) and SVG. +# The default value is: HTML-CSS. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_FORMAT = HTML-CSS + +# When MathJax is enabled you need to specify the location relative to the HTML +# output directory using the MATHJAX_RELPATH option. The destination directory +# should contain the MathJax.js script. For instance, if the mathjax directory +# is located at the same level as the HTML output directory, then +# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax +# Content Delivery Network so you can quickly see the result without installing +# MathJax. However, it is strongly recommended to install a local copy of +# MathJax from https://www.mathjax.org before deployment. The default value is: +# - in case of MathJax version 2: https://cdn.jsdelivr.net/npm/mathjax@2 +# - in case of MathJax version 3: https://cdn.jsdelivr.net/npm/mathjax@3 +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_RELPATH = https://cdn.jsdelivr.net/npm/mathjax@2 + +# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax +# extension names that should be enabled during MathJax rendering. For example +# for MathJax version 2 (see +# https://docs.mathjax.org/en/v2.7-latest/tex.html#tex-and-latex-extensions): +# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols +# For example for MathJax version 3 (see +# http://docs.mathjax.org/en/latest/input/tex/extensions/index.html): +# MATHJAX_EXTENSIONS = ams +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_EXTENSIONS = + +# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces +# of code that will be used on startup of the MathJax code. See the MathJax site +# (see: +# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. For an +# example see the documentation. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_CODEFILE = + +# When the SEARCHENGINE tag is enabled doxygen will generate a search box for +# the HTML output. The underlying search engine uses javascript and DHTML and +# should work on any modern browser. Note that when using HTML help +# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) +# there is already a search function so this one should typically be disabled. +# For large projects the javascript based search engine can be slow, then +# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to +# search using the keyboard; to jump to the search box use + S +# (what the is depends on the OS and browser, but it is typically +# , /