Colmap内置的 tri_ignore_two_view_tracks 只能过滤严格的两视图轨迹(track_length=2),有时会产生错误的3D点无法被过滤掉,影响整体效果,我希望通过增加轨迹阈值参数、修改BA损失类型、BA加GPS约束等来解决这一问题(实际解决不了)。后面已经没有必要自己慢慢调试colmap代码了,只要有好想法都可以让claude code直接帮忙修改测试。
1、准备调试环境
参考之前的文章Ubuntu下Colmap源码编译调试,这里直接以源码目录作为工程目录,tasks.json、launch.json和settings.json都放到该目录下,目录结构如图:

核心操作:
安装GDB: apt update && apt install -y gdb
CMakeLists.txt中增加架构参数
配置
tasks.json、launch.json和settings.json
tasks.json内容如下:
C++
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"type": "shell",
"command": "cd build && cmake --build . -j22",
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared"
},
"problemMatcher": [
"$gcc"
]
}
]
}
launch.json内容如下:
C++
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "(gdb) 调试示例",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/src/colmap/exe/colmap", // 你的可执行文件路径
"args": ["feature_extractor", "--database_path", "/root/code/colmap/sh24/database.db", "--image_path", "/root/code/colmap/sh24/images"], // 调试时传入的参数,对应你的--message
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "为 gdb 启用整齐打印",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
],
"preLaunchTask": "build"
}
]
}
settings.json内容如下:
C++
{
"cmake.sourceDirectory": "/root/code/colmap"
}
2、增加轨迹命令
先打开CMakeLists.txt,按ctrl+s编译源码,然后按F5调试,这里我增加了一条新命令:
python
~/code/colmap/build/src/colmap/exe/colmap match_filtering -h
# 新增命令
~/code/colmap/build/src/colmap/exe/colmap match_filtering --database_path "/root/code/colmap/sh24/database.db" --min_track_length 4 --min_num_inliers 15
~/code/colmap/build/src/colmap/exe/colmap pose_prior_mapper \
--database_path "/root/code/colmap/sh24/database.db" \
--image_path "/root/code/colmap/sh24/images" \
--output_path "/root/code/colmap/sh24/sparse" \
--overwrite_priors_covariance=1 \
--prior_position_std_x=0.05 \
--prior_position_std_y=0.05 \
--prior_position_std_z=0.05 \
--use_robust_loss_on_prior_position=1 \
--Mapper.num_threads 80 \
--Mapper.abs_pose_max_error 4 \
--Mapper.abs_pose_min_num_inliers 50 \
--Mapper.abs_pose_min_inlier_ratio 0.35 \
--Mapper.filter_max_reproj_error 2 \
--Mapper.filter_min_tri_angle 3 \
--Mapper.tri_min_angle 2 \
--Mapper.tri_create_max_angle_error 2 \
--Mapper.tri_ignore_two_view_tracks 1 \
--Mapper.init_num_trials 50 \
--Mapper.init_min_num_inliers 100 \
--Mapper.min_num_matches 15 \
--Mapper.ba_local_max_num_iterations 15 \
--Mapper.ba_global_max_num_iterations 25 \
--Mapper.ba_local_max_refinements 3 \
--Mapper.tri_merge_max_reproj_error 2 \
--Mapper.tri_complete_max_reproj_error 3 \
--Mapper.ba_refine_focal_length 0 \
--Mapper.ba_refine_principal_point 0 \
--Mapper.ba_refine_extra_params 0
# 不依赖gps的稀疏重建
~/code/colmap/build/src/colmap/exe/colmap mapper \
--database_path "/root/code/colmap/sh24/database.db" \
--image_path "/root/code/colmap/sh24/images" \
--output_path "/root/code/colmap/sh24/sparse" \
--Mapper.num_threads 80 \
--Mapper.abs_pose_max_error 4 \
--Mapper.abs_pose_min_num_inliers 50 \
--Mapper.abs_pose_min_inlier_ratio 0.35 \
--Mapper.filter_max_reproj_error 2 \
--Mapper.filter_min_tri_angle 3 \
--Mapper.tri_min_angle 2 \
--Mapper.tri_create_max_angle_error 2 \
--Mapper.tri_ignore_two_view_tracks 1 \
--Mapper.init_num_trials 50 \
--Mapper.init_min_num_inliers 100 \
--Mapper.min_num_matches 15 \
--Mapper.ba_local_max_num_iterations 15 \
--Mapper.ba_global_max_num_iterations 25 \
--Mapper.ba_local_max_refinements 3 \
--Mapper.tri_merge_max_reproj_error 2 \
--Mapper.tri_complete_max_reproj_error 3 \
--Mapper.ba_refine_focal_length 0 \
--Mapper.ba_refine_principal_point 0 \
--Mapper.ba_refine_extra_params 0
colmap model_converter --input_path "/root/code/colmap/sh24/sparse/0" --output_path "/root/code/colmap/sh24/sparse/0" --output_type TXT
colmap model_converter --input_path "/workspace/projects/000000-20260420100210216-493OM574AB01K9/sub_task_10/sparse/0" --output_path "/workspace/projects/000000-20260420100210216-493OM574AB01K9/sub_task_10/sparse/0" --output_type TXT
这里在匹配之后、稀疏重建之前增加了一个过滤匹配点的功能,该功能根据保留的轨迹长度阈值删除部分匹配点,并更新到数据库中。源码中修改点如下:

2.1 注册命令
加入注册命令代码:

python
commands.emplace_back("match_filtering", &colmap::RunMatchFiltering);
2.2 实现匹配过滤功能
命令对应的实现函数,主要在sfm.cc中实现,增加如下功能:
c++
#include "colmap/scene/correspondence_graph.h"
#include "colmap/util/timer.h"
int RunMatchFiltering(int argc, char** argv) {
std::string database_path;
size_t min_track_length = 3;
int min_num_inliers = 15;
OptionManager options;
options.AddRequiredOption("database_path", &database_path);
options.AddDefaultOption("min_track_length", &min_track_length);
options.AddDefaultOption("min_num_inliers", &min_num_inliers);
if (!options.Parse(argc, argv)) {
return EXIT_FAILURE;
}
// Open database
auto database = Database::Open(database_path);
// Read all two_view_geometries
LOG(INFO) << "Loading two_view_geometries from database...";
const std::vector<std::pair<image_pair_t, TwoViewGeometry>>
two_view_geometries = database->ReadTwoViewGeometries();
LOG(INFO) << "Loaded " << two_view_geometries.size() << " image pairs";
// Read all images to get their point2D counts
LOG(INFO) << "Loading images from database...";
const std::vector<Image> images = database->ReadAllImages();
std::unordered_map<image_t, point2D_t> image_num_points2D;
for (const auto& image : images) {
// Get keypoints count for each image
const size_t num_keypoints = database->NumKeypointsForImage(image.ImageId());
image_num_points2D[image.ImageId()] = static_cast<point2D_t>(num_keypoints);
}
// Build correspondence graph from all matches
LOG(INFO) << "Building correspondence graph...";
CorrespondenceGraph correspondence_graph;
for (const auto& [image_id, num_points2D] : image_num_points2D) {
correspondence_graph.AddImage(image_id, num_points2D);
}
size_t total_matches_before = 0;
for (const auto& [pair_id, tvg] : two_view_geometries) {
if (tvg.inlier_matches.size() >= static_cast<size_t>(min_num_inliers)) {
const auto [image_id1, image_id2] = PairIdToImagePair(pair_id);
correspondence_graph.AddCorrespondences(
image_id1, image_id2, tvg.inlier_matches);
total_matches_before += tvg.inlier_matches.size();
}
}
correspondence_graph.Finalize();
LOG(INFO) << "Correspondence graph built with " << total_matches_before
<< " total matches";
// Filter matches based on track length
LOG(INFO) << "Filtering matches with track length < " << min_track_length;
Timer timer;
timer.Start();
size_t total_matches_after = 0;
size_t total_filtered = 0;
size_t pairs_removed = 0;
DatabaseTransaction transaction(database.get());
database->ClearTwoViewGeometries();
for (const auto& [pair_id, tvg] : two_view_geometries) {
const auto [image_id1, image_id2] = PairIdToImagePair(pair_id);
FeatureMatches filtered_matches;
filtered_matches.reserve(tvg.inlier_matches.size());
for (const auto& match : tvg.inlier_matches) {
// Compute track length for this observation
const size_t track_length =
correspondence_graph.ComputeTrackLength(image_id1, match.point2D_idx1);
if (track_length >= min_track_length) {
filtered_matches.push_back(match);
} else {
total_filtered++;
}
}
// Only write if there are enough matches remaining
if (filtered_matches.size() >= static_cast<size_t>(min_num_inliers)) {
TwoViewGeometry filtered_tvg = tvg;
filtered_tvg.inlier_matches = filtered_matches;
database->WriteTwoViewGeometry(image_id1, image_id2, filtered_tvg);
total_matches_after += filtered_matches.size();
} else {
pairs_removed++;
}
}
LOG(INFO) << "Filtering completed in " << timer.ElapsedSeconds() << " seconds";
LOG(INFO) << "Matches before: " << total_matches_before;
LOG(INFO) << "Matches after: " << total_matches_after;
LOG(INFO) << "Matches filtered: " << total_filtered;
LOG(INFO) << "Image pairs removed: " << pairs_removed;
return EXIT_SUCCESS;
}
对应的sfm.h中声明:
c++
int RunMatchFiltering(int argc, char** argv);

2.3 计算轨迹长度相关代码
这部分代码主要在correspondence_graph.cc,
c++
size_t CorrespondenceGraph::ComputeTrackLength(
const image_t image_id, const point2D_t point2D_idx) const {
std::vector<Correspondence> corrs;
// Use a large transitivity value to find all connected observations
ExtractTransitiveCorrespondences(image_id, point2D_idx, 100, &corrs);
// Count unique images in the track
std::set<image_t> images_in_track;
for (const auto& corr : corrs) {
images_in_track.insert(corr.image_id);
}
return images_in_track.size();
}
对应的.h代码:
c++
// Compute the track length (number of images) for a given observation.
// This transitively finds all correspondences and counts unique images.
size_t ComputeTrackLength(image_t image_id, point2D_t point2D_idx) const;

3、修改BA损失
定义两个损失类型变量,一个局部损失类型,一个全局损失类型,这3个损失类型依次理解为对异常点不同的抑制强度。

原始局部BA损失类型用的L1损失,这里改成可以配置的变量:

原始全局BA损失类型用的均权损失,这里也改成可以配置的变量:

开放对外参数:


python
"--Mapper.ba_local_loss_function_type", "2", # CAUCHY for local BA
"--Mapper.ba_global_loss_function_type", "2", # CAUCHY for global BA
4、带GPS的BA


对应代码:
c++
int RunGpsBundleAdjuster(int argc, char** argv) {
std::string input_path;
std::string output_path;
std::string database_path;
bool use_robust_loss_on_prior_position = true;
double prior_position_loss_scale = 7.815; // chi2 for 3DOF at 95%
OptionManager options;
options.AddRequiredOption("input_path", &input_path);
options.AddRequiredOption("output_path", &output_path);
options.AddRequiredOption("database_path", &database_path);
options.AddBundleAdjustmentOptions();
options.AddDefaultOption("use_robust_loss_on_prior_position",
&use_robust_loss_on_prior_position,
"Use robust loss (Cauchy) on GPS prior residuals");
options.AddDefaultOption("prior_position_loss_scale",
&prior_position_loss_scale,
"Loss scale for GPS prior (default: 7.815 for 95% chi2)");
if (!options.Parse(argc, argv)) {
return EXIT_FAILURE;
}
if (!ExistsDir(input_path)) {
LOG(ERROR) << "`input_path` is not a directory";
return EXIT_FAILURE;
}
if (!ExistsDir(output_path)) {
LOG(ERROR) << "`output_path` is not a directory";
return EXIT_FAILURE;
}
if (!ExistsFile(database_path)) {
LOG(ERROR) << "`database_path` does not exist";
return EXIT_FAILURE;
}
// Load reconstruction
auto reconstruction = std::make_shared<Reconstruction>();
reconstruction->Read(input_path);
LOG(INFO) << "Loaded reconstruction with " << reconstruction->NumImages()
<< " images and " << reconstruction->NumPoints3D() << " points";
// Load pose priors from database
auto database = Database::Open(database_path);
std::unordered_map<image_t, PosePrior> pose_priors;
const std::vector<Image> images = database->ReadAllImages();
for (const auto& image : images) {
if (database->ExistsPosePrior(image.ImageId())) {
PosePrior prior = database->ReadPosePrior(image.ImageId());
if (prior.IsValid()) {
pose_priors[image.ImageId()] = prior;
}
}
}
LOG(INFO) << "Loaded " << pose_priors.size() << " valid pose priors from database";
if (pose_priors.size() < 3) {
LOG(ERROR) << "Need at least 3 valid GPS priors for alignment";
return EXIT_FAILURE;
}
// Check which registered images have priors
size_t registered_with_prior = 0;
for (const image_t image_id : reconstruction->RegImageIds()) {
if (pose_priors.count(image_id) > 0) {
registered_with_prior++;
}
}
LOG(INFO) << registered_with_prior << " registered images have GPS priors";
if (registered_with_prior < 3) {
LOG(ERROR) << "Need at least 3 registered images with GPS priors";
return EXIT_FAILURE;
}
// Setup pose priors with covariance if needed
for (auto& [image_id, prior] : pose_priors) {
if (!prior.IsCovarianceValid()) {
// Set default covariance (1m standard deviation)
prior.position_covariance = Eigen::Matrix3d::Identity();
}
}
// Create config for all images
BundleAdjustmentConfig config;
for (const image_t image_id : reconstruction->RegImageIds()) {
config.AddImage(image_id);
}
for (const point3D_t point3D_id : reconstruction->Point3DIds()) {
config.AddVariablePoint(point3D_id);
}
// Setup prior options
PosePriorBundleAdjustmentOptions prior_options;
prior_options.use_robust_loss_on_prior_position = use_robust_loss_on_prior_position;
prior_options.prior_position_loss_scale = prior_position_loss_scale;
// Create and run GPS bundle adjuster
auto ba = CreatePosePriorBundleAdjuster(
*options.bundle_adjustment,
prior_options,
config,
pose_priors,
*reconstruction);
LOG(INFO) << "Running GPS-aligned bundle adjustment...";
ceres::Solver::Summary summary = ba->Solve();
if (summary.IsSolutionUsable()) {
LOG(INFO) << "GPS BA completed successfully";
reconstruction->Write(output_path);
LOG(INFO) << "Saved aligned reconstruction to " << output_path;
return EXIT_SUCCESS;
} else {
LOG(ERROR) << "GPS BA failed";
return EXIT_FAILURE;
}
}


调用:
python
# 典型用法
colmap gps_bundle_adjuster \
--input_path sparse/0 \
--output_path sparse_gps_aligned \
--database_path database.db \
--use_robust_loss_on_prior_position 1 \
--prior_position_loss_scale 7.815
5、 重新编译安装
注意要编译成release版本,否则速度慢的离谱。
python
cd /root/code/colmap && \
rm -rf build/* && \
cp -r /root/code/colmap-0509/build/_deps/* build/_deps/ 2>/dev/null || mkdir -p build/_deps && \
cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCUDA_ENABLED=ON -DFETCHCONTENT_FULLY_DISCONNECTED=ON && \
cd build && make -j$(nproc) && make install
其中,cp -r /root/code/colmap-0509/build/_deps/* build/_deps/ 2>/dev/null || mkdir -p build/_deps && \使用了之前编译好的第三方依赖,当然也可以自己下载faiss和PoseLib等库,但是网络经常不好,容易下载失败。