什么是SLAM:
SLAM 是 Simultaneous Localization and Mapping 的缩写,中文译作“同时定位与地图构建”。它是指搭载特定传感器的主体,在没有环境先验信息的情况下,于运动过程中建立环境的模型,同时估计自己的运动。
三种视觉SLAM中主要的相机:
运动方程:
通常,机器人会携带一个测量自身运动的传感器,比如说码盘或惯性传感器。这个传感器可以测量有关运动的读数,但不一定直接是位置之差,还可能是加速度、角速度等信息
其中Xk是当前位置,Xk-1是上一时刻位置,uk是输入,wk是噪声
运动方程只有在带有运动传感器的时候才存在
观测方程:
观测方程描述的是,当小萝卜在 xk 位置上看到某个路标点 yj,产生了一个观测数据 zk,j
观测方程的数量是不一定的,取决于xk那个时刻看到了多少个路标y
常用框架如下
目录结构如下:
├── test目录
│ ├── CMakeLists.txt
│ ├── bin目录(存放生成的可执行文件)
│ ├── include目录
│ │ ├── file1.h
│ ├── src 目录
│ │ ├── main.cpp
│ │ ├── file1.cpp
│ ├── build目录
# 声明要求的 cmake 最低版本
#这行命令是可选的,我们可以不写这句话,但在有些情况下,如果 CMakeLists.txt 文件中使用了一些高版本 cmake 特有的一些命令的时候,就需要加上这样一行,提醒用户升级到该版本之后再执行 cmake。
cmake_minimum_required( VERSION 2.8 )
# 声明一个 cmake 工程名称
project( 01 )
# 设置编译模式,可以选择Debug或者Release
set( CMAKE_BUILD_TYPE "Debug" )
set(CMAKE_CXX_FLAGS_DEBUG "$ENV{CXXFLAGS} -O0 -Wall -g2 -ggdb")
set(CMAKE_CXX_FLAGS_RELEASE "$ENV{CXXFLAGS} -O3 -Wall")
# 添加一个可执行程序
# 语法:add_executable( 程序名 源代码文件 )
add_executable( main src/main.cpp )
#将可执行文件输出到文件夹bin中,方便管理
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
# 包含头文件所在目录
include_directories(${PROJECT_SOURCE_DIR}/include)
# 添加一个库,默认为静态链接库
# add_library( namesp src/namesp.cpp )
# 添加一个动态共享库
add_library( file1_shared SHARED src/file1.cpp )
# 将库文件链接到可执行程序上
target_link_libraries( main file1_shared )
在写.h文件的时候,可以使用 编译预处理指令#pragma once
代替ifndef #define
,他们的效果是一样的,但是更简洁
使用#progma once
#pragma once
// Code placed here is included only once per translation unit
使用宏定义方式
#ifndef HEADER_H_
#define HEADER_H_
// Code placed here is included only once per translation unit
#endif // HEADER_H_
使用矩阵方式(旋转矩阵和变换矩阵)表示三维刚体运动的缺陷:
旋转向量:任意旋转都可以用一个旋转轴和一个旋转角来刻画。于是,我们可以使用一个向量,其方向与旋转轴一致,而长度等于旋转角。这种向量,称为旋转向量。
罗德里格斯公式:
罗德里格斯公式实现了从旋转向量到旋转矩阵的转换
旋转矩阵用九个量描述三自由度的旋转,具有冗余性;欧拉角和旋转向量是紧凑的,但具有奇异性。
引入一种和数学中复数类似的数,
数学中的复数:乘上复数 i 相当于逆时针把一个复向量旋转 90 度。
四元数正是在表达三维空间旋转时的一种类似的代数
优点:它既是紧凑的,也没有奇异性。
缺点:四元数不够直观,运算较为复杂
定义:
sudo apt-get install libeigen3-dev
include_directories( "/usr/include/eigen3" )
// Eigen 部分
#include
// 稠密矩阵的代数运算(逆,特征值等)
#include
#include
using namespace std;
#include //包含计时的函数,在本程序中用来对比两种方程求解方法的运算速度
// Eigen 部分
#include
// 稠密矩阵的代数运算(逆,特征值等)
#include
#define MATRIX_SIZE 50
/****************************
* 本程序演示了 Eigen 基本类型的使用
****************************/
int main( int argc, char** argv )
{
// Eigen 中所有向量和矩阵都是Eigen::Matrix,它是一个模板类。它的前三个参数为:数据类型,行,列
// 声明一个2*3的float矩阵
Eigen::Matrix<float, 2, 3> matrix_23;
// 同时,Eigen 通过 typedef 提供了许多内置类型,不过底层仍是Eigen::Matrix
// 例如 Vector3d 实质上是 Eigen::Matrix,即三维向量
Eigen::Vector3d v_3d;
// 这是一样的
Eigen::Matrix<float,3,1> vd_3d;
// Matrix3d 实质上是 Eigen::Matrix
Eigen::Matrix3d matrix_33 = Eigen::Matrix3d::Zero(); //初始化为零
// 如果不确定矩阵大小,可以使用动态大小的矩阵
Eigen::Matrix< double, Eigen::Dynamic, Eigen::Dynamic > matrix_dynamic;
// 更简单的
Eigen::MatrixXd matrix_x;
// 这种类型还有很多,我们不一一列举
// 下面是对Eigen阵的操作
// 输入数据(初始化)
matrix_23 << 1, 2, 3, 4, 5, 6;
// 输出
cout << matrix_23 << endl;
// 用()访问矩阵中的元素
for (int i=0; i<2; i++) {
for (int j=0; j<3; j++)
cout<<matrix_23(i,j)<<"\t";
cout<<endl;
}
// 矩阵和向量相乘(实际上仍是矩阵和矩阵)
v_3d << 3, 2, 1;
vd_3d << 4,5,6;
// 但是在Eigen里你不能混合两种不同类型的矩阵,像这样是错的
// Eigen::Matrix result_wrong_type = matrix_23 * v_3d;
// 应该显式转换
Eigen::Matrix<double, 2, 1> result = matrix_23.cast<double>() * v_3d;
cout << result << endl;
Eigen::Matrix<float, 2, 1> result2 = matrix_23 * vd_3d;
cout << result2 << endl;
// 同样你不能搞错矩阵的维度
// 试着取消下面的注释,看看Eigen会报什么错
// Eigen::Matrix result_wrong_dimension = matrix_23.cast() * v_3d;
// 一些矩阵运算
// 四则运算就不演示了,直接用+-*/即可。
matrix_33 = Eigen::Matrix3d::Random(); // 随机数矩阵
cout << matrix_33 << endl << endl;
cout << matrix_33.transpose() << endl; // 转置
cout << matrix_33.sum() << endl; // 各元素和
cout << matrix_33.trace() << endl; // 迹
cout << 10*matrix_33 << endl; // 数乘
cout << matrix_33.inverse() << endl; // 逆
cout << matrix_33.determinant() << endl; // 行列式
// 特征值
// 实对称矩阵可以保证对角化成功
Eigen::SelfAdjointEigenSolver<Eigen::Matrix3d> eigen_solver ( matrix_33.transpose()*matrix_33 );
cout << "Eigen values = \n" << eigen_solver.eigenvalues() << endl;
cout << "Eigen vectors = \n" << eigen_solver.eigenvectors() << endl;
// 解方程
// 我们求解 matrix_NN * x = v_Nd 这个方程
// N的大小在前边的宏里定义,它由随机数生成
// 直接求逆自然是最直接的,但是求逆运算量大
Eigen::Matrix< double, MATRIX_SIZE, MATRIX_SIZE > matrix_NN;
matrix_NN = Eigen::MatrixXd::Random( MATRIX_SIZE, MATRIX_SIZE );
Eigen::Matrix< double, MATRIX_SIZE, 1> v_Nd;
v_Nd = Eigen::MatrixXd::Random( MATRIX_SIZE,1 );
clock_t time_stt = clock(); // 计时
// 直接求逆
Eigen::Matrix<double,MATRIX_SIZE,1> x = matrix_NN.inverse()*v_Nd;
cout <<"time use in normal inverse is " << 1000* (clock() - time_stt)/(double)CLOCKS_PER_SEC << "ms"<< endl;
// 通常用矩阵分解来求,例如QR分解,速度会快很多
time_stt = clock();
x = matrix_NN.colPivHouseholderQr().solve(v_Nd);
cout <<"time use in Qr decomposition is " <<1000* (clock() - time_stt)/(double)CLOCKS_PER_SEC <<"ms" << endl;
return 0;
}
群的定义
群(Group)是一种集合加上一种运算的代数结构。我们把集合记作 A,运算记作 ·,那么群可以记作 G = (A, ·)。群要求这个运算满足以下几个条件:
1.封闭性: ∀a1, a2 ∈ A, a1 · a2 ∈ A.
2.结合律: ∀a1, a2, a3 ∈ A, (a1 · a2) · a3 = a1 · (a2 · a3).
3.幺元: ∃a0 ∈ A, s.t. ∀a ∈ A, a0 · a = a · a0 = a.
4.逆: ∀a ∈ A, ∃a−1 ∈ A, s.t. a · a−1 = a0.
群的例子:
实数关于加法是一个群,封闭性和结合律不必说,这个群的幺元是0,逆就是数a的相反数。
常见的群
一般线性群 GL(n) 指 n × n 的可逆矩阵,它们对矩阵乘法成群。
特殊正交群 SO(n) 也就是所谓的旋转矩阵群,其中 SO(2) 和SO(3) 最为常见。
特殊欧氏群 SE(n) 也就是前面提到的 n 维欧氏变换,如 SE(2) 和 SE(3)。
李代数
每个李群都有与之对应的李代数。李代数描述了李群的局部性质。通用的李代数的定义如下:
李代数由一个集合 V,一个数域 F 和一个二元运算 [, ] 组成。如果它们满足以下几条性质,称 (V, F, [ , ]) 为一个李代数,记作 g。
1.封闭性: ∀X,Y ∈ V, [X,Y ] ∈ V.
2.双线性: ∀X,Y , Z ∈ V, a, b ∈ F, 有:[aX + bY , Z] = a[X, Z] + b[Y , Z], [Z, aX + bY ] = a[Z, X] + b[Z,Y ].
3.自反性: ∀X ∈ V, [X, X] = 0.
4.雅可比等价: ∀X,Y , Z ∈ V, [X, [Y , Z]] + [Z, [Y , X]] + [Y , [Z, X]] = 0.
其中的二元运算被称为李括号。李括号表达了两个元素的差异。
李代数的例子:三维向量 R3 上定义的叉积 × 是一种李括号,因此 g = (R3, R, ×) 构成了一个李代数。
从SE(3)到se(3)的指数映射
ξ=ln(T) ∨
根据变换矩阵 T 求 so(3) 上的对应向量也有更省事的方式:从左上的 R 计算旋转向量,而右上的 t 满足:t = Jρ
由于 J 可以由 ϕ 得到,所以这里的 ρ 亦可由此线性方程解得。
BCH公式
注意,对于矩阵,以下这个式子并不成立
两个李代数指数映射乘积的完整形式,由 Baker-Campbell-Hausdorff 公式(BCH 公式)给出。由于它完整的形式较复杂,我们给出它展开式的前几项
其中 [,] 为李括号。
SO(3)上的近似形式
当ϕ1为小量时称为左乘,ϕ2为小量时称为右乘
左乘 BCH 近似雅可比 Jl:
它的逆为
右乘雅可比Jr仅需要对自变量取负号即可
假定对某个旋转 R,对应的李代数为 ϕ。我们给它左乘一个微小旋转,记作 ∆R,对应的李代数为 ∆ϕ。那么,在李群上,得到的结果就是 ∆R · R,而在李代数上,根据 BCH近似,为:Jl-1(ϕ)∆ϕ + ϕ。合并起来,可以简单地写成:
反之在李代数上进行加法:
有两种模型,导数模型和扰动模型
假设我们对一个空间点 p 进行了旋转,得到了 Rp
O − x − y − z 为相机坐标系;实际物体点P在相机坐标系中的坐标为[X,Y,Z]T,投影到O’-X’-Y’平面上的点P’坐标为[X’,Y’,Z’]T,则有
相机坐标系到像素坐标系
设像素坐标为[u,v]T
进一步地
最终得到
其中K称为内参数矩阵
内参:通常认为,相机的内参在出厂之后是固定的,不会在使用过程中发生变化,我们把它看做相机的固有属性,所以称为内参。
将相机坐标P转换为世界坐标PW
其中T称为外参数矩阵
注意:上式隐含了一次从齐次坐标到非齐次坐标的转换(KTPW是一个41的,而等号左边是31的)
内参矩阵K表示了从归一化的相机坐标到像素坐标的变换
外参矩阵T(或R,t)表示了从世界坐标到相机坐标的变换
畸变通常包含两种:径向畸变和切向畸变
目前的 RGB-D 相机按原理可分为两大类:
本讲当中还有关于图像的基本知识和OpenCV的相关知识,可以参见笔者另一篇文章OpenCV相关知识.