Skip to content

Conversation

@QueenofUSSR
Copy link

Pull Request Readiness Checklist

See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request

  • I agree to contribute to the project under Apache 2 License.
  • To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV
  • The PR is proposed to the proper branch
  • There is a reference to the original bug report and related work
  • There is accuracy test, performance test and test data in opencv_extra repository, if applicable
    Patch to opencv_extra has the same branch name.
  • The feature is well documented and sample code can be built with the project CMake

Summary

This PR adds the SLAM module (slam) providing a compact visual odometry and small-scale SLAM toolkit. The module includes feature extraction, matching, two-view initialization, pose estimation, local mapping (MapManager / MapPoint), a simple optimizer (local BA with SFM fallback and optional g2o support), visualizer/top-down trajectory, localizer (PnP-based relocalization), and utilities for loading image sequences and intrinsics.

Motivation

Offer a lightweight, self-contained SLAM implementation that demonstrates how to use OpenCV building blocks for VO/SLAM research and examples. Useful as an educational reference and quick prototyping base for researchers who want to extend or compare with other methods (ORB-SLAM family, etc.). It also provides a straightforward path to enable g2o when available for robust BA.

What changed / what’s included

slam.hpp — module aggregator
vo.hpp — VisualOdometry wrapper
data_loader.hpp — sequence loader with YAML intrinsics parsing
feature.hpp — FeatureExtractor (ORB + ANMS + flow-aware selection)
matcher.hpp — Matcher (ratio test, bucketing, mutual check)
initializer.hpp — Two-view initializer using H/F decomposition + triagulation
pose.hpp — PoseEstimator (essential matrix + recoverPose)
map.hpp & keyframe.hpp — MapManager, MapPoint, KeyFrame
localizer.hpp — PnP-based relocalizer
optimizer.hpp — Optimizer (local BA using either g2o or OpenCV-based SFM fallback)
visualizer.hpp — simple frame/top-down visualization and trajectory saving
Implementation (under src/):
vo.cpp — main VisualOdometry run loop, backend thread for local BA, CSV diagnostics, initialization + tracking + triangulation pipeline
data_loader.cpp — filesystem/glob image enumerator, optional sensor.yaml intrinsics parsing (small comment fix in this PR)
feature.cpp, matcher.cpp, initializer.cpp, pose.cpp, optimizer.cpp, visualizer.cpp, localizer.cpp — corresponding implementations described above

@abhishek-gola
Copy link

abhishek-gola commented Dec 5, 2025

FAILED: modules/slam/CMakeFiles/opencv_slam.dir/src/data_loader.cpp.o 
/usr/bin/ccache /usr/bin/c++  -DCVAPI_EXPORTS -D_USE_MATH_DEFINES -D__OPENCV_BUILD=1 -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS -D__STDC_LIMIT_MACROS -I/home/ci/opencv/hal/ipp/include -I3rdparty/ippicv/ippicv_lnx/icv/include -I3rdparty/ippicv/ippicv_lnx/iw/include -I/home/ci/opencv/3rdparty/dlpack/include -I/home/ci/opencv_contrib/modules/slam/include -Imodules/slam -I/home/ci/opencv_contrib/modules/cudev/include -I/home/ci/opencv/modules/core/include -I/home/ci/opencv_contrib/modules/cudaarithm/include -I/home/ci/opencv/modules/flann/include -I/home/ci/opencv/modules/imgproc/include -I/home/ci/opencv/modules/ml/include -I/home/ci/opencv/modules/dnn/include -I/home/ci/opencv/modules/features2d/include -I/home/ci/opencv/modules/imgcodecs/include -I/home/ci/opencv/modules/videoio/include -I/home/ci/opencv/modules/calib3d/include -I/home/ci/opencv/modules/highgui/include -I/home/ci/opencv_contrib/modules/shape/include -I/home/ci/opencv/modules/video/include -I/home/ci/opencv_contrib/modules/xfeatures2d/include -I/home/ci/opencv_contrib/modules/sfm/include -isystem . -isystem /usr/include/eigen3 -fsigned-char -W -Wall -Wreturn-type -Wnon-virtual-dtor -Waddress -Wsequence-point -Wformat -Wformat-security -Wmissing-declarations -Wundef -Winit-self -Wpointer-arith -Wshadow -Wsign-promo -Wuninitialized -Wsuggest-override -Wno-delete-non-virtual-dtor -Wno-comment -Wimplicit-fallthrough=3 -Wno-strict-overflow -fdiagnostics-show-option -pthread -fomit-frame-pointer -ffunction-sections -fdata-sections  -msse3 -fvisibility=hidden -fvisibility-inlines-hidden -O3 -DNDEBUG  -DNDEBUG -fPIC   -std=c++17 -MD -MT modules/slam/CMakeFiles/opencv_slam.dir/src/data_loader.cpp.o -MF modules/slam/CMakeFiles/opencv_slam.dir/src/data_loader.cpp.o.d -o modules/slam/CMakeFiles/opencv_slam.dir/src/data_loader.cpp.o -c /home/ci/opencv_contrib/modules/slam/src/data_loader.cpp
/home/ci/opencv_contrib/modules/slam/src/data_loader.cpp:7:6: error: invalid preprocessing directive #namespace
    7 | #    namespace fs = std::filesystem;
      |      ^~~~~~~~~
/home/ci/opencv_contrib/modules/slam/src/data_loader.cpp: In constructor 'cv::vo::DataLoader::DataLoader(const string&)':
/home/ci/opencv_contrib/modules/slam/src/data_loader.cpp:33:9: error: 'fs' has not been declared
   33 |         fs::path p(imageDir);
      |         ^~
/home/ci/opencv_contrib/modules/slam/src/data_loader.cpp:34:13: error: 'fs' has not been declared
   34 |         if(!fs::exists(p) || !fs::is_directory(p)){
      |             ^~
/home/ci/opencv_contrib/modules/slam/src/data_loader.cpp:34:24: error: 'p' was not declared in this scope
   34 |         if(!fs::exists(p) || !fs::is_directory(p)){
      |                        ^
/home/ci/opencv_contrib/modules/slam/src/data_loader.cpp:34:31: error: 'fs' has not been declared
   34 |         if(!fs::exists(p) || !fs::is_directory(p)){
      |                               ^~
/home/ci/opencv_contrib/modules/slam/src/data_loader.cpp:67:5: error: 'fs' has not been declared
   67 |     fs::path p(imageDir);
      |     ^~
/home/ci/opencv_contrib/modules/slam/src/data_loader.cpp:68:26: error: 'p' was not declared in this scope
   68 |     std::string yaml1 = (p / "sensor.yaml").string();
      |                          ^
[2987/3796] Building CXX object modules/sfm/src/libmv/libmv/correspondence/CMakeFiles/opencv.sfm.correspondence.dir/nRobustViewMatching.cc.o
[2988/3796] Building CXX object modules/slam/CMakeFiles/opencv_slam.dir/src/matcher.cpp.o
/home/ci/opencv_contrib/modules/slam/src/matcher.cpp: In member function 'void cv::vo::Matcher::match(const cv::Mat&, const cv::Mat&, const std::vector<cv::KeyPoint>&, const std::vector<cv::KeyPoint>&, std::vector<cv::DMatch>&, int, int, int, int, int, int, bool, bool)':
/home/ci/opencv_contrib/modules/slam/src/matcher.cpp:26:50: warning: unused parameter 'kps1' [-Wunused-parameter]
   26 |                     const std::vector<KeyPoint> &kps1, const std::vector<KeyPoint> &kps2,
      |                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~
[2989/3796] Building CXX object modules/slam/CMakeFiles/opencv_slam.dir/src/feature.cpp.o
/home/ci/opencv_contrib/modules/slam/src/feature.cpp: In lambda function:
/home/ci/opencv_contrib/modules/slam/src/feature.cpp:142:80: warning: declaration of 'b' shadows a previous local [-Wshadow]
  142 |         std::sort(b.begin(), b.end(), [](const CandScore &a, const CandScore &b){ return a.score > b.score; });
      |                                                                                ^
/home/ci/opencv_contrib/modules/slam/src/feature.cpp:140:15: note: shadowed declaration is here
  140 |     for(auto &b: buckets){
      |               ^
[2990/3796] Building CXX object modules/slam/CMakeFiles/opencv_slam.dir/src/localizer.cpp.o
[2991/3796] Building CXX object modules/slam/CMakeFiles/opencv_slam.dir/src/map.cpp.o
[2992/3796] Building CXX object modules/slam/CMakeFiles/opencv_slam.dir/src/initializer.cpp.o
/home/ci/opencv_contrib/modules/slam/src/initializer.cpp: In member function 'void cv::vo::Initializer::decomposeH(const cv::Mat&, std::vector<cv::Mat>&, std::vector<cv::Mat>&, std::vector<cv::Mat>&)':
/home/ci/opencv_contrib/modules/slam/src/initializer.cpp:318:9: warning: unused variable 'solutions' [-Wunused-variable]
  318 |     int solutions = decomposeHomographyMat(H_normalized, Mat::eye(3,3,CV_64F),
      |         ^~~~~~~~~

@QueenofUSSR build is failing.

@abhishek-gola
Copy link

@QueenofUSSR, can you add some tests and a sample?

@abhishek-gola abhishek-gola self-assigned this Dec 17, 2025
@QueenofUSSR
Copy link
Author

@QueenofUSSR, can you add some tests and a sample?

Okay, we will upload some tests and a visualized sample soon!

Comment on lines +16 to +17
// Adaptive Non-Maximal Suppression (ANMS)
static void anms(const std::vector<KeyPoint> &in, std::vector<KeyPoint> &out, int maxFeatures)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rename it to something like adaptiveNonMaxSuppression

Comment on lines +39 to +72
bool ensureDirectoryExists(const std::string &dir){
if(dir.empty()) return false;
#if HAVE_STD_FILESYSTEM
try{
fs::path p(dir);
return fs::create_directories(p) || fs::exists(p);
}catch(...){ return false; }
#else
std::string tmp = dir;
if(tmp.empty()) return false;
while(tmp.size() > 1 && tmp.back() == '/') tmp.pop_back();
std::string cur;
if(!tmp.empty() && tmp[0] == '/') cur = "/";
size_t pos = 0;
while(pos < tmp.size()){
size_t next = tmp.find('/', pos);
std::string part = (next == std::string::npos) ? tmp.substr(pos) : tmp.substr(pos, next-pos);
if(!part.empty()){
if(cur.size() > 1 && cur.back() != '/') cur += '/';
cur += part;
if(mkdir(cur.c_str(), 0755) != 0){
if(errno == EEXIST){ /* ok */ }
else {
struct stat st;
if(stat(cur.c_str(), &st) == 0 && S_ISDIR(st.st_mode)){
// ok
} else return false;
}
}
}
if(next == std::string::npos) break;
pos = next + 1;
}
return true;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use opencv's filesystem <opencv2/core/utils/filesystem.hpp> it has inbuilt functions like createDirectories and isDirectory

Comment on lines +26 to +36
for(int i=0;i<N;++i){
for(int j=0;j<N;++j){
if(in[j].response > in[i].response){
float dx = in[i].pt.x - in[j].pt.x;
float dy = in[i].pt.y - in[j].pt.y;
float d2 = dx*dx + dy*dy;
if(d2 < radius[i]) radius[i] = d2;
}
}
// if no stronger keypoint exists, radius[i] stays INF
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can use parallel_for_ loop here.

Comment on lines +88 to +91
try{
calcOpticalFlowPyrLK(prevGray, image, prevPts, trackedPts, status, err, Size(21,21), 3,
TermCriteria(TermCriteria::COUNT+TermCriteria::EPS, 30, 0.01), 0, 1e-4);
} catch(...) { status.clear(); }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please format the catch part better, log the proper error using CV_Error.

Comment on lines +154 to +159
Mat P2(3, 4, CV_64F);
for(int i = 0; i < 3; ++i) {
for(int j = 0; j < 3; ++j) P2.at<double>(i,j) = R.at<double>(i,j);
P2.at<double>(i, 3) = t.at<double>(i, 0);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cv::Mat P2;
cv::hconcat(R, t, P2);

It can also be done this way.

catch(...) { points4D.release(); }
if(points4D.empty()) return newPoints;
Mat p4d64;
if(points4D.type() != CV_64F) points4D.convertTo(p4d64, CV_64F); else p4d64 = points4D;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

format it better

Comment on lines +278 to +307
// Compute median descriptor (for binary descriptors, use majority voting per bit)
if(descriptors[0].type() == CV_8U) {
// Binary descriptor (ORB)
int bytes = descriptors[0].cols;
Mat median = Mat::zeros(1, bytes, CV_8U);

for(int b = 0; b < bytes; ++b) {
int bitCount[8] = {0};
for(const auto &desc : descriptors) {
uchar byte = desc.at<uchar>(0, b);
for(int bit = 0; bit < 8; ++bit) {
if(byte & (1 << bit)) bitCount[bit]++;
}
}

uchar medianByte = 0;
int threshold = static_cast<int>(descriptors.size()) / 2;
for(int bit = 0; bit < 8; ++bit) {
if(bitCount[bit] > threshold) {
medianByte |= (1 << bit);
}
}
median.at<uchar>(0, b) = medianByte;
}

mp.descriptor = median;
} else {
// Fallback: use first descriptor
mp.descriptor = descriptors[0].clone();
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can parallelize this loop too

Comment on lines +246 to +267
for (int kfId : localKfIndices) {
if (fixedSet.find(kfId) != fixedSet.end()) continue;
if (kfId < 0 || kfId >= static_cast<int>(keyframes.size())) continue;
KeyFrame &kf = keyframes[kfId];
// Build matchedMpIndices: for each keypoint in this KF, which mappoint index it corresponds to
std::vector<int> matchedMpIndices(kf.kps.size(), -1);
for (size_t mpIdx = 0; mpIdx < mappoints.size(); ++mpIdx) {
const MapPoint &mp = mappoints[mpIdx];
if (mp.isBad) continue;
for (const auto &obs : mp.observations) {
if (obs.first == kfId) {
int kpIdx = obs.second;
if (kpIdx >= 0 && kpIdx < static_cast<int>(matchedMpIndices.size()))
matchedMpIndices[kpIdx] = static_cast<int>(mpIdx);
}
}
}
std::vector<bool> inliers;
// Use the same number of iterations as outer loop for RANSAC attempts
optimizePose(kf, mappoints, matchedMpIndices, fx, fy, cx, cy, inliers, std::max(20, iterations));
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid this nested loop of 3 for loop calls, you can precompute matchedMpIndices.

Comment on lines +174 to +198
auto ratioKeep = [&](const std::vector<std::vector<DMatch>>& knn, bool forward) {
std::vector<DMatch> filtered;
for(size_t qi=0; qi<knn.size(); ++qi){
if(knn[qi].empty()) continue;
DMatch best = knn[qi][0];
float ratio = 0.75f;
if(knn[qi].size() >= 2){
if(knn[qi][1].distance > 0) {
if(best.distance / knn[qi][1].distance > ratio) continue;
}
}
// mutual check
int t = forward ? best.trainIdx : (int)qi;
// find reverse match for t
const auto &rev = forward ? knn21 : knn12;
if(t < 0 || t >= (int)rev.size() || rev[t].empty()) continue;
DMatch rbest = rev[t][0];
if((forward && rbest.trainIdx == (int)qi) || (!forward && rbest.trainIdx == best.queryIdx)){
filtered.push_back(best);
}
}
return filtered;
};
std::vector<DMatch> goodMatches = ratioKeep(knn12, true);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need this lambda function? you can use the code directly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants