文章

探索TheiaSfM:1-概述

An Overview of the TheiaSfM Project and Introductions to GFlags, GTest, and GLog

0 从基础使用开始

先了解TheiaSfM库作为一个三维重建库的基础应用,这些使用在源码的appliction文件夹下有详细的举例,这里以一个基本的重建任务为例,源文件build_reconstruction.cc代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <vector>
#include <string>
#include <gflags/gflags.h>
#include <glog/logging.h>
#include <theia/theia.h>

using namespace theia;
int main(int argc, char* argv[]) {
  THEIA_GFLAGS_NAMESPACE::ParseCommandLineFlags(&argc, &argv, true);
  google::InitGoogleLogging(argv[0]);

  CHECK_GT(FLAGS_output_reconstruction.size(), 0);

  // Initialize the features and matches database.
  std::unique_ptr<FeaturesAndMatchesDatabase> features_and_matches_database(
      new theia::RocksDbFeaturesAndMatchesDatabase(
          FLAGS_matching_working_directory));

  // Create the reconstruction builder.
  const ReconstructionBuilderOptions options =
      SetReconstructionBuilderOptions();
  ReconstructionBuilder reconstruction_builder(
      options, features_and_matches_database.get());

  // If matches are provided, load matches otherwise load images.
  if (features_and_matches_database->NumMatches() > 0) {
    AddMatchesToReconstructionBuilder(features_and_matches_database.get(),
                                      &reconstruction_builder);
  } else if (FLAGS_images.size() != 0) {
    AddImagesToReconstructionBuilder(&reconstruction_builder);
  } else {
    LOG(FATAL) << "You must specifiy either images to reconstruct or supply a "
                  "database with matches stored in it.";
  }

  std::vector<Reconstruction*> reconstructions;
  CHECK(reconstruction_builder.BuildReconstruction(&reconstructions))
      << "Could not create a reconstruction.";

  for (int i = 0; i < reconstructions.size(); i++) {
    const std::string output_file =
        theia::StringPrintf("%s-%d", FLAGS_output_reconstruction.c_str(), i);
    LOG(INFO) << "Writing reconstruction " << i << " to " << output_file;
    CHECK(theia::WriteReconstruction(*reconstructions[i], output_file))
        << "Could not write reconstruction to file.";
  }
}

展开

从官方这份示例代码来看,略去LOGCHECK日志语词,应用层上一个重建任务的构建流程十分简洁清晰,首先如果能提供特征和匹配数据库FeaturesAndMatchesDatabase,则将数据库传入重建类ReconstructionBuilder,否则将传入图像集,然后由ReconstructionBuilder执行重建得到保存为std::vector<Reconsrtruction>的重建结果,最后依次输出为文件。

当然,这里略去了繁杂的各类重建的细节参数的设置。

上述代码阅后,不难看出:

  • ReconstructionBuilderReconstruction就是分管重建过程和重建数据的两个核心类,可以作为我们阅读源码的一个重要入口;
  • 特征与匹配本身与图像分析更紧密,在上述代码中与图像集都隶属于重建的入口数据源,可以将其的阅读优先级往后放;
  • 此外,我们还不禁问一个问题,为什么重建结果以vector的形式保存的,重建过程具体又是怎样的。

1 项目结构

首先一览TheiaSfM的项目结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
├─applications
├─cmake
├─data
├─docs
├─include
├─libraries
└─src
    └─theia
        ├─alignment
        ├─image
        │  ├─descriptor
        │  └─keypoint_detector
        ├─io
        ├─matching
        ├─math
        │  ├─graph
        │  ├─matrix
        │  └─probability
        ├─sfm
        │  ├─bundle_adjustment
        │  ├─camera
        │  ├─estimators
        │  ├─global_pose_estimation
        │  ├─pose
        │  ├─transformation
        │  ├─triangulation
        │  └─view_graph
        ├─solvers
        ├─test
        └─util

展开

applictions内是调用TheiaSfM库进行与三维重建相关任务的源码,cmake包含一些C++库查找和其他配置的CMake子模块,data存放一些用以测试的图像数据,docs是项目文档,include是theiaSfM库的包含路径,libraries是涉及到的一些三方库源码,最后src则是重点:TheiaSfM的源码。

  • alignment:内含单个头文件,用以提供Eigenstd::vector特化,一遍正确处理内存对齐问题;
  • image:图像特征提取;
  • io:图像、特征、标定文件、平差文件、重建文件等的输入输出流管理;
  • math:与矩阵、图和概率论相关的数学工具;
  • sfm:涉及SfM的相机模型、位姿、三角化、重建和平差的代码核心部分;
  • solvers:利用RANSAC及其变体进行模型估计的相关类;
  • test:单元测试主程序,各个单元的test代码分散上各个文件夹内,以模块名辅以_test.cc结尾;
  • util:一些工具函数或类,包括随机数、字符串、文件流、线程池等。

在所有的第三方库中,真正从输入开始“贯穿全文”的是GFlags、GLog和GTest,下面简单介绍这几个库的作用和使用。

2 GFlags简介

我们知道主函数int main(int argc, char* argv[])具有外部输入的参数,argc是外部参数数量加一,argv就是所有外部参数和程序字符串本身的集合。将这些参数排列成(name, value)对,那么生成的可执行程序program.exe在运行时可以有语义更加清晰的额外的配置,比如:

1
program -height -age 20 -name "Vincent"

展开

其中-height-age-name称为命令行标志(Commandline Flags), 20Vincent是对应的命令行参数(Commandline arguments),GFlags就是用来为程序定义命令行标志的C++库。

GFlags安装1很简单,官方文档2很详尽,这里简洁地概括一下。

GFlags编译好后在CMake中配置

1
2
find_package(gflags REQUIRED)
target_link_libraries(program gflags::gflags)

展开

然后在需要使用的地方引入头文件#include <gflags/gflags.h>,即可使用。

如果你需要定义一个标志,使用DEFINE_type(name, default_value, "description"),比如

  • DEFINE_bool(is_student, false, "Is the target a student.")
  • DEFINE_string(name, "", "Vincent")

具体支持以下几种类型:

  • DEFINE_bool: boolean
  • DEFINE_int32: 32-bit integer
  • DEFINE_int64: 64-bit integer
  • DEFINE_uint64: unsigned 64-bit integer
  • DEFINE_double: double
  • DEFINE_string: C++ string

标志定义处的当前源文件内皆可访问这些标志的参数值,方式是通过标志名前面添加FLAGS_,比如FLAGS_is_student。如果要引用其它源文件内定义的标志,需要在当前源文件进行标志的前置声明:DECLARE_bool(is_student)

如果需要对输出参数进行有效性检查,可以定义一个检查函数并传给DEFINE_validator(flag_name, &validation_function)

1
2
3
4
5
6
7
8
9
10
static bool IsFlagAgeValid(const char* flag_name, int value) 
{
    if (value > 0 && value < 100)
      return true;
    std::cout << "Invalid value for --" << flag_name 
              << ", value = " << value << std::endl;
   return false;
}
DEFINE_int32(age, 0, "Age of student");
DEFINE_validator(age, &IsFlagAgeValid);

展开

最后在使用时需要在主函数调用解析函数:

1
GFLAGS_NAMESPACE::ParseCommandLineFlags(&argc, &argv, true);

展开

取址符号表明了这个函数很可能会修改argcargv,修改方式取决于第三个参数remove_flags。如果reomve_flagstrue,将移除argv所有标志只保留参数,并调整argc;如果为false则只对argv进行重排,将标志移动到前面,参数移动到后面。

最后即可在调用可执行程序时,通过program --name "Vincent"的方式传递外部参数了,标志前单横线双横线皆可。

此外,有一些内置的默认标志。可以通过--helpfull显示所有的标志,使用--flagfile可以指定读入参数的文本文件,用来取代在命令行中手动敲入,TheiaSfM即是采用这种方式:

1
2
3
4
5
6
############### Input/Output ###############
# Input/output files.
# Set these if a matches file is not present. Images should be a filepath with a
# wildcard e.g., /home/my_username/my_images/*.jpg
--images= 
--output_matches_file= 

展开

TheiaSfM将GFLAGS_NAMESPACE用宏替换为了THEIA_GFLAGS_NAMESPACE,但GLog直接用的google::,暂不清楚如此处理的原因。可能是处理Gflags在不同版本时的命名空间不一致的问题。

GFlags更多细节和注意事项请查阅官方文档。

3 GLog简介

GLog全称Google Logging Library,由Google出品的日志库。类似地在官方Github3下载源码编译后由CMake引入:

1
2
find_package(glog REQUIRED)
target_link_libraries(program glog::glog)

展开

然后引入头文件并在主函数初始化即可使用GLog

1
2
3
4
5
#include <glog/logging.h>

int main(int argc, char* argv[]) {
    google::InitGoogleLogging(argv[0]);  
}

展开

argv即使在前文的gFlags初始化时进行了修改也不会造成冲突。GLog支持四个等级的日志输出INFOWARNINGERROR、和FATAL,其中FATAL会伴随着程序终止。

默认的日志输出格式为:

1
Lyyyymmdd hh:mm:ss.uuuuuu threadid file:line] msg...

展开

常见用法为LOG(level) << "message"即可打印对应等级的日志信息。

1
LOG(INFO) << "Program initialized.";

展开

还可以附带条件地打印日志信息:

1
LOG_IF(INFO, is_success) << "Program initialized.";

展开

我们可以为日志设置额外的等级,通过VLOG(level)进行输出,经由命令行标志-v来进行等级控制,只有小于设置等级的VLOG被打印。

1
2
VLOG(1) << "I’m printed when you run the program with --v=1 or higher";
VLOG(2) << "I’m printed when you run the program with --v=2 or higher";

展开

GLog还提供CHECK宏进行运行时条件检查,不满足时直接终止程序。

1
2
3
4
CHECK(name == "Vincent") << "Error person!";
CHECK_NE(1, 2) << ": The world must be ending!";
CHECK_EQ(string("abc")[1], 'b') << "That's strange!";
CHECK_NOTNULL(some_ptr) << "Empty pointer!";

展开

以上就是GLog的常见用法,更多细节以及和GFlags搭配使用请参考官方文档4

4 GTest简介

类似的,GTest从Github官方5下载源码编译后,在CMake中引入

1
2
find_package(GTest)
target_link_libraries(program GTest::gtest GTest::gtest_main)

展开

GTest通过写各种断言来对目标条件进行检测,失败后当前函数终止,否则程序继续执行。一条测试用例通过TEST(TestUnitName, TestName)定义,内部包含诸多断言语句。ASSERT_断言会产生致命失败并结束当前函数,EXPECT_断言会产生非致命失败但不终止当前函数。

1
2
3
4
5
6
7
8
9
10
11
12
// Tests factorial of 0.
TEST(FactorialTest, HandlesZeroInput) {
  EXPECT_EQ(Factorial(0), 1);
}

// Tests factorial of positive numbers.
TEST(FactorialTest, HandlesPositiveInput) {
  EXPECT_EQ(Factorial(1), 1);
  EXPECT_EQ(Factorial(2), 2);
  EXPECT_EQ(Factorial(3), 6);
  EXPECT_EQ(Factorial(8), 40320);
}

展开

在主函数种初始化后再RUN_ALL_TESTS()即可执行所有测试。

1
2
3
4
int main(int argc, char *argv[]) {
  google::InitGoogleLogging(argv[0]);
  return RUN_ALL_TESTS();
}

展开

执行目标程序时即可运行相应的测试用例

1
2
3
4
program # run all tests
program --gtest_filter=* # run all tests
program --gtest_filter=TargetUnit.* # run TestUnitName == TargetUnit
program --gtest_filter=TargetUnit.*-TargetUnit.Outsider # run TestUnitName == TargetUnit, except for TargetUnit.Outsider。

展开

其它与GLog相关的更为复杂的设置参加官方文档6

5 小结

本文概述了TheiaSfM的项目结构以及简单介绍了GFlagsGLogGTest的使用。

参考

  1. GFlags Github. https://github.com/gflags/gflags ↩︎

  2. How To Use gflags (formerly Google Commandline Flags). https://gflags.github.io/gflags ↩︎

  3. GLog Github. https://github.com/google/glog ↩︎

  4. Google Logging Libraryhttps://google.github.io/glog ↩︎

  5. GTest Github. https://github.com/google/googletest ↩︎

  6. GoogleTest User’s Guide. https://google.github.io/googletest ↩︎

本文由作者按照 CC BY NC SA 4.0 进行授权