Colmap增加轨迹过滤等参数

Colmap内置的 tri_ignore_two_view_tracks 只能过滤严格的两视图轨迹(track_length=2),有时会产生错误的3D点无法被过滤掉,影响整体效果,我希望通过增加轨迹阈值参数、修改BA损失类型、BA加GPS约束等来解决这一问题(实际解决不了)。后面已经没有必要自己慢慢调试colmap代码了,只要有好想法都可以让claude code直接帮忙修改测试。

1、准备调试环境

参考之前的文章Ubuntu下Colmap源码编译调试,这里直接以源码目录作为工程目录,tasks.jsonlaunch.jsonsettings.json都放到该目录下,目录结构如图:

核心操作:

安装GDB: apt update && apt install -y gdb

CMakeLists.txt中增加架构参数

配置tasks.jsonlaunch.jsonsettings.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等库,但是网络经常不好,容易下载失败。

相关推荐
千殇华来11 小时前
3D材料选择
百度·3d
yeflx12 小时前
COTR算法demo测试
3d
云飞云共享云桌面12 小时前
SolidWorks 服务器通过云飞云共享云桌面10人研发共享方案
运维·服务器·3d·设计模式·电脑
木斯佳13 小时前
HarmonyOS 6 ArkGraphics 3D精讲:坐标、向量与矩阵——初识3D数学的“空间建模”
线性代数·3d·矩阵
meilindehuzi_a13 小时前
Vibe Coding 实战:我用一条 Prompt 指挥 AI “盲盒式”生成 3D 积木物理世界
3d·prompt
小飞侠是个胖子13 小时前
在 WebGL 中构建高性能 3D 沉浸式系统的三套高阶方案
前端·3d
不知名的老吴15 小时前
CAXA 3D实体设计保姆级下载和安装教程(图文详解)
3d
DisonTangor1 天前
【SIGGRAPH 2026】Pixal3D: 基于图像的像素对齐三维生成
人工智能·3d·开源·aigc
CG_MAGIC1 天前
主流 3D 软件文件互通互导教程
3d·材质·效果图·建模教程·渲云渲染