3. 工程结构解析:源码目录树分析、CMakeLists.txt构建系统、核心类依赖关系图、数据流架构
好,咱们今天来聊聊ORB-SLAM3的工程结构。说实话,我第一次打开这个源码的时候,也被它的目录结构吓了一跳——文件不少,类也不少。但别慌,我带你一层层剥开它。
3.1 源码目录树:从根到叶
先看看根目录下都有什么。我个人习惯,拿到一个新项目,第一件事就是看目录结构。这就像你去一个陌生的城市,先看地图一样。
ORB-SLAM3/
├── CMakeLists.txt # 主构建文件
├── Thirdparty/ # 第三方库
│ ├── DBow2/ # 词袋模型
│ ├── g2o/ # 图优化库
│ └── Sophus/ # 李代数库
├── Examples/ # 示例程序
│ ├── Monocular/ # 单目
│ ├── Stereo/ # 双目
│ ├── RGB-D/ # RGB-D
│ └── Monocular-Inertial/ # 单目+IMU
├── include/ # 头文件
│ └── System.h # 系统核心
├── src/ # 源文件
│ ├── System.cpp
│ ├── Tracking.cpp
│ ├── LocalMapping.cpp
│ ├── LoopClosing.cpp
│ └── ...
├── Vocabulary/ # 离线词袋
└── config/ # 配置文件
嗯,这里要注意:Thirdparty 里的三个库,是ORB-SLAM3的基石。DBow2做回环检测,g2o做后端优化,Sophus处理位姿。我在项目中遇到过,有人把g2o版本搞错了,编译直接报错——所以版本一定要对齐。
3.2 CMakeLists.txt构建系统:别小看它
很多初学者觉得CMakeLists.txt就是复制粘贴,其实不然。ORB-SLAM3的构建系统设计得挺讲究的。咱们看看核心部分:
cmake_minimum_required(VERSION 2.8)
project(ORB_SLAM3)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -O3")
# 找依赖
find_package(OpenCV 4.4 REQUIRED)
find_package(Eigen3 3.1.0 REQUIRED)
find_package(Pangolin REQUIRED)
# 第三方库
add_subdirectory(Thirdparty/g2o)
add_subdirectory(Thirdparty/DBow2)
add_subdirectory(Thirdparty/Sophus)
# 主库
add_library(${PROJECT_NAME} SHARED
src/System.cpp
src/Tracking.cpp
src/LocalMapping.cpp
src/LoopClosing.cpp
src/Atlas.cpp
src/KeyFrameDatabase.cpp
...
)
target_link_libraries(${PROJECT_NAME}
${OpenCV_LIBS}
${Eigen3_LIBS}
g2o
DBow2
Sophus::Sophus
Pangolin
)
你看,它把第三方库作为子模块编译,而不是用系统安装的。为什么?说白了,就是为了版本控制。我曾经因为系统g2o版本不对,折腾了两天——后来学乖了,直接用源码里的。
3.3 核心类依赖关系图
这是整个课程里最核心的一张图。我建议你多看几遍,把类之间的关系刻在脑子里。
你看,System 是总入口,它创建并管理三个核心线程:Tracking、LocalMapping 和 LoopClosing。这三个线程又共享 Atlas(地图集),而 Atlas 管理着多个 Map,每个 Map 包含 KeyFrame 和 MapPoint。
3.4 数据流架构:图像进来,位姿出去
好了,类的关系搞清楚了,咱们看看数据是怎么流动的。说白了,就是图像帧进来,经过一系列处理,最后输出相机位姿和地图。
| 步骤 | 模块 | 输入 | 输出 | 关键操作 |
|---|---|---|---|---|
| 1 | Tracking | 图像帧 | 初始位姿 | 特征提取、匹配、PnP |
| 2 | LocalMapping | 关键帧 | 局部地图 | 三角化、BA优化 |
| 3 | LoopClosing | 关键帧 | 回环校正 | 词袋检索、Sim3求解 |
| 4 | Atlas | 地图数据 | 全局地图 | 地图合并、管理 |
我举个例子,你想想看:当摄像头采集到一帧图像后,Tracking 线程会先提取ORB特征点,然后和上一帧或者地图点匹配,用PnP算法算出一个初始位姿。如果匹配质量好,就生成关键帧,丢给 LocalMapping 线程。
LocalMapping 拿到关键帧后,会做两件事:一是三角化新的地图点,二是做局部BA优化。嗯,这里要注意,BA优化很耗时,所以它只优化当前关键帧附近的帧和点,不是全局的。
最后,LoopClosing 线程会定期检查是否形成了回环。怎么检查?用词袋模型。如果检测到回环,就计算Sim3变换,然后做全局BA,把累积误差消除掉。
3.5 多线程架构:三个线程的协作
ORB-SLAM3 用了三个线程并行工作。它们之间怎么通信?说白了,就是通过队列和互斥锁。
// Tracking 向 LocalMapping 发送关键帧
mlNewKeyFrames.push_back(pKF); // 队列
// LocalMapping 处理
while(!mlNewKeyFrames.empty()) {
// 加锁
unique_lock<mutex> lock(mMutexNewKFs);
KeyFrame* pKF = mlNewKeyFrames.front();
mlNewKeyFrames.pop_front();
lock.unlock();
// 处理...
}
你看,Tracking 把关键帧塞进队列,LocalMapping 从队列里取出来处理。这种生产者-消费者模式,在多线程编程里很常见。我建议你仔细看看 Tracking.cpp 里的 GrabImageMonocular() 函数,它是整个数据流的起点。
好了,工程结构就讲到这里。记住这张依赖关系图,后面咱们逐行解析代码时,你会经常回来参考它。