书中给了我们这样的介绍: 我们经常在实际场景中定义各种各样的坐标系,如果考虑运动的机器人(即相机),那么常见的做法是设定一个惯性坐标系(或者叫世界坐标系),可以认为它是固定不动的。这时就会有这样的疑问:相机视野中某个向量 p p p,它在相机坐标系下的坐标为 p c p_c pc,而在世界坐标系下看其坐标为 p w p_w pw,那么,这两个坐标之间是如何转换的呢?这时,需要先得到该点针对机器人坐标系的坐标值,再根据机器人位姿变换到世界坐标系中,可以通过数学手段的变换矩阵 T T T来描述它。
下面给出刚体运动的定义
我们假设某个单位正交基 ( e 1 , e 2 , e 3 ) (e_1,e_2,e_3) (e1,e2,e3)经过一次旋转变成了 ( e 1 ′ , e 2 ′ , e 3 ′ ) (e^{\prime}_1,e^{\prime}_2,e^{\prime}_3) (e1′,e2′,e3′),这时,对于同一个向量 a a a,它在两个基坐标系下的坐标分别为 [ a 1 , a 2 , a 3 ] T [a_1,a_2,a_3]^T [a1,a2,a3]T和 [ a 1 ′ , a 2 ′ , a 3 ′ ] T [a^{\prime}_1,a^{\prime}_2,a^{\prime}_3]^T [a1′,a2′,a3′]T。因为向量本身并没有发生变化,所以下列等式成立:
[ e 1 , e 2 , e 3 ] [ a 1 a 2 a 3 ] = [ e 1 ′ , e 2 ′ , e 3 ′ ] [ a 1 ′ a 2 ′ a 3 ′ ] [e_1,e_2,e_3]\begin{bmatrix}a_1\\a_2\\a_3\end{bmatrix}=[e^{\prime}_1,e^{\prime}_2,e^{\prime}_3]\begin{bmatrix}a^{\prime}_1\\a^{\prime}_2\\a^{\prime}_3\end{bmatrix} [e1,e2,e3] a1a2a3 =[e1′,e2′,e3′] a1′a2′a3′
为了让两个坐标之间的关系看起来更清晰,我们将上述等式两侧同时左乘 [ e 1 T , e 2 T , e 3 T ] [e^T_1,e^T_2,e^T_3] [e1T,e2T,e3T],此时左侧第一项将变为单位矩阵:
[ a 1 a 2 a 3 ] = [ e 1 T e 1 ′ e 1 T e 2 ′ e 1 T e 3 ′ e 2 T e 1 ′ e 2 T e 2 ′ e 2 T e 3 ′ e 3 T e 1 ′ e 3 T e 2 ′ e 3 T e 3 ′ ] [ a 1 ′ a 2 ′ a 3 ′ ] = d e f R a ′ \begin{bmatrix}a_1\\a_2\\a_3\end{bmatrix}=\begin{bmatrix}e^T_1e^{\prime}_1&e^T_1e^{\prime}_2&e^T_1e^{\prime}_3\\ e^T_2e^{\prime}_1&e^T_2e^{\prime}_2&e^T_2e^{\prime}_3 \\ e^T_3e^{\prime}_1&e^T_3e^{\prime}_2&e^T_3e^{\prime}_3 \end{bmatrix}\begin{bmatrix}a^{\prime}_1\\a^{\prime}_2\\a^{\prime}_3\end{bmatrix}\xlongequal{\rm{def}}\bm{R}a^{\prime} a1a2a3 = e1Te1′e2Te1′e3Te1′e1Te2′e2Te2′e3Te2′e1Te3′e2Te3′e3Te3′ a1′a2′a3′ defRa′
矩阵 R \bm{R} R描述了不同坐标系下同一向量的坐标变换关系。可以说,矩阵 R \bm{R} R描述了旋转本身。所以矩阵 R \bm{R} R称为旋转矩阵(Rotation Matrix)。显然,我们定义的矩阵 R \bm{R} R是由两组基之间的内积组成,实际上是各基向量夹角的余弦值,所以我们也可以称矩阵 R \bm{R} R为方向余弦矩阵(Direction Cosine Matrix)。
同时,我们可以看出,矩阵 R \bm{R} R为正交矩阵,根据正交矩阵的性质,我们可以得到下面的关系:
a ′ = R − 1 a = R T a a^{\prime}=\bm{R}^{-1}a=\bm{R}^Ta a′=R−1a=RTa很明显, R − 1 ( R T ) \bm{R}^{-1}(\bm{R}^T) R−1(RT)刻画了一个相反的旋转。
除此之外,旋转矩阵也有一些特别的性质,我们通过矩阵 R R R的行列式可以看出,它是一个行列式为1的正交矩阵。反过来说,行列式为1的正交矩阵也是一个旋转矩阵。
根据这些性质,我们可以推广到 n n n维旋转矩阵,可以将 n n n维旋转矩阵的集合定义如下:
S O ( n ) = { R ∈ R n × n ∣ R R T = I , d e t ( R ) = 1 } . \rm{SO(n)}=\{\bm{R}\in \mathbb{R}^{n\times n}|\bm{RR}^{T}=I,\rm{det}(\bm{R})=1\}. SO(n)={R∈Rn×n∣RRT=I,det(R)=1}.在这里, S O ( n ) SO(n) SO(n)表示的是特殊正交群(Special Orthogonal Group)的意思。通过上式我们可以看出,这个集合由 n n n维空间的旋转矩阵组成,所以我们可以用 S O ( 3 ) SO(3) SO(3)表示三维空间的旋转。我们之后可以通过旋转矩阵直接谈论两个坐标系之间的旋转变换,而不再通过基表述。
在欧式变换中,我们可以通过直接在旋转后的坐标后加上平移向量,这可以非常简单的表示欧式变换中的平移。我们首先考虑一个世界坐标系中的向量 a a a,经过一次旋转和一次平移后,得到了 a ′ a^{\prime} a′,通过开头的描述,我们将旋转和平移合到一起,有 a ′ = R a + t a^{\prime}=\bm{R}a+\bm{t} a′=Ra+t这里的 t \bm{t} t即为平移向量。我们可以通过上式用一个旋转矩阵 R \bm{R} R和一个平移向量 t \bm{t} t来完整的描述一个欧式空间的坐标变换。
这里我们通过一个例子,来规定一下角标。在现实中,我们会定义坐标系1和坐标系2,同时向量 a a a在两个坐标系下的坐标分别为 a 1 a_1 a1和 a 2 a_2 a2,根据上式,他们之间的关系应该这样表示: a 1 = R 12 a 2 + t 12 . a_1=\bm{R}_{12}a_2+\bm{t}_{12}. a1=R12a2+t12.这里的 R 12 \bm{R}_{12} R12是指“把坐标系2的向量变换到坐标系1”中,即为“从2到1的旋转矩阵”。由于向量乘在矩阵的右边(右乘),所以它的下标是从右往左读的。而对于平移向量 t 12 \bm{t}_{12} t12,它实际对应的是坐标系1原点指向坐标系2原点的向量,即在坐标系1下取的坐标,这里建议把它记作“从1到2的向量”,它的下标是从左读到右的,由于两坐标系旋转的关系,它并不等于 − t 12 -\bm{t}_{12} −t12。
根据上一节的介绍,通过式子 a ′ = R a + t a^{\prime}=\bm{R}a+\bm{t} a′=Ra+t可以完整的表达欧式空间的旋转与平移。下面假设我们进行两次变换: R 1 , t 1 \bm{R}_1,\bm{t}_1 R1,t1和 R 2 , t 2 \bm{R}_2,\bm{t}_2 R2,t2: b = R 1 a + t 1 , c = R 2 b + t 2 b=\bm{R}_1a+\bm{t}_1,c=\bm{R}_2b+\bm{t}_2 b=R1a+t1,c=R2b+t2 c = R 2 ( R 1 a + t 1 ) + t 2 c=\bm{R}_2(\bm{R}_1a+\bm{t}_1)+\bm{t}_2 c=R2(R1a+t1)+t2这里我们可以明显看出,连续两次变换的关系并不是一个线性关系,这也是欧式变换的一个问题,这样的形式在变换多次后会变得非常啰嗦和复杂。
在这里,我们引入齐次坐标和变换矩阵的概念,对式 a ′ = R a + t a^{\prime}=\bm{R}a+\bm{t} a′=Ra+t进行重新表示
Eigen是一个C++开源线性代数库,它提供了快速的有关矩阵的线性代数运算,还包括解方程等功能。许多上层的软件库也使用Eigen进行矩阵运算,包括g2o、Sophus等。Eigen库的优势在于,它是一个纯用头文件搭建起来的库,这意味着你只能找到它的头文件,而没有类似.so
或.a
的二进制文件。在使用时,只需引入头文件即可,不需要链接库文件。
通过Eigen官网教程,我们可以学习更多关于Eigen库的知识。
Eigen库需要我们自己进行安装才可以使用,我们可以通过在终端命令行输入以下指令来安装Eigen库。
sudo apt-get install libeigen3-dev
示例代码地址:https://github.com/gaoxiang12/slambook2
我们可以通过以下指令,克隆SLAM十四讲的示例代码到个人电脑上
git clone https://github.com/gaoxiang12/slambook2.git
然后在终端输入下面的命令,打开对应的实例程序即可
cd slambook2/ch3/useEigen/
code .
下面是操作演示
这里给出使用Eigen库表示矩阵和向量并进行基本运算和调用的示例代码
#include
using namespace std;
#include
// Eigen 核心部分
#include
// 稠密矩阵的代数运算(逆,特征值等)
#include
using namespace Eigen;
#define MATRIX_SIZE 50
/****************************
* 本程序演示了 Eigen 基本类型的使用
****************************/
int main(int argc, char **argv)
{
// Eigen 中所有向量和矩阵都是Eigen::Matrix,它是一个模板类。它的前三个参数为:数据类型,行,列
// 声明一个2*3的float矩阵
Matrix<float, 2, 3> matrix_23;
// 同时,Eigen 通过 typedef 提供了许多内置类型,不过底层仍是Eigen::Matrix
// 例如 Vector3d 实质上是 Eigen::Matrix,即三维向量
Vector3d v_3d;
// 这是一样的
Matrix<float, 3, 1> vd_3d;
// Matrix3d 实质上是 Eigen::Matrix
Matrix3d matrix_33 = Matrix3d::Zero(); // 初始化为零
// 如果不确定矩阵大小,可以使用动态大小的矩阵
Matrix<double, Dynamic, Dynamic> matrix_dynamic;
// 更简单的
MatrixXd matrix_x;
// 这种类型还有很多,我们不一一列举
// 下面是对Eigen阵的操作
// 输入数据(初始化)
matrix_23 << 1, 2, 3, 4, 5, 6;
// 输出
cout << "matrix 2x3 from 1 to 6: \n"
<< matrix_23 << endl;
// 用()访问矩阵中的元素
cout << "print matrix 2x3: " << 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里你不能混合两种不同类型的矩阵,像这样是错的
// Matrix result_wrong_type = matrix_23 * v_3d;
// 应该显式转换
Matrix<double, 2, 1> result = matrix_23.cast<double>() * v_3d;
cout << "[1,2,3;4,5,6]*[3,2,1]=" << result.transpose() << endl;
Matrix<float, 2, 1> result2 = matrix_23 * vd_3d;
cout << "[1,2,3;4,5,6]*[4,5,6]: " << result2.transpose() << endl;
// 同样你不能搞错矩阵的维度
// 试着取消下面的注释,看看Eigen会报什么错
// Eigen::Matrix result_wrong_dimension = matrix_23.cast() * v_3d;
// 一些矩阵运算
// 四则运算就不演示了,直接用+-*/即可。
matrix_33 = Matrix3d::Random(); // 随机数矩阵
cout << "random matrix: \n"
<< matrix_33 << endl;
cout << "transpose: \n"
<< matrix_33.transpose() << endl; // 转置
cout << "sum: " << matrix_33.sum() << endl; // 各元素和
cout << "trace: " << matrix_33.trace() << endl; // 迹
cout << "times 10: \n"
<< 10 * matrix_33 << endl; // 数乘
cout << "inverse: \n"
<< matrix_33.inverse() << endl; // 逆
cout << "det: " << matrix_33.determinant() << endl; // 行列式
// 特征值
// 实对称矩阵可以保证对角化成功
SelfAdjointEigenSolver<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的大小在前边的宏里定义,它由随机数生成
// 直接求逆自然是最直接的,但是求逆运算量大
Matrix<double, MATRIX_SIZE, MATRIX_SIZE> matrix_NN = MatrixXd::Random(MATRIX_SIZE, MATRIX_SIZE);
matrix_NN = matrix_NN * matrix_NN.transpose(); // 保证半正定
Matrix<double, MATRIX_SIZE, 1> v_Nd = MatrixXd::Random(MATRIX_SIZE, 1);
clock_t time_stt = clock(); // 计时
// 直接求逆
Matrix<double, MATRIX_SIZE, 1> x = matrix_NN.inverse() * v_Nd;
cout << "time of normal inverse is "
<< 1000 * (clock() - time_stt) / (double)CLOCKS_PER_SEC << "ms" << endl;
cout << "x = " << x.transpose() << endl;
// 通常用矩阵分解来求,例如 QR 分解,速度会快很多
time_stt = clock();
x = matrix_NN.colPivHouseholderQr().solve(v_Nd);
cout << "time of Qr decomposition is "
<< 1000 * (clock() - time_stt) / (double)CLOCKS_PER_SEC << "ms" << endl;
cout << "x = " << x.transpose() << endl;
// 对于正定矩阵,还可以用cholesky分解来解方程
time_stt = clock();
x = matrix_NN.ldlt().solve(v_Nd);
cout << "time of ldlt decomposition is "
<< 1000 * (clock() - time_stt) / (double)CLOCKS_PER_SEC << "ms" << endl;
cout << "x = " << x.transpose() << endl;
return 0;
}
CMakelists配置文件
cmake_minimum_required(VERSION 2.8) # 最低版本声明
project(useEigen) # 项目名称
set(CMAKE_BUILD_TYPE "Release")
# CMake预定义的内建变量,且他们是全局的。该变量可用于设置编译选项。直接使用set修改其值即可。
set(CMAKE_CXX_FLAGS "-O3") # '-O3'是一个优化选项,告诉编译器优化我们的代码。
# 添加Eigen头文件
# 方式一
# include_directories("/usr/include/eigen3")
# 方式二
find_package(Eigen3 REQUIRED)
include_directories( ${EIGEN3_INCLUDE_DIRS})
# 声明一个 C++ 可执行文件
add_executable(eigenMatrix eigenMatrix.cpp)
通过下面的指令进行程序的编译和执行
mkdir build
cd build
cmake ..
make
./eigenMatrix
matrix 2x3 from 1 to 6:
1 2 3
4 5 6
print matrix 2x3:
1 2 3
4 5 6
[1,2,3;4,5,6]*[3,2,1]=10 28
[1,2,3;4,5,6]*[4,5,6]: 32 77
random matrix:
0.680375 0.59688 -0.329554
-0.211234 0.823295 0.536459
0.566198 -0.604897 -0.444451
transpose:
0.680375 -0.211234 0.566198
0.59688 0.823295 -0.604897
-0.329554 0.536459 -0.444451
sum: 1.61307
trace: 1.05922
times 10:
6.80375 5.9688 -3.29554
-2.11234 8.23295 5.36459
5.66198 -6.04897 -4.44451
inverse:
-0.198521 2.22739 2.8357
1.00605 -0.555135 -1.41603
-1.62213 3.59308 3.28973
det: 0.208598
Eigen values =
0.0242899
0.992154
1.80558
Eigen vectors =
-0.549013 -0.735943 0.396198
0.253452 -0.598296 -0.760134
-0.796459 0.316906 -0.514998
time of normal inverse is 0.262ms
x = -55.7896 -298.793 130.113 -388.455 -159.312 160.654 -40.0416 -193.561 155.844 181.144 185.125 -62.7786 19.8333 -30.8772 -200.746 55.8385 -206.604 26.3559 -14.6789 122.719 -221.449 26.233 -318.95 -78.6931 50.1446 87.1986 -194.922 132.319 -171.78 -4.19736 11.876 -171.779 48.3047 84.1812 -104.958 -47.2103 -57.4502 -48.9477 -19.4237 28.9419 111.421 92.1237 -288.248 -23.3478 -275.22 -292.062 -92.698 5.96847 -93.6244 109.734
time of Qr decomposition is 0.05ms
x = -55.7896 -298.793 130.113 -388.455 -159.312 160.654 -40.0416 -193.561 155.844 181.144 185.125 -62.7786 19.8333 -30.8772 -200.746 55.8385 -206.604 26.3559 -14.6789 122.719 -221.449 26.233 -318.95 -78.6931 50.1446 87.1986 -194.922 132.319 -171.78 -4.19736 11.876 -171.779 48.3047 84.1812 -104.958 -47.2103 -57.4502 -48.9477 -19.4237 28.9419 111.421 92.1237 -288.248 -23.3478 -275.22 -292.062 -92.698 5.96847 -93.6244 109.734
time of ldlt decomposition is 0.02ms
x = -55.7896 -298.793 130.113 -388.455 -159.312 160.654 -40.0416 -193.561 155.844 181.144 185.125 -62.7786 19.8333 -30.8772 -200.746 55.8385 -206.604 26.3559 -14.6789 122.719 -221.449 26.233 -318.95 -78.6931 50.1446 87.1986 -194.922 132.319 -171.78 -4.19736 11.876 -171.779 48.3047 84.1812 -104.958 -47.2103 -57.4502 -48.9477 -19.4237 28.9419 111.421 92.1237 -288.248 -23.3478 -275.22 -292.062 -92.698 5.96847 -93.6244 109.734
参考资料:
https://en.wikipedia.org/wiki/Cross_product
视觉SLAM十四讲:从理论到实践(第2版)(ISBN:9787121369421)
现代机器人学:机构、规划与控制(ISBN:9787111639848)
关于齐次坐标的理解(经典)
为什么要引入齐次坐标,齐次坐标的意义(一)
为什么要引入齐次坐标,齐次坐标的意义(二)