1
0
mirror of https://github.com/opencv/opencv.git synced 2026-07-26 05:43:05 +04:00
Files
opencv/modules/slam/src/odometry/visual_odometry.cpp
T
2026-06-17 13:15:57 +05:30

462 lines
14 KiB
C++

// 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.
// Third party copyrights are property of their respective owners.
#include "../precomp.hpp"
#include "vo_impl.hpp"
#include <fstream>
#include <sstream>
namespace cv {
namespace slam {
namespace {
const char* stateName(OdometryState s)
{
switch (s)
{
case NOT_INITIALIZED: return "NOT_INITIALIZED";
case INITIALIZING: return "INITIALIZING";
case TRACKING: return "TRACKING";
}
return "NOT_INITIALIZED";
}
String joinPath(const String& dir, const String& name)
{
if (dir.empty()) return name;
char last = dir.back();
if (last == '/' || last == '\\') return dir + name;
return dir + "/" + name;
}
} // anonymous namespace
// Factory
VisualOdometry::VisualOdometry() = default;
VisualOdometry::~VisualOdometry() = default;
Ptr<VisualOdometry> VisualOdometry::create(
const Ptr<Feature2D>& detector,
const Ptr<DescriptorMatcher>& matcher,
const String& imagesFolder,
const String& outputFolder,
InputArray cameraMatrix,
InputArray distCoeffs,
const OdometryParams& params)
{
CV_Assert(detector && "VisualOdometry::create: detector must not be null");
CV_Assert(matcher && "VisualOdometry::create: matcher must not be null");
Mat K = cameraMatrix.getMat();
CV_Assert(!K.empty() && K.rows == 3 && K.cols == 3);
Mat dist = distCoeffs.empty() ? Mat() : distCoeffs.getMat();
return makePtr<VisualOdometryImpl>(
detector, matcher, imagesFolder, outputFolder, K, dist, params);
}
// Constructor
VisualOdometryImpl::VisualOdometryImpl(
const Ptr<Feature2D>& detector,
const Ptr<DescriptorMatcher>& matcher,
const String& imagesFolder,
const String& outputFolder,
const Mat& cameraMatrix,
const Mat& distCoeffs,
const OdometryParams& params)
: detector(detector), matcher(matcher), params(params),
imagesFolder(imagesFolder), outputFolder(outputFolder)
{
cameraMatrix.convertTo(K, CV_64F);
if (!distCoeffs.empty())
distCoeffs.convertTo(dist, CV_64F);
}
// reset / processFrame
void VisualOdometryImpl::reset()
{
state = NOT_INITIALIZED;
lastPoseCw = Matx44d::eye();
refFrame = Frame();
lastKf = nullptr;
framesSinceKf = 0;
lastKfInliers = 0;
velocity = Matx44d::eye();
hasVelocity = false;
prevFrame = Frame();
hasPrevFrame = false;
lastEvent.clear();
poseFilenames.clear();
map.clear();
}
bool VisualOdometryImpl::processFrame(InputArray image)
{
CV_INSTRUMENT_REGION();
if (image.empty()) return false;
lastEvent.clear();
Frame cur;
extractFeatures(image, cur);
if (cur.keypoints.empty() || cur.descriptors.empty()) return false;
cur.mapPoints.assign(cur.keypoints.size(), nullptr);
cur.outliers.assign(cur.keypoints.size(), false);
cur.buildGrid();
switch (state)
{
case NOT_INITIALIZED:
refFrame = cur;
state = INITIALIZING;
return false;
case INITIALIZING:
return bootstrap(cur);
case TRACKING:
return track(cur);
}
return false;
}
// Feature extraction
void VisualOdometryImpl::extractFeatures(InputArray image, Frame& out) const
{
Mat img = image.getMat();
out.imageSize = img.size();
out.keypoints.clear();
// Detect and compute on the original image (color/grey is up to the detector).
detector->detectAndCompute(img, noArray(), out.keypoints, out.descriptors);
// Store a greyscale copy for the optical-flow fallback.
if (img.channels() > 1)
cvtColor(img, out.image, COLOR_BGR2GRAY);
else
out.image = img.clone();
// Pre-compute undistorted pixel coordinates used by every stage.
if (!out.keypoints.empty())
{
std::vector<Point2f> raw;
raw.reserve(out.keypoints.size());
for (const auto& kp : out.keypoints)
raw.push_back(kp.pt);
if (!dist.empty())
undistortPoints(raw, out.undistKpts, K, dist, noArray(), K);
else
out.undistKpts = raw;
}
}
// Frame matching helper
void VisualOdometryImpl::matchFrames(
const std::vector<KeyPoint>& qKp, const Mat& qDesc, Size qSz,
const std::vector<KeyPoint>& tKp, const Mat& tDesc, Size tSz,
std::vector<DMatch>& matches) const
{
matches.clear();
if (qDesc.empty() || tDesc.empty()) return;
if (qKp.empty() || tKp.empty()) return;
LightGlueMatcher* lg = dynamic_cast<LightGlueMatcher*>(matcher.get());
if (lg)
{
Mat qk((int)qKp.size(), 2, CV_32F);
for (size_t i = 0; i < qKp.size(); ++i)
{ qk.at<float>((int)i,0) = qKp[i].pt.x; qk.at<float>((int)i,1) = qKp[i].pt.y; }
Mat tk((int)tKp.size(), 2, CV_32F);
for (size_t i = 0; i < tKp.size(); ++i)
{ tk.at<float>((int)i,0) = tKp[i].pt.x; tk.at<float>((int)i,1) = tKp[i].pt.y; }
lg->setPairInfo(qk, tk, qSz, tSz);
}
matcher->match(qDesc, tDesc, matches);
}
// Batch run()
bool VisualOdometryImpl::run()
{
CV_INSTRUMENT_REGION();
if (imagesFolder.empty())
{
CV_LOG_ERROR(NULL, "VisualOdometry::run: imagesFolder is empty");
return false;
}
std::vector<String> allFiles;
try { cv::glob(imagesFolder, allFiles, false); }
catch (const cv::Exception& e)
{
CV_LOG_ERROR(NULL, "VisualOdometry::run: glob failed: " << e.what());
return false;
}
std::vector<String> imgFiles;
imgFiles.reserve(allFiles.size());
for (const auto& f : allFiles)
if (cv::haveImageReader(f)) imgFiles.push_back(f);
std::sort(imgFiles.begin(), imgFiles.end());
if (imgFiles.empty())
{
CV_LOG_WARNING(NULL, "VisualOdometry::run: no images in " << imagesFolder);
return false;
}
std::ofstream log;
if (!outputFolder.empty())
{
cv::utils::fs::createDirectories(outputFolder);
log.open(joinPath(outputFolder, "vo.log").c_str());
}
auto logln = [&](const String& s) { if (log.is_open()) log << s << "\n"; };
logln("[INFO] optimizer = reprojection inlier check");
logln(String("[INFO] images_folder = ") + imagesFolder);
logln(String("[INFO] output_folder = ") + outputFolder);
{
std::ostringstream ss;
ss << "[INFO] found " << imgFiles.size() << " image(s)";
logln(ss.str());
}
reset();
int nEmitted = 0;
size_t prevTrajLen = 0;
String refFilename;
for (size_t i = 0; i < imgFiles.size(); ++i)
{
Mat img = imread(imgFiles[i]);
if (img.empty())
{
std::ostringstream ss;
ss << "[FRAME " << i << "] file=" << imgFiles[i] << " ERROR: imread failed";
logln(ss.str()); continue;
}
OdometryState before = state;
bool emitted = processFrame(img);
OdometryState after = state;
if (emitted) ++nEmitted;
// Track which input image maps to each trajectory pose.
if (before == NOT_INITIALIZED ||
(before == TRACKING && after == INITIALIZING))
refFilename = imgFiles[i];
const size_t added = map.trajectory().size() - prevTrajLen;
if (added == 1)
poseFilenames.push_back(imgFiles[i]);
else if (added == 2)
{
poseFilenames.push_back(refFilename);
poseFilenames.push_back(imgFiles[i]);
}
prevTrajLen = map.trajectory().size();
std::ostringstream ss;
ss << "[FRAME " << i << "] file=" << imgFiles[i]
<< " state=" << stateName(before);
if (before != after) ss << "->" << stateName(after);
ss << " emitted=" << (emitted ? "yes" : "no")
<< " keyframes=" << map.numKeyframes()
<< " map_points=" << map.numMapPoints();
if (!lastEvent.empty()) ss << " [" << lastEvent << "]";
if (emitted)
{
Point3d C = detail::cameraCenterWorld(lastPoseCw);
ss << " C=(" << C.x << "," << C.y << "," << C.z << ")";
}
logln(ss.str());
}
if (!outputFolder.empty())
{
writeTrajectoryText(joinPath(outputFolder, "trajectory.txt"));
writeTrajectoryBin (joinPath(outputFolder, "trajectory.bin"));
writeMapPoints (joinPath(outputFolder, "map_points.txt"));
writeKeypoints (joinPath(outputFolder, "keypoints.txt"));
writeImagesTxt (joinPath(outputFolder, "images.txt"));
std::ostringstream ss;
ss << "[INFO] run complete: frames=" << imgFiles.size()
<< " emitted=" << nEmitted
<< " keyframes=" << map.numKeyframes()
<< " map_points=" << map.numMapPoints();
logln(ss.str());
logln("[INFO] wrote trajectory.txt, trajectory.bin, map_points.txt, "
"keypoints.txt, images.txt");
}
return nEmitted > 0;
}
// IO helpers
void VisualOdometryImpl::writeTrajectoryText(const String& path) const
{
std::ofstream f(path.c_str());
if (!f.is_open()) { CV_LOG_WARNING(NULL, "writeTrajectoryText: cannot open " << path); return; }
f << "# Per-frame camera center in world coordinates.\n# Columns: Cx Cy Cz\n";
f.setf(std::ios::scientific); f.precision(9);
for (const auto& T : map.trajectory())
{
Point3d C = detail::cameraCenterWorld(T);
f << C.x << " " << C.y << " " << C.z << "\n";
}
}
void VisualOdometryImpl::writeTrajectoryBin(const String& path) const
{
std::ofstream f(path.c_str(), std::ios::binary);
if (!f.is_open()) { CV_LOG_WARNING(NULL, "writeTrajectoryBin: cannot open " << path); return; }
const char magic[4] = {'V','O','T','R'};
f.write(magic, 4);
int32_t version = 1;
f.write(reinterpret_cast<const char*>(&version), sizeof(int32_t));
int32_t n = (int32_t)map.trajectory().size();
f.write(reinterpret_cast<const char*>(&n), sizeof(int32_t));
for (const auto& T : map.trajectory())
{
double buf[16];
for (int r = 0; r < 4; ++r)
for (int c = 0; c < 4; ++c) buf[r*4+c] = T(r,c);
f.write(reinterpret_cast<const char*>(buf), sizeof(buf));
}
}
void VisualOdometryImpl::writeMapPoints(const String& path) const
{
std::ofstream f(path.c_str());
if (!f.is_open()) { CV_LOG_WARNING(NULL, "writeMapPoints: cannot open " << path); return; }
f << "# Map points in world coordinates.\n# Columns: id X Y Z n_observations\n";
f.setf(std::ios::scientific); f.precision(9);
for (MapPoint* mp : map.mapPoints())
{
if (!mp || mp->bad) continue;
f << mp->id << " "
<< mp->pos.x << " " << mp->pos.y << " " << mp->pos.z << " "
<< mp->observations.size() << "\n";
}
}
void VisualOdometryImpl::writeKeypoints(const String& path) const
{
std::ofstream f(path.c_str());
if (!f.is_open()) { CV_LOG_WARNING(NULL, "writeKeypoints: cannot open " << path); return; }
f << "# Per-keyframe keypoints.\n"
<< "# Block header: KF kf_id Cx Cy Cz n_keypoints\n"
<< "# Followed by n_keypoints rows: kpIdx x y size angle response octave mpId\n"
<< "# mpId = -1 if the keypoint has no map point.\n";
f.setf(std::ios::fixed); f.precision(6);
for (KeyFrame* kf : map.keyframes())
{
if (!kf) continue;
Point3d C = detail::cameraCenterWorld(kf->poseCw);
f << "KF " << kf->id << " " << C.x << " " << C.y << " " << C.z
<< " " << kf->keypoints.size() << "\n";
for (size_t i = 0; i < kf->keypoints.size(); ++i)
{
const KeyPoint& kp = kf->keypoints[i];
int mpId = -1;
if (i < kf->mapPoints.size() && kf->mapPoints[i])
mpId = kf->mapPoints[i]->id;
f << i << " " << kp.pt.x << " " << kp.pt.y << " "
<< kp.size << " " << kp.angle << " " << kp.response << " "
<< kp.octave << " " << mpId << "\n";
}
}
}
// Shepperd's method: numerically-stable R → unit quaternion (qw, qx, qy, qz).
static void rotMatToQuat(const Matx33d& R,
double& qw, double& qx, double& qy, double& qz)
{
const double tr = R(0,0) + R(1,1) + R(2,2);
if (tr > 0.0)
{
double s = std::sqrt(tr + 1.0) * 2.0;
qw = 0.25 * s;
qx = (R(2,1) - R(1,2)) / s;
qy = (R(0,2) - R(2,0)) / s;
qz = (R(1,0) - R(0,1)) / s;
}
else if (R(0,0) > R(1,1) && R(0,0) > R(2,2))
{
double s = std::sqrt(1.0 + R(0,0) - R(1,1) - R(2,2)) * 2.0;
qw = (R(2,1) - R(1,2)) / s; qx = 0.25 * s;
qy = (R(0,1) + R(1,0)) / s; qz = (R(0,2) + R(2,0)) / s;
}
else if (R(1,1) > R(2,2))
{
double s = std::sqrt(1.0 + R(1,1) - R(0,0) - R(2,2)) * 2.0;
qw = (R(0,2) - R(2,0)) / s; qx = (R(0,1) + R(1,0)) / s;
qy = 0.25 * s; qz = (R(1,2) + R(2,1)) / s;
}
else
{
double s = std::sqrt(1.0 + R(2,2) - R(0,0) - R(1,1)) * 2.0;
qw = (R(1,0) - R(0,1)) / s; qx = (R(0,2) + R(2,0)) / s;
qy = (R(1,2) + R(2,1)) / s; qz = 0.25 * s;
}
}
static String basenameOf(const String& path)
{
const size_t slash = path.find_last_of("/\\");
return (slash == String::npos) ? path : path.substr(slash + 1);
}
void VisualOdometryImpl::writeImagesTxt(const String& path) const
{
std::ofstream f(path.c_str());
if (!f.is_open()) { CV_LOG_WARNING(NULL, "writeImagesTxt: cannot open " << path); return; }
const auto& traj = map.trajectory();
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);
double qw, qx, qy, qz;
rotMatToQuat(R, qw, qx, qy, qz);
const String name = (i < poseFilenames.size())
? basenameOf(poseFilenames[i])
: (String("pose_") + std::to_string(i));
f << i << " " << qw << " " << qx << " " << qy << " " << qz << " "
<< T(0,3) << " " << T(1,3) << " " << T(2,3) << " " << 1 << " " << name << "\n\n";
}
}
}} // namespace cv::slam