具身智能工程领域all in one

主要存放机器人技术等相关工程技术,如ros的学习笔记

ros

ros是专门为机器人设计的操作系统, 但是需要在宿主操作系统上运行, 例如支持最完善的linux

就一个简单的项目来说, ros的目录树如上所述, 可以描述为:

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

WorkSpace --- 自定义的工作空间

|--- build:编译空间,用于存放CMake和catkin的缓存信息、配置信息和其他中间文件。

|--- devel:开发空间,用于存放编译后生成的目标文件,包括头文件、动态&静态链接库、可执行文件等。

|--- src: 源码

|-- package:功能包(ROS基本单元)包含多个节点、库与配置文件,包名所有字母小写,只能由字母、数字与下划线组成

|-- CMakeLists.txt 配置编译规则,比如源文件、依赖项、目标文件

|-- package.xml 包信息,比如:包名、版本、作者、依赖项...(以前版本是 manifest.xml)

|-- scripts 存储python文件

|-- src 存储C++源文件

|-- include 头文件

|-- msg 消息通信格式文件

|-- srv 服务通信格式文件

|-- action 动作格式文件

|-- launch 可一次性运行多个节点

|-- config 配置信息

|-- CMakeLists.txt: 编译的基本配置

相关的命令大部分都很直接, 例如roscd,rosls, rosrun, roslaunch等,除了操作单位是功能包以外和linux系统命令几乎一样, 具体可以参考ROS官方文档
一些独特的命令有:

  1. roscore: 启动ros的核心服务
  2. rosrun: 运行某个功能包中的节点
  3. roslaunch: 根据配置启动某个功能包中的多个节点
  4. rqt_graph: 查看多个节点之间的关系

在视觉 slam 领域里, 简单来说,相机的位姿(pose)就是相机的位置和姿态的合称,它描述了世界坐标系与相机坐标系之间的转换关系。

如上图所示:点 P 的世界坐标为 P_{w},可以通过相机的位姿矩阵 T 转换到相机坐标系下为 P_{c} : \[P_c=T_{cw}P_w\]

可以将点 P 从相机坐标系转换到世界坐标系中: \[P_w=T_{wc}P_c\]

其中 \(T_{cw}\) 为该点从世界坐标系变换到相机坐标系的变换矩阵, \(T_{wc}\) 为该点从相机坐标系变换到世界坐标系的变换矩阵。它们二者都可以用来表示相机的位姿,前者称为相机的外参。

通信

对一个机器人来说, 其功能模块可能非常繁杂, 且内部实现差异很大, 因此ros采取了耦合度低的分布式实现, 各个功能都有自己的进程, 准确地说ROS是进程(也称为Nodes)的分布式框架
ros的通信机制有三种实现策略:

  • 话题通信(发布订阅模式): 在这个模式下有三个角色: 管理者,发布者,订阅者;以名字来说很好理解其结构
  • 服务通信(请求响应模式): 类似网络请求, 以请求响应模式服务和客户端互相通信
  • 参数服务器(参数共享模式): 相当于一个全局状态/数据库, 所有节点都可以访问和修改

slam

FAST-LIVO2: Fast, Direct LiDAR-Inertial-Visual Odometry

以下简称f-l, f-l同时使用激光雷达和rgb摄像头作为输入, 激光提供点云信息, 在rosbag中其输入格式如下:

1
2
3
4
5
6
7
8

types: livox_ros_driver/CustomMsg [e4d6829bdfe657cb6c21a746c86b21a6]
sensor_msgs/Image [060021388200f6f0f447d0fcd9c64743]
sensor_msgs/Imu [6a62c6daae103f4ff57a132d6f95cec2]
topics: /left_camera/image 1355 msgs : sensor_msgs/Image
/livox/imu 27447 msgs : sensor_msgs/Imu
/livox/lidar 1355 msgs : livox_ros_driver/CustomMsg

f-l的整体框架如下:

输入数据的示意图:

因为看不懂细节, 这里只记录大致的流程, 三种数据中雷达(点云)是采样频率最高的, 因此会让它配合最慢的图像数据频率, 过程中不断使用这三种数据进行前向传播和反向优化来生成地图(点云)
下面介绍地图的格式, 地图的抽象格式为八叉树体素地图, 存储上使用哈希表存储根节点, 每个根节点体素是一个0.5m边长的正方体, 其中的叶节点则代表局部平面, 局部平面上存储雷达的原始点, 部分叶节点可以存储视觉补丁(patch), 这里不介绍细节, 地图构建过程中会在到达最大深度或者划分出的子体素代表一个平坦的面前不断划分体素, 并选择合适的体素加入视觉补丁
其构建和更新过程为:

  1. 起初以雷达中心为形心, 将周边的一个方块划为局部地图区域Mc
  2. 以雷达中心p为原点,在一个半径内制图
  3. 当p中心的圆碰到Mc的边缘后, 以p为中心重新划分局部地图区域Mc
  4. 由于Mc划定了存储在内存里的地图, 丢弃掉不属于当前范围的部分地图

  • patch: 图像的局部区域

视觉坐标和雷达坐标转化

该slam主要使用雷达提供点云, 以及存储点云的体素地图, 而视觉信息提供体素图的patch, 这些patch最直接的作用是给点云上色,除此之外, 也能用于校正整个体素图
具体流程如下:

  1. 选取视觉地图点: 简单地说,在体素图中选一些视觉上可见且比较明显的点,并将其映射到相机帧的像素上(相机和雷达是同步的), 这些点的集合称为视觉子图(visual submap), 具体分为以下几步:
    1. 可见点查询: 鉴于体素图中拥有过多的点,更合适的候选对象是当次雷达扫描的体素中的点, 这些点我们有它的世界坐标, 再加上雷达和相机的外参,可以将其转化到相机坐标系中,用一个raycast映射到对应像素。 此外,鉴于局部性原理, 对上一次雷达扫描涉及的有可见点的体素, 也对其做类似的可见点查询
    2. 按需raycast: 1. 中的可见点查询能提供很多视觉点, 但如果雷达因为太靠近扫描区等原因无法正常返回扫描点时, 就需要来自纯粹视觉信息的补充(准确地说是回忆, 因为过去雷达可能扫到了同一个物体的点但没有将其标记为视觉点, 对雷达退化的场合, 也就是是可见点查询无法覆盖整个相机帧时, 我们需要回忆出这些点), 将图像划分为一系列网格(网格大小是超参数), 如果有空白网格存在, 对其沿着射线反投影, 得到射线上一系列固定间隔的采样点(这些射线的采样点坐标会被预计算), 从深度最低的采样点开始, 对采样点所属体素(如果有的话), 如果有投影后位于网格的地图点, 将其加入视觉子图中
    3. 异常值剔除: 分为:
      1. 遮挡关系检查: 由于视觉上可能有很多点存在遮挡关系, 对映射到同一个网格的视觉点, 根据当前雷达位姿计算其深度, 只保留深度最小的
      2. 深度连续性检查: 由于相机帧是二维的, 两个相邻的像素, 其对应深度可能差别巨大;用雷达扫描的最新点投影到当前帧产生深度信息,从而剔除对和相邻点深度梯度较大的点
      3. 视角检查: 法向量和当前视线夹角过大的视觉点会被剔除

总之,这些视觉点就是一些位于场景表面, 能被映射到图像局部的一些扫描点, 主要用于提供patch位置和优化雷达采样准确性

点云语义分割

dualmap中,通过yolo检测rgb帧中物体的类别和bbox, 并用sam模型来进一下划分生成掩码,通过2d掩码结合深度信息, 反向投影得到点云, 再对点云进行聚类算法去噪,产生一个属于某个物体的点云, 并在之后的更新过程中进行一些简单地重叠度检测等操作,从而产生场景中点云的分割
这样的优点是实现简单, 只需要一个rgbd相机就能实现, 对2d图像做分割的算力负担也比较小, 缺点则是精度受限,且得到的点云实际上只有物品信息, 整个空间的建图信息应该是没有slam得到的点云全面的

openscene

OpenScene将3d场景的点云以及来自图像文本的clip嵌入对齐, 过程为:

  1. 图像语义提取: 就是对rgb图像进行逐点的语义嵌入,并将其融合到点云中,语义嵌入是通过2d图像分割模型如openseg做的; 点云的语义嵌入则通过对世界坐标系的点云通过相机外参转到相机坐标系,再用相机内参转移到像素坐标系,即对每帧,场景中的点可以投影到它的像素中, 也就是说一个点可以被关联到多个相机帧的不同像素,也可以完全不被投影,但只要有对应关系,就能将像素语义灌入点云中(文中使用的是对关联像素语义平均池化), 这一步最后得到的是一个特征点云
  2. 3d蒸馏: 此前的一步能得到来源于2d图像的特征点云, 并可以用于基于图像的导航, 但是如果想在非图像输入的任务中保留场景语义,就需要一个可以直接用3d点位置得到语义的编码器(网络), 也就是让一个接受点云输入的网络去学习我们上一步得到的特征点云, 例如使用MinkowskiNet18A卷积模型训练, 以余弦相似度为损失函数(L = 1 − cos(F2D, F3D)) , 使得3d点云的特征嵌入和2d图像的特征嵌入尽可能接近,从而实现对3d点云的语义理解,此外,这个训练过程也能一定程度上减少噪声
  3. 2D-3D 特征融合: 实验中发现, 2d特征对小物品和形状模糊的物品比较准确,而3d特征对大的,形状明确的物品比较准确,因此考虑将两者的优点融合,对两种特征以及一段预定义的文本提示词嵌入计算cos相似度, 选择得分更高的作为最终的语义标签

对于推理任务, 使用余弦相似度作为推断标准

UniM-OV3D

单就以点云、图像(rgb/rgbd)、文本等模态为数据的3d空间感知问题来说, 目前主流的问题是,语义信息(例如嵌入向量)的产生比较依赖clip这种视觉模型, 因此要么将点云转化为图片输入clip, 要么用图像和深度信息生成点云,或者将语义灌输到点云中, 也就是把点云和图像对齐
而本文的思路是: 将点云、图像、文本统一编码到特征向量空间中:

  1. 点云模型预训练: 训练的数据是经过标志的3d点云, 将标注对象分为常见类别和新颖类别, 训练时, 模型只能访问常见类别对象的相应注释, 由此提高模型的zero-shot能力
  2. 点云分层特征提取, 以点云为输入, 使用变形金刚对齐点云层次, 然后用残差注意力产生嵌入向量
  3. 点云语义嵌入
  4. 多模态对齐

OpenMask3D

这篇论文的方法比较工程化, 使用类别无关的3d物体分割,再用根据rgbd的原始数据得到其视觉视图,用视觉视图计算语义嵌入, 从而得到开放词汇的3d语义点云图
由于希望利用clip等嵌入模型强大的zero-shot能力, 我们只关心空间中的物体实例分割, 而不关心其类别, 即使使用基于类别的分割模型, 也要丢弃类别数据(本文使用mask模型)
点云到相机帧的映射通过rgbd数据得到, 假设每个分割掩码mask(物体实例)至少在一帧里可见, 对掩码中的3d点,计算其是否落在合法的像素坐标内, 且需要判断是否相机拍到的相机数据和几何计算出的有很大差距, 这样的操作后,对一个实例掩码,会得到一个相机帧掩码的列表, 也就是将3d掩码转化为2d掩码, 对于这样的掩码, 由于3d-2d转换可能有噪声以及误差, 再使用一个类别无关的2d分割模型, 优化2d掩码
得到top-k(基于可见点数量选择)个这样的2d掩码后,将其输入clip计算语义嵌入,并取平均值

POP-3D

这篇paper提出了基于模型的特征体素图的生成方式,具体来说:

  1. 2d-3d编码器: 这部分看上去很神奇,训练一个接受2d图像输入的编码器,生成3d体素图(有一个特征维度)
  2. 占用头: 输入一个特征体素, 将其分类为占用或者为空, 用这个head产生所有体素的占用情况
  3. 3d-语言头: 与占用头并行, 输入一个特征体素, 输出其对齐到视觉语言向量空间的一个嵌入, 这是因为1. 产生的特征泛化能力有限, vl的特征则有clip等泛化能力强大可以实现开放词汇的模型

当然,这个第一步其实需要额外的训练数据,文章中使用vlm模型提取图像特征,再用雷达数据将提取到的图像特征反投影到体素图中, 从而得到ground-truth用于训练, 也就是只有训练时需要雷达点云数据, 通过训练让模型得到从图像生成点云的能力

OpenSU3D

输入为rgbd, 使用实例分割模型提取实例的2d掩码, 用gpt4v模型处理掩码得到自然语言描述, 再用clip编码整个图像、2d掩码的多个缩放
对个2d掩码,反投影到点云空间中,每个像素对应一个点, 对场景中已有的同位置点云, 计算欧几里得距离, 不超过门槛值就进行一一匹配, 存储这些点云对 , 由于空间点云用kd-tree存储,这样的计算是成本可控的
当重叠点云数量中的实例id到达门槛后,取其中最多的标签作为这部分的点云实例id
点云会被使用dbscan聚类来后处理, 且由于一个实例点云可能会对应多个视图, 用置信度得分保留最可靠的一个描述或者类别
特征嵌入则使用基于经验的加权聚合,此处略公式

VL-SAT

这篇有些特殊, 它假设已经有完整的场景点云以及类别无关的实例级别分割掩码, 将问题定义为建立拓扑关系图, 也就是<头结点,尾结点,关系>三元组, 其中头结点和尾结点是实例掩码, 是封闭集的谓词标签
因此抽象地说,问题就是对有向图的节点对分类, 对每个节点对, 用其几何特征的拼接作为边缘特征r的输入(这个特征使用MLP产生)

这篇Paper的一个特点是, 使用所谓的视觉语言语义来辅助训练, 也就是借助clip这样的vlm来产生语义编码, 仅在训练时参考这些语义信息

openfusion++

这篇文章主要解决的是从2d掩码建立实例分割时遇到的边缘模糊问题, 它假设每个(小)体素只对应最多一个实例, 因此对每个体素计算点云的平均置信度, 将最高的一个实例点云关联到这个体素
此外, 对多个实例, 为了解决动态环境更新实例的问题, 通过投射2d掩码的深度得到其在平面上的面积, 以这个面积作为判断实例准确性的标准, 将其作为key维护一个最小堆, 堆里存储各个view里的嵌入语义
查询时, 使用一个关键词提取模型提取查询的关键词,再对其编码得到语义向量, 以余弦相似度匹配点云图语义

cmake

modern cmake

cmake 行为准则:
接下来的两个列表很大程度上基于优秀的 gist Effective Modern CMake.

CMake 应避免的行为

不要使用具有全局作用域的函数:这包含 link_directories、 include_libraries 等相似的函数。
不要添加非必要的 PUBLIC 要求:你应该避免把一些不必要的东西强加给用户(-Wall)。相比于 PUBLIC,更应该把他们声明为 PRIVATE。
不要在file函数中添加 GLOB 文件:如果不重新运行 CMake,Make 或者其他的工具将不会知道你是否添加了某个文件。值得注意的是,CMake 3.12 添加了一个 CONFIGURE_DEPENDS 标志能够使你更好的完成这件事。
将库直接链接到需要构建的目标上:如果可以的话,总是显式地将库链接到目标上。
当链接库文件时,不要省略 PUBLIC 或 PRIVATE 关键字:这将会导致后续所有的链接都是缺省的。

CMake 应遵守的规范

把 CMake 程序视作代码:它是代码。它应该和其他的代码一样,是整洁并且可读的。
建立目标的观念:你的目标应该代表一系列的概念。为任何需要保持一致的东西指定一个 (导入型)INTERFACE 目标,然后每次都链接到该目标。
导出你的接口:你的 CMake 项目应该可以直接构建或者安装。
为库书写一个 Config.cmake 文件:这是库作者为支持客户的体验而应该做的。
声明一个 ALIAS 目标以保持使用的一致性:使用 add_subdirectory 和 find_package 应该提供相同的目标和命名空间。
将常见的功能合并到有详细文档的函数或宏中:函数往往是更好的选择。
使用小写的函数名: CMake 的函数和宏的名字可以定义为大写或小写,但是一般都使用小写,变量名用大写。
使用 cmake_policy 和/或 限定版本号范围: 每次改变版本特性 (policy) 都要有据可依。应该只有不得不使用旧特性时才降低特性 (policy) 版本。

ros的cmake实例

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149

cmake_minimum_required(VERSION 2.8.3)
project(fast_livo)

set(CMAKE_BUILD_TYPE "Release")
message(STATUS "Build Type: ${CMAKE_BUILD_TYPE}")

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# Set common compile options
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread -fexceptions")

# Specific settings for Debug build
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -O0 -g")

# Detect CPU architecture
message(STATUS "Current CPU architecture: ${CMAKE_SYSTEM_PROCESSOR}")

# Specific settings for Release build
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm|aarch64|ARM|AARCH64)")
if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64")
# 64-bit ARM optimizations (e.g., RK3588 and Jetson Orin NX)
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -mcpu=native -mtune=native -ffast-math")
message(STATUS "Using 64-bit ARM optimizations: -O3 -mcpu=native -mtune=native -ffast-math")
else()
# 32-bit ARM optimizations with NEON support
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -mcpu=native -mtune=native -mfpu=neon -ffast-math")
message(STATUS "Using 32-bit ARM optimizations: -O3 -mcpu=native -mtune=native -mfpu=neon -ffast-math")
endif()
add_definitions(-DARM_ARCH)
else()
# x86-64 (Intel/AMD) optimizations
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -march=native -mtune=native -funroll-loops") #-flto
message(STATUS "Using general x86 optimizations: -O3 -march=native -mtune=native -funroll-loops")
add_definitions(-DX86_ARCH)
endif()

# Define project root directory
add_definitions(-DROOT_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}/\")

# Detect CPU core count for potential multithreading optimization
include(ProcessorCount)
ProcessorCount(N)
message(STATUS "Processor count: ${N}")

# Set the number of cores for multithreading
if(N GREATER 4)
math(EXPR PROC_NUM "4")
add_definitions(-DMP_EN -DMP_PROC_NUM=${PROC_NUM})
message(STATUS "Multithreading enabled. Cores: ${PROC_NUM}")
elseif(N GREATER 1)
math(EXPR PROC_NUM "${N}")
add_definitions(-DMP_EN -DMP_PROC_NUM=${PROC_NUM})
message(STATUS "Multithreading enabled. Cores: ${PROC_NUM}")
else()
add_definitions(-DMP_PROC_NUM=1)
message(STATUS "Single core detected. Multithreading disabled.")
endif()

# Check for OpenMP support
find_package(OpenMP QUIET)
if(OpenMP_CXX_FOUND)
message(STATUS "OpenMP found")
add_compile_options(${OpenMP_CXX_FLAGS})
else()
message(STATUS "OpenMP not found, proceeding without it")
endif()

# Check for mimalloc support
find_package(mimalloc QUIET)
if(mimalloc_FOUND)
message(STATUS "mimalloc found")
else()
message(STATUS "mimalloc not found, proceeding without it")
endif()

# Find catkin and required dependencies
find_package(catkin REQUIRED COMPONENTS
geometry_msgs
nav_msgs
sensor_msgs
roscpp
rospy
std_msgs
pcl_ros
tf
message_generation
eigen_conversions
vikit_common
vikit_ros
cv_bridge
image_transport
)

find_package(Eigen3 REQUIRED)
find_package(PCL REQUIRED)
find_package(OpenCV REQUIRED)
find_package(Sophus REQUIRED)
find_package(Boost REQUIRED COMPONENTS thread)

set(Sophus_LIBRARIES libSophus.so)

# Define the catkin package
catkin_package(
CATKIN_DEPENDS geometry_msgs nav_msgs roscpp rospy std_msgs message_runtime cv_bridge vikit_common vikit_ros image_transport
DEPENDS EIGEN3 PCL OpenCV Sophus
)

# Include directories for dependencies
include_directories(
${catkin_INCLUDE_DIRS}
${EIGEN3_INCLUDE_DIR}
${PCL_INCLUDE_DIRS}
${OpenCV_INCLUDE_DIRS}
${Sophus_INCLUDE_DIRS}
include
)

# Add libraries
add_library(vio src/vio.cpp src/frame.cpp src/visual_point.cpp)
add_library(lio src/voxel_map.cpp)
add_library(pre src/preprocess.cpp)
add_library(imu_proc src/IMU_Processing.cpp)
add_library(laser_mapping src/LIVMapper.cpp)

# Add the main executable
add_executable(fastlivo_mapping src/main.cpp)

# Link libraries to the executable
target_link_libraries(fastlivo_mapping
laser_mapping
vio
lio
pre
imu_proc
${catkin_LIBRARIES}
${PCL_LIBRARIES}
${OpenCV_LIBRARIES}
${Sophus_LIBRARIES}
${Boost_LIBRARIES}
)

# Link mimalloc if found
if(mimalloc_FOUND)
target_link_libraries(fastlivo_mapping mimalloc)
endif()

文件结构分析

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
CMakeLists.txt 文件结构
├── 1. 基础配置 (行 1-5)
│ ├── CMake 最低版本要求
│ └── 项目名称和构建类型

├── 2. 编译器配置 (行 7-48)
│ ├── C++ 标准设置
│ ├── 基础编译选项
│ ├── CPU 架构检测
│ └── 平台特定优化

├── 3. 多线程配置 (行 50-72)
│ ├── CPU 核心数检测
│ ├── OpenMP 支持
│ └── mimalloc 内存分配器

├── 4. 依赖管理 (行 74-104)
│ ├── ROS (Catkin) 依赖
│ ├── 第三方库 (Eigen, PCL, OpenCV, Sophus)
│ └── Catkin 包配置

├── 5. 编译目标 (行 106-130)
│ ├── 库文件定义
│ ├── 可执行文件定义
│ └── 链接配置

└── 6. 条件编译 (行 132-135)
└── mimalloc 可选链接

逐行解析

1
cmake_minimum_required(VERSION 2.8.3)
  • 作用:指定 CMake 的最低版本要求
  • 解释:确保使用的 CMake 版本支持后续的语法和功能
  • 版本 2.8.3:较老的版本,保证在旧系统上的兼容性
1
project(fast_livo)
  • 关键字project(项目名称)
  • 作用:定义项目名称,会自动设置以下变量:
    • PROJECT_NAME:项目名称(fast_livo)
    • PROJECT_SOURCE_DIR:项目源代码根目录
    • PROJECT_BINARY_DIR:构建输出目录
1
2
set(CMAKE_BUILD_TYPE "Release")
message(STATUS "Build Type: ${CMAKE_BUILD_TYPE}")
  • set(变量名 值):设置 CMake 变量
  • CMAKE_BUILD_TYPE:构建类型,常用值:
    • Debug:包含调试信息,无优化(适合开发调试)
    • Release:高度优化,无调试信息(适合发布)
    • RelWithDebInfo:优化 + 调试信息
    • MinSizeRel:最小化体积
  • message():输出信息到控制台
    • STATUS:普通信息(绿色)
    • WARNING:警告(黄色)
    • FATAL_ERROR:致命错误,停止配置

1
2
3
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
  • CMAKE_CXX_STANDARD:C++ 标准版本
    • 17:使用 C++17 标准(支持 std::optional、结构化绑定等)
  • CMAKE_CXX_STANDARD_REQUIRED:是否强制要求
    • ON:如果编译器不支持 C++17,报错
    • OFF:降级到编译器支持的最高版本
  • CMAKE_CXX_EXTENSIONS:是否允许编译器扩展
    • OFF:禁用 GNU 扩展(如 -std=gnu++17),使用纯标准 -std=c++17
1
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread -fexceptions")
  • CMAKE_CXX_FLAGS:全局 C++ 编译选项
  • -pthread:启用 POSIX 线程支持(必需,因为代码使用 std::thread
  • -fexceptions:启用 C++ 异常处理
1
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -O0 -g")
  • CMAKE_CXX_FLAGS_DEBUG:Debug 模式特定的编译选项
  • -O0:关闭所有优化(方便调试)
  • -g:生成调试符号(GDB 可以设置断点、查看变量)

CPU 架构检测与优化
1
message(STATUS "Current CPU architecture: ${CMAKE_SYSTEM_PROCESSOR}")
  • CMAKE_SYSTEM_PROCESSOR:自动检测的 CPU 架构
    • x86_64:Intel/AMD 64 位处理器
    • aarch64:ARM 64 位(如树莓派 4、Jetson Orin)
    • armv7l:ARM 32 位(如树莓派 3)
ARM 架构优化
1
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm|aarch64|ARM|AARCH64)")
  • if(条件):条件判断
  • MATCHES:正则表达式匹配
  • ^(arm|aarch64|ARM|AARCH64):匹配任何以 arm 或 aarch64 开头的字符串
1
2
if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -mcpu=native -mtune=native -ffast-math")
  • 64 位 ARM 优化选项
    • -O3:最高级别优化(激进,可能增加代码体积)
    • -mcpu=native:针对当前 CPU 生成代码(如利用 RK3588 的特殊指令)
    • -mtune=native:调度优化,适应当前 CPU 的流水线特性
    • -ffast-math:放松浮点运算标准,提升速度(可能损失精度)
1
2
else()
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -mcpu=native -mtune=native -mfpu=neon -ffast-math")
  • 32 位 ARM 额外选项
    • -mfpu=neon:启用 ARM NEON SIMD 指令集(向量化加速)
1
add_definitions(-DARM_ARCH)
  • add_definitions():添加预处理器宏定义
  • 效果:代码中可以使用 #ifdef ARM_ARCH 进行条件编译
x86-64 架构优化
1
2
else()
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -march=native -mtune=native -funroll-loops")
  • x86-64 优化选项
    • -march=native:使用当前 CPU 的指令集(如 AVX2、AVX-512)
    • -funroll-loops:循环展开,减少跳转指令

项目根目录定义
1
add_definitions(-DROOT_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}/\")
  • CMAKE_CURRENT_SOURCE_DIR:当前 CMakeLists.txt 所在目录
  • 效果:代码中可以使用 ROOT_DIR 宏访问项目根目录
  • 用途:加载配置文件(如 ROOT_DIR/config/params.yaml

多线程配置
1
2
3
include(ProcessorCount)
ProcessorCount(N)
message(STATUS "Processor count: ${N}")
  • include(模块名):加载 CMake 内置模块
  • ProcessorCount:检测 CPU 核心数,结果存入变量 N
1
2
3
if(N GREATER 4)
math(EXPR PROC_NUM "4")
add_definitions(-DMP_EN -DMP_PROC_NUM=${PROC_NUM})
  • 逻辑
    • 如果 CPU 核心 > 4,使用 4 个核心(避免过度并行开销)
    • 如果 CPU 核心 ≤ 4,使用全部核心
  • math(EXPR 变量 表达式):数学运算
  • 宏定义
    • -DMP_EN:启用多线程模式
    • -DMP_PROC_NUM=4:定义线程数为 4
OpenMP 支持
1
2
3
4
find_package(OpenMP QUIET)
if(OpenMP_CXX_FOUND)
message(STATUS "OpenMP found")
add_compile_options(${OpenMP_CXX_FLAGS})
  • find_package(包名 [QUIET] [REQUIRED]):查找第三方库
    • QUIET:找不到时不报错
    • REQUIRED:找不到时停止配置
  • OpenMP:并行计算框架(自动并行化 for 循环)
  • OpenMP_CXX_FOUND:布尔变量,表示是否找到
  • add_compile_options():添加编译选项到所有目标
mimalloc 内存分配器
1
2
3
find_package(mimalloc QUIET)
if(mimalloc_FOUND)
message(STATUS "mimalloc found")
  • mimalloc:微软开源的高性能内存分配器
  • 用途:替代系统默认的 malloc/free,提升内存分配速度

依赖管理

ROS Catkin 依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
find_package(catkin REQUIRED COMPONENTS
geometry_msgs
nav_msgs
sensor_msgs
roscpp
rospy
std_msgs
pcl_ros
tf
message_generation
eigen_conversions
vikit_common
vikit_ros
cv_bridge
image_transport
)
- catkin:ROS 的构建系统 - COMPONENTS:指定需要的 ROS 包 - roscpp:ROS C++ 客户端库 - sensor_msgs:传感器消息类型(点云、图像) - pcl_ros:PCL 与 ROS 的桥接 - cv_bridge:OpenCV 与 ROS 图像消息转换

第三方库

1
2
3
4
5
find_package(Eigen3 REQUIRED)
find_package(PCL REQUIRED)
find_package(OpenCV REQUIRED)
find_package(Sophus REQUIRED)
find_package(Boost REQUIRED COMPONENTS thread)
- Eigen3:线性代数库(矩阵运算) - PCL (Point Cloud Library):点云处理库 - OpenCV:计算机视觉库 - Sophus:李群/李代数库(SO(3)、SE(3) 旋转表示) - Boost:C++ 通用库,这里只需要 thread 组件

1
set(Sophus_LIBRARIES libSophus.so)
  • 手动设置库名:覆盖 find_package 的默认设置

Catkin 包配置
1
2
3
4
catkin_package(
CATKIN_DEPENDS geometry_msgs nav_msgs roscpp rospy std_msgs message_runtime cv_bridge vikit_common vikit_ros image_transport
DEPENDS EIGEN3 PCL OpenCV Sophus
)
  • catkin_package():声明当前包的依赖关系
  • CATKIN_DEPENDS:依赖的 ROS 包
  • DEPENDS:依赖的非 ROS 库
  • 作用:其他包依赖本包时,会自动链接这些库

头文件目录
1
2
3
4
5
6
7
8
include_directories(
${catkin_INCLUDE_DIRS}
${EIGEN3_INCLUDE_DIR}
${PCL_INCLUDE_DIRS}
${OpenCV_INCLUDE_DIRS}
${Sophus_INCLUDE_DIRS}
include
)
  • include_directories(目录列表):添加头文件搜索路径
  • ${变量名}:引用变量(CMake 语法)
  • 效果:编译器可以找到 #include <eigen3/Eigen/Dense>

库目标定义
1
2
3
4
5
add_library(vio src/vio.cpp src/frame.cpp src/visual_point.cpp)
add_library(lio src/voxel_map.cpp)
add_library(pre src/preprocess.cpp)
add_library(imu_proc src/IMU_Processing.cpp)
add_library(laser_mapping src/LIVMapper.cpp)
  • add_library(库名 源文件列表):创建库目标
  • 默认:生成静态库(.a 文件)
  • 可选参数
    • SHARED:动态库(.so 文件)
    • STATIC:静态库(默认)
  • 模块化:将不同功能编译为独立的库,便于管理

可执行文件定义
1
add_executable(fastlivo_mapping src/main.cpp)
  • add_executable(可执行文件名 源文件):创建可执行目标
  • 输出devel/lib/fast_livo/fastlivo_mapping

链接配置
1
2
3
4
5
6
7
8
9
10
11
12
target_link_libraries(fastlivo_mapping
laser_mapping
vio
lio
pre
imu_proc
${catkin_LIBRARIES}
${PCL_LIBRARIES}
${OpenCV_LIBRARIES}
${Sophus_LIBRARIES}
${Boost_LIBRARIES}
)
  • target_link_libraries(目标 依赖库列表):链接库
  • 链接顺序
    1. 自定义库(laser_mapping、vio 等)
    2. 第三方库(PCL、OpenCV 等)
  • 自动传递依赖:链接 laser_mapping 时,会自动链接其依赖的库

条件链接
1
2
3
if(mimalloc_FOUND)
target_link_libraries(fastlivo_mapping mimalloc)
endif()
  • 条件编译:只有在找到 mimalloc 时才链接
  • 可选依赖:不影响基础功能,但能提升性能

关键语法与关键字

变量操作

命令 语法 示例
设置变量 set(变量 值) set(MY_VAR "hello")
引用变量 ${变量} message("${MY_VAR}")
列表操作 list(APPEND 变量 值) list(APPEND SOURCES main.cpp)
字符串操作 string(TOUPPER ${变量} 输出变量) string(TOUPPER "abc" RESULT)

控制流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 条件判断
if(条件)
# ...
elseif(条件)
# ...
else()
# ...
endif()

# 循环
foreach(变量 IN LISTS 列表)
# ...
endforeach()

# while 循环
while(条件)
# ...
endwhile()

常用命令

命令 作用 示例
project() 定义项目 project(MyProject)
add_executable() 创建可执行文件 add_executable(app main.cpp)
add_library() 创建库 add_library(mylib lib.cpp)
target_link_libraries() 链接库 target_link_libraries(app mylib)
find_package() 查找第三方库 find_package(OpenCV REQUIRED)
include_directories() 添加头文件路径 include_directories(include)
add_definitions() 添加宏定义 add_definitions(-DDEBUG)

内置变量

变量 含义
CMAKE_SOURCE_DIR 顶层 CMakeLists.txt 所在目录
CMAKE_BINARY_DIR 构建目录
CMAKE_CURRENT_SOURCE_DIR 当前处理的 CMakeLists.txt 所在目录
PROJECT_NAME 项目名称
CMAKE_CXX_COMPILER C++ 编译器路径
CMAKE_SYSTEM_PROCESSOR CPU 架构