目录
一、找到model_merger的具体实现函数:RunModelMerge
三、合并重建MergeAndFilterReconstructions代码
2. MergeAndFilterReconstructions 函数
4. AlignReconstructionsViaReprojections 函数
一、找到model_merger的具体实现函数:RunModelMerge
二、分析RunModelMerge代码
int RunModelMerger(int argc, char** argv) {
std::string input_path1;
std::string input_path2;
std::string output_path;
double max_reproj_error = 64.0;
OptionManager options;
options.AddRequiredOption("input_path1", &input_path1);
options.AddRequiredOption("input_path2", &input_path2);
options.AddRequiredOption("output_path", &output_path);
options.AddDefaultOption("max_reproj_error", &max_reproj_error);
options.Parse(argc, argv);
Reconstruction reconstruction1;
reconstruction1.Read(input_path1);
PrintHeading2("Reconstruction 1");
LOG(INFO) << StringPrintf("Images: %d", reconstruction1.NumRegImages());
LOG(INFO) << StringPrintf("Points: %d", reconstruction1.NumPoints3D());
Reconstruction reconstruction2;
reconstruction2.Read(input_path2);
PrintHeading2("Reconstruction 2");
LOG(INFO) << StringPrintf("Images: %d", reconstruction2.NumRegImages());
LOG(INFO) << StringPrintf("Points: %d", reconstruction2.NumPoints3D());
PrintHeading2("Merging reconstructions");
if (MergeAndFilterReconstructions(
max_reproj_error, reconstruction1, reconstruction2)) {
LOG(INFO) << "=> Merge succeeded";
PrintHeading2("Merged reconstruction");
LOG(INFO) << StringPrintf("Images: %d", reconstruction2.NumRegImages());
LOG(INFO) << StringPrintf("Points: %d", reconstruction2.NumPoints3D());
} else {
LOG(INFO) << "=> Merge failed";
}
reconstruction2.Write(output_path);
return EXIT_SUCCESS;
}
这段代码包含了7步操作分别是:
1.定义输入输出路径变量以及重投影误差变量
2.OptionManager 设置:使用 OptionManager
来解析命令行参数,指定输入文件路径、输出路径和最大重投影误差等。
3.加载和打印第一个重建(reconstruction1
):创建一个 Reconstruction
对象并通过 Read
方法从文件 input_path1
加载数据。然后打印该重建的图片数量和三维点数量。
4.加载和打印第二个重建(reconstruction2
)
5.合并重建:调用 MergeAndFilterReconstructions
函数合并两个重建,如果合并成功,打印合并后的重建的图片和三维点数量;如果失败,输出失败信息。
6.写入合并后的重建:将合并后的重建结果保存到指定的 output_path
文件中。
7.程序执行成功后返回退出代码 EXIT_SUCCESS
。
可以看出核心代码在于第5步合并重建MergeAndFilterReconstructions
三、合并重建MergeAndFilterReconstructions代码
bool MergeAndFilterReconstructions(const double max_reproj_error,
const Reconstruction& src_reconstruction,
Reconstruction& tgt_reconstruction) {
if (!MergeReconstructions(
max_reproj_error, src_reconstruction, tgt_reconstruction)) {
return false;
}
ObservationManager(tgt_reconstruction)
.FilterAllPoints3D(max_reproj_error, /*min_tri_angle=*/0);
return true;
}
可以看出,第一步是合并两个模型,第二步根据重投影误差过滤3d点
四、MergeReconstructions
bool MergeReconstructions(const double max_reproj_error,
const Reconstruction& src_reconstruction,
Reconstruction& tgt_reconstruction) {
Sim3d tgt_from_src;
if (!AlignReconstructionsViaReprojections(src_reconstruction,
tgt_reconstruction,
/*min_inlier_observations=*/0.3,
max_reproj_error,
&tgt_from_src)) {
return false;
}
// Find common and missing images in the two reconstructions.
std::unordered_set<image_t> common_image_ids;
common_image_ids.reserve(src_reconstruction.NumRegImages());
std::unordered_set<image_t> missing_image_ids;
missing_image_ids.reserve(src_reconstruction.NumRegImages());
for (const auto& image_id : src_reconstruction.RegImageIds()) {
if (tgt_reconstruction.ExistsImage(image_id)) {
common_image_ids.insert(image_id);
} else {
missing_image_ids.insert(image_id);
}
}
// Register the missing images in this src_reconstruction.
for (const auto image_id : missing_image_ids) {
auto src_image = src_reconstruction.Image(image_id);
src_image.ResetCameraPtr();
src_image.SetCamFromWorld(
TransformCameraWorld(tgt_from_src, src_image.CamFromWorld()));
if (!tgt_reconstruction.ExistsCamera(src_image.CameraId())) {
tgt_reconstruction.AddCamera(
src_reconstruction.Camera(src_image.CameraId()));
}
tgt_reconstruction.AddImage(src_image);
tgt_reconstruction.RegisterImage(image_id);
}
// Merge the two point clouds using the following two rules:
// - copy points to this src_reconstruction with non-conflicting tracks,
// i.e. points that do not have an already triangulated observation
// in this src_reconstruction.
// - merge tracks that are unambiguous, i.e. only merge points in the two
// reconstructions if they have a one-to-one mapping.
// Note that in both cases no cheirality or reprojection test is performed.
for (const auto& point3D : src_reconstruction.Points3D()) {
Track new_track;
Track old_track;
std::unordered_set<point3D_t> old_point3D_ids;
for (const auto& track_el : point3D.second.track.Elements()) {
if (common_image_ids.count(track_el.image_id) > 0) {
const auto& point2D = tgt_reconstruction.Image(track_el.image_id)
.Point2D(track_el.point2D_idx);
if (point2D.HasPoint3D()) {
old_track.AddElement(track_el);
old_point3D_ids.insert(point2D.point3D_id);
} else {
new_track.AddElement(track_el);
}
} else if (missing_image_ids.count(track_el.image_id) > 0) {
tgt_reconstruction.Image(track_el.image_id)
.ResetPoint3DForPoint2D(track_el.point2D_idx);
new_track.AddElement(track_el);
}
}
const bool create_new_point = new_track.Length() >= 2;
const bool merge_new_and_old_point =
(new_track.Length() + old_track.Length()) >= 2 &&
old_point3D_ids.size() == 1;
if (create_new_point || merge_new_and_old_point) {
const Eigen::Vector3d xyz = tgt_from_src * point3D.second.xyz;
const auto point3D_id =
tgt_reconstruction.AddPoint3D(xyz, new_track, point3D.second.color);
if (old_point3D_ids.size() == 1) {
tgt_reconstruction.MergePoints3D(point3D_id, *old_point3D_ids.begin());
}
}
}
return true;
}
合并过程如下:
1.估计相似变换:
Sim3d tgt_from_src;
if (!AlignReconstructionsViaReprojections(src_reconstruction,
tgt_reconstruction,
/*min_inlier_observations=*/0.3,
max_reproj_error,
&tgt_from_src)) {
return false;
}
2. 查找共有和缺失的图像:
这段代码首先查找 src_reconstruction
中的图像 ID,确定哪些图像在目标重建中存在,哪些是缺失的。图像的 ID 被分别存储在 common_image_ids
(共有图像)和 missing_image_ids
(缺失图像)中。
std::unordered_set<image_t> common_image_ids;
common_image_ids.reserve(src_reconstruction.NumRegImages());
std::unordered_set<image_t> missing_image_ids;
missing_image_ids.reserve(src_reconstruction.NumRegImages());
for (const auto& image_id : src_reconstruction.RegImageIds()) {
if (tgt_reconstruction.ExistsImage(image_id)) {
common_image_ids.insert(image_id);
} else {
missing_image_ids.insert(image_id);
}
}
3.注册缺失的图像:
对于 missing_image_ids
中的每个图像,首先获取源重建中的图像对象 src_image
。然后将其相机从世界坐标系转换到目标重建中的坐标系(通过 TransformCameraWorld
和 tgt_from_src
)。如果目标重建中不存在相机,添加新的相机。接着,将图像添加到目标重建中,并注册图像。
for (const auto image_id : missing_image_ids) {
auto src_image = src_reconstruction.Image(image_id);
src_image.ResetCameraPtr();
src_image.SetCamFromWorld(
TransformCameraWorld(tgt_from_src, src_image.CamFromWorld()));
if (!tgt_reconstruction.ExistsCamera(src_image.CameraId())) {
tgt_reconstruction.AddCamera(
src_reconstruction.Camera(src_image.CameraId()));
}
tgt_reconstruction.AddImage(src_image);
tgt_reconstruction.RegisterImage(image_id);
}
4.合并点云:
对于源重建中的每个 3D 点(point3D
),代码检查其轨迹(track
)中包含的每个 2D 点(track_el
)。如果该 2D 点存在于目标重建的图像中,且其关联的 3D 点已经存在,则将其加入 old_track
。如果该点在目标重建中没有关联的 3D 点,则加入 new_track
。如果该 2D 点来自缺失的图像,目标重建的该点云数据被重置。
for (const auto& point3D : src_reconstruction.Points3D()) {
Track new_track;
Track old_track;
std::unordered_set<point3D_t> old_point3D_ids;
for (const auto& track_el : point3D.second.track.Elements()) {
if (common_image_ids.count(track_el.image_id) > 0) {
const auto& point2D = tgt_reconstruction.Image(track_el.image_id)
.Point2D(track_el.point2D_idx);
if (point2D.HasPoint3D()) {
old_track.AddElement(track_el);
old_point3D_ids.insert(point2D.point3D_id);
} else {
new_track.AddElement(track_el);
}
} else if (missing_image_ids.count(track_el.image_id) > 0) {
tgt_reconstruction.Image(track_el.image_id)
.ResetPoint3DForPoint2D(track_el.point2D_idx);
new_track.AddElement(track_el);
}
}
5.创建或合并点:
如果 new_track
的长度大于或等于 2(即该轨迹包含足够的观测数据来创建一个新的 3D 点),或者 new_track
和 old_track
的长度之和大于或等于 2,并且 old_point3D_ids
只有一个 ID(即只有一个已有的 3D 点),则进行点云的创建或合并。创建新点时,使用变换后的 3D 坐标。若有可以合并的点,则合并 3D 点。
const bool create_new_point = new_track.Length() >= 2;
const bool merge_new_and_old_point =
(new_track.Length() + old_track.Length()) >= 2 &&
old_point3D_ids.size() == 1;
if (create_new_point || merge_new_and_old_point) {
const Eigen::Vector3d xyz = tgt_from_src * point3D.second.xyz;
const auto point3D_id =
tgt_reconstruction.AddPoint3D(xyz, new_track, point3D.second.color);
if (old_point3D_ids.size() == 1) {
tgt_reconstruction.MergePoints3D(point3D_id, *old_point3D_ids.begin());
}
}
五、sim3计算:
这步colmap中分析从一个模型转换到另外一个模型的sim3计算方法,从第四步中可以看到,该部分的关键函数是
AlignReconstructionsViaReprojections(src_reconstruction,tgt_reconstruction, /*min_inlier_observations=*/0.3,max_reproj_error, &tgt_from_src))
bool AlignReconstructionsViaReprojections(
const Reconstruction& src_reconstruction,
const Reconstruction& tgt_reconstruction,
const double min_inlier_observations,
const double max_reproj_error,
Sim3d* tgt_from_src) {
THROW_CHECK_GE(min_inlier_observations, 0.0);
THROW_CHECK_LE(min_inlier_observations, 1.0);
RANSACOptions ransac_options;
ransac_options.max_error = 1.0 - min_inlier_observations;
ransac_options.min_inlier_ratio = 0.2;
LORANSAC<ReconstructionAlignmentEstimator, ReconstructionAlignmentEstimator>
ransac(ransac_options,
ReconstructionAlignmentEstimator(
max_reproj_error, &src_reconstruction, &tgt_reconstruction),
ReconstructionAlignmentEstimator(
max_reproj_error, &src_reconstruction, &tgt_reconstruction));
const std::vector<std::pair<image_t, image_t>> common_image_ids =
src_reconstruction.FindCommonRegImageIds(tgt_reconstruction);
if (common_image_ids.size() < 3) {
return false;
}
std::vector<const Image*> src_images(common_image_ids.size());
std::vector<const Image*> tgt_images(common_image_ids.size());
for (size_t i = 0; i < common_image_ids.size(); ++i) {
src_images[i] = &src_reconstruction.Image(common_image_ids[i].first);
tgt_images[i] = &tgt_reconstruction.Image(common_image_ids[i].second);
}
const auto report = ransac.Estimate(src_images, tgt_images);
if (report.success) {
*tgt_from_src = report.model;
}
return report.success;
}
核心代码分析如下:
1.查找共有图像
FindCommonRegImageIds
函数用于找到源重建和目标重建中共有的图像对,并返回一个包含这些图像 ID 的对列表(common_image_ids
)。
const std::vector<std::pair<image_t, image_t>> common_image_ids =
src_reconstruction.FindCommonRegImageIds(tgt_reconstruction);
2.检查共有图像数量
如果共有的图像对少于 3 个,说明数据不足以进行有效的对齐,因此函数返回 false
。
if (common_image_ids.size() < 3) {
return false;
}
3.准备源图像和目标图像
将源重建和目标重建中的每对共有图像提取出来,分别存储在 src_images
和 tgt_images
向量中,以便后续用于 RANSAC 对齐过程。
std::vector<const Image*> src_images(common_image_ids.size());
std::vector<const Image*> tgt_images(common_image_ids.size());
for (size_t i = 0; i < common_image_ids.size(); ++i) {
src_images[i] = &src_reconstruction.Image(common_image_ids[i].first);
tgt_images[i] = &tgt_reconstruction.Image(common_image_ids[i].second);
}
4.执行 RANSAC 对齐
调用 ransac.Estimate()
方法,传入 src_images
和 tgt_images
,执行 RANSAC 对齐过程。该过程会尝试找到一个变换模型(例如,仿射变换或其他几何变换),将源重建的坐标系对齐到目标重建的坐标系。
const auto report = ransac.Estimate(src_images, tgt_images);
5.拿到相似变换:
if (report.success) {
*tgt_from_src = report.model;
}
六、总结
model_merger命令整体目的是实现两个重建模型(Reconstruction)的合并操作,特别是通过计算相似变换(Sim3d)对两个重建模型进行对齐,并合并它们的图像和三维点云数据。
1. RunModelMerger
函数
-
输入输出路径和参数设置:使用
OptionManager
来解析命令行参数,获取两个输入路径和一个输出路径。还设定了最大重投影误差(默认为 64.0)。 -
加载重建数据:分别加载两个重建数据,打印每个重建中包含的图像数量和三维点数量。
-
合并过程:调用
MergeAndFilterReconstructions
函数来合并两个重建。如果合并成功,打印合并后的重建数据(包括图像和三维点数量);否则,输出失败信息。 -
保存结果:将合并后的重建数据保存到指定的输出路径。
2. MergeAndFilterReconstructions
函数
-
通过调用
MergeReconstructions
合并两个重建。如果合并成功,进一步对目标重建中的三维点进行过滤(基于重投影误差)。 -
重投影误差过滤:通过
ObservationManager
对目标重建中的三维点进行过滤,移除误差过大的点。
3. MergeReconstructions
函数
-
相似变换估计:首先,使用
AlignReconstructionsViaReprojections
函数通过重投影误差对两个重建模型进行对齐,估计出源重建到目标重建的相似变换(Sim3d)。 -
查找共有和缺失的图像:比较两个重建中的图像,找出共有图像和缺失图像。
-
注册缺失的图像:将源重建中缺失的图像以及相关的相机和三维点注册到目标重建中。
-
合并点云:对于每个源重建中的三维点,检查其在目标重建中的对应点,并根据情况创建新点或合并已有点。
4. AlignReconstructionsViaReprojections
函数
-
查找共有图像:使用
FindCommonRegImageIds
找到源重建和目标重建中共有的图像。 -
执行 RANSAC 对齐:通过 RANSAC 算法估计源重建和目标重建之间的变换关系,确保只有足够的共有图像时才进行对齐。
-
获取相似变换:如果 RANSAC 对齐成功,返回相似变换模型(Sim3d)。
5. 核心步骤概括
-
估计相似变换:通过
AlignReconstructionsViaReprojections
对源重建和目标重建进行对齐,计算相似变换。 -
查找和注册缺失图像:在目标重建中注册源重建中的缺失图像。
-
合并点云:通过对轨迹数据的处理,创建新的三维点或者合并已有的点,确保两个重建中的三维点云合并无冲突。
-
过滤误差:通过重投影误差和最小角度限制,过滤掉不符合要求的点。