// This file is part of OpenCV project. // It is subject to the license terms in the LICENSE file found in the top-level directory // of this distribution and at http://opencv.org/license.html. // Copyright (C) 2026, BigVision LLC, all rights reserved. #include #include #include #include #include #include #include #include #include #include #include "../dnn/common.hpp" using namespace cv; using namespace std; namespace { // path helper: strips the directory part off a path String basenameOf(const String& path) { const size_t slash = path.find_last_of("/\\"); return (slash == String::npos) ? path : path.substr(slash + 1); } // reader: lists readable image files under a directory, sorted by filename std::vector listImageFiles(const String& imagesDir) { std::vector allFiles; glob(imagesDir, allFiles, false); std::vector imgFiles; imgFiles.reserve(allFiles.size()); for (const auto& f : allFiles) if (haveImageReader(f)) imgFiles.push_back(f); std::sort(imgFiles.begin(), imgFiles.end()); return imgFiles; } // bookkeeping: maps each pose in vo->getTrajectory() back to its source image filename. // // processFrame() doesn't report which frame a pose came from, so this infers it: most frames // add one pose, but the frame where two-view initialization completes adds two at once (the // reference frame's pose, remembered when initialization started, plus the current frame's). class PoseFilenameTracker { public: void update(slam::OdometryState before, slam::OdometryState after, size_t trajectorySize, const String& filename) { if (before == slam::NOT_INITIALIZED || (before == slam::TRACKING && after == slam::INITIALIZING)) refFilename_ = filename; const size_t added = trajectorySize - prevTrajectorySize_; if (added == 1) filenames_.push_back(filename); else if (added == 2) { filenames_.push_back(refFilename_); filenames_.push_back(filename); } prevTrajectorySize_ = trajectorySize; } const std::vector& filenames() const { return filenames_; } private: std::vector filenames_; size_t prevTrajectorySize_ = 0; String refFilename_; }; // writer: dumps camera.txt, point3d.txt and images.txt (COLMAP text format) for @p vo into // @p outputFolder; @p poseFilenames[i] is the source image for trajectory pose i void writeColmapFiles(const slam::VisualOdometry& vo, const Mat& K, const std::vector& poseFilenames, const String& outputFolder) { cv::utils::fs::createDirectories(outputFolder); { std::ofstream f(cv::utils::fs::join(outputFolder, "camera.txt").c_str()); int width = 0, height = 0; if (!vo.getMap().keyframes().empty()) { const slam::KeyFrame* kf = *vo.getMap().keyframes().begin(); width = kf->imageSize.width; height = kf->imageSize.height; } f.setf(std::ios::fixed); f.precision(4); f << "fx " << K.at(0, 0) << "\n" << "fy " << K.at(1, 1) << "\n" << "cx " << K.at(0, 2) << "\n" << "cy " << K.at(1, 2) << "\n" << "width " << width << "\n" << "height " << height << "\n"; } { std::ofstream f(cv::utils::fs::join(outputFolder, "point3d.txt").c_str()); f << "# Map points in world coordinates.\n# Columns: id X Y Z n_observations\n"; f.setf(std::ios::scientific); f.precision(9); for (slam::MapPoint* mp : vo.getMap().mapPoints()) { if (!mp || mp->bad) continue; f << mp->id << " " << mp->pos.x << " " << mp->pos.y << " " << mp->pos.z << " " << mp->observations.size() << "\n"; } } { std::ofstream f(cv::utils::fs::join(outputFolder, "images.txt").c_str()); const auto& traj = vo.getTrajectory(); f << "# Image list with two lines of data per image:\n" << "# IMAGE_ID, QW, QX, QY, QZ, TX, TY, TZ, CAMERA_ID, NAME\n" << "# POINTS2D[] as (X, Y, POINT3D_ID)\n" << "# Number of images: " << traj.size() << ", mean observations per image: 0.0\n"; f.setf(std::ios::fixed); f.precision(6); for (size_t i = 0; i < traj.size(); ++i) { const Matx44d& T = traj[i]; Matx33d R; for (int r = 0; r < 3; ++r) for (int c = 0; c < 3; ++c) R(r, c) = T(r, c); const Quatd q = Quatd::createFromRotMat(R); const String name = (i < poseFilenames.size()) ? basenameOf(poseFilenames[i]) : (String("pose_") + std::to_string(i)); f << i << " " << q.w << " " << q.x << " " << q.y << " " << q.z << " " << T(0,3) << " " << T(1,3) << " " << T(2,3) << " " << 1 << " " << name << "\n"; } } } } // namespace const string about = "Monocular visual odometry using ALIKED + LightGlue.\n\n" "Runs feature detection (ALIKED) and matching (LightGlue) from ONNX models over a\n" "directory of images, estimates the camera trajectory and writes it to --output.\n" "To run:\n" "\t ./example_slam_visual_odometry --aliked=aliked.onnx --lightglue=lightglue.onnx --images=./seq\n" "Sample command (run on the GPU):\n" "\t ./example_slam_visual_odometry --aliked=aliked.onnx --lightglue=lightglue.onnx --images=./seq --target=cuda\n"; const string param_keys = "{ help h | | Print help message }" "{ aliked | | Path to ALIKED ONNX model }" "{ lightglue | | Path to LightGlue ONNX model }" "{ images | | Path to directory with input images }" "{ output | vo_out | Output directory for trajectory and map }" "{ fx | 718.856 | Camera focal length X }" "{ fy | 718.856 | Camera focal length Y }" "{ cx | 607.1928 | Camera principal point X }" "{ cy | 185.2157 | Camera principal point Y }" "{ dist | | Lens distortion coeffs, comma-separated k1,k2,p1,p2[,k3,...] (default: none) }"; const string backend_keys = format( "{ backend | default | Choose one of computation backends: " "default: automatically (by default), " "openvino: Intel's Deep Learning Inference Engine (https://software.intel.com/openvino-toolkit), " "opencv: OpenCV implementation, " "vkcom: VKCOM, " "cuda: CUDA, " "webnn: WebNN }"); const string target_keys = format( "{ target | cpu | Choose one of target computation devices: " "cpu: CPU target (by default), " "opencl: OpenCL, " "opencl_fp16: OpenCL fp16 (half-float precision), " "vpu: VPU, " "vulkan: Vulkan, " "cuda: CUDA, " "cuda_fp16: CUDA fp16 (half-float preprocess) }"); string keys = param_keys + backend_keys + target_keys; int main(int argc, char** argv) { CommandLineParser parser(argc, argv, keys); parser.about(about); if (parser.has("help")) { parser.printMessage(); return 0; } const String alikedPath = parser.get("aliked"); const String lightgluePath = parser.get("lightglue"); const String imagesDir = parser.get("images"); const int backendId = getBackendID(parser.get("backend")); const int targetId = getTargetID(parser.get("target")); if (!parser.check() || alikedPath == "" || lightgluePath == "" || imagesDir == "") { parser.printErrors(); parser.printMessage(); return 1; } const String outputDir = parser.get("output"); const Matx33d K(parser.get("fx"), 0., parser.get("cx"), 0., parser.get("fy"), parser.get("cy"), 0., 0., 1.); // Optional lens distortion (k1,k2,p1,p2[,k3,...]); empty means no distortion. Mat distCoeffs; { std::stringstream ss(parser.get("dist")); std::vector coeffs; String tok; while (std::getline(ss, tok, ',')) { const size_t b = tok.find_first_not_of(" \t"); const size_t e = tok.find_last_not_of(" \t"); if (b == String::npos) continue; coeffs.push_back(std::stod(tok.substr(b, e - b + 1))); } if (!coeffs.empty()) distCoeffs = Mat(coeffs, true).reshape(1, 1); } ALIKED::Params detParams; detParams.inputSize = Size(640, 640); detParams.engine = dnn::ENGINE_OPENCV; detParams.backend = backendId; detParams.target = targetId; auto detector = ALIKED::create(alikedPath, detParams); auto matcher = LightGlueMatcher::create(lightgluePath, 0.0f, backendId, targetId); slam::OdometryParams voParams; voParams.minInitParallaxDeg = 1.5; voParams.minInitPoints = 50; voParams.pnpReprojThresh = 4.0; voParams.kfMaxFrames = 30; voParams.localMapTopK = 10; auto vo = slam::VisualOdometry::create( detector, matcher, Mat(K), distCoeffs, voParams); const std::vector imgFiles = listImageFiles(imagesDir); if (imgFiles.empty()) { std::cerr << "no images found in " << imagesDir << "\n"; return 1; } std::cout << "images folder : " << imagesDir << "\n" << "output folder : " << outputDir << "\n" << "total frames : " << imgFiles.size() << "\n\n"; PoseFilenameTracker poseTracker; int nEmitted = 0; const int nDigits = static_cast(std::to_string(imgFiles.size()).size()); const int64 t0 = getTickCount(); for (size_t i = 0; i < imgFiles.size(); ++i) { const int64 tFrameStart = getTickCount(); Mat img = imread(imgFiles[i]); if (img.empty()) { std::cerr << "Frame " << std::setw(nDigits) << (i + 1) << "/" << imgFiles.size() << " failed to read: " << imgFiles[i] << "\n"; continue; } const slam::OdometryState before = vo->getState(); const bool emitted = vo->processFrame(img); const slam::OdometryState after = vo->getState(); if (emitted) ++nEmitted; poseTracker.update(before, after, vo->getTrajectory().size(), imgFiles[i]); const double frameElapsed = (getTickCount() - tFrameStart) / getTickFrequency(); const double fps = frameElapsed > 0. ? 1. / frameElapsed : 0.; std::cout << "Frame " << std::setw(nDigits) << (i + 1) << "/" << imgFiles.size() << " processed | fps: " << std::fixed << std::setprecision(1) << std::setw(5) << fps << " | emitted: " << (emitted ? "yes" : "no ") << " | keyframes: " << vo->getMap().numKeyframes() << " | map points: " << vo->getMap().numMapPoints() << "\n"; } const double elapsed = (getTickCount() - t0) / getTickFrequency(); const double avgFps = elapsed > 0. ? imgFiles.size() / elapsed : 0.; const bool ok = nEmitted > 0; if (ok && !outputDir.empty()) writeColmapFiles(*vo, Mat(K), poseTracker.filenames(), outputDir); std::cout << "\n" << "==================== Visual Odometry Result ====================\n" << "status : " << (ok ? "OK" : "FAILED") << "\n" << "total frames : " << imgFiles.size() << "\n" << "camera poses : " << vo->getTrajectory().size() << "\n" << "keyframes : " << vo->getMap().numKeyframes() << "\n" << "map points : " << vo->getMap().numMapPoints() << "\n" << "elapsed time : " << std::fixed << std::setprecision(2) << elapsed << " s\n" << "average fps : " << std::fixed << std::setprecision(2) << avgFps << "\n" << "output dir : " << outputDir << "\n" << "====================================================================\n"; return ok ? 0 : 1; }