工作进度 #
- 学弟的医学图像处理系统能在学长笔记本电脑运行
- 配置服务器防火墙
- Openssh漏洞]
- Bundle Adjustment 笔记
- Local Bundle Adjustment 笔记
- Pose Optimization 笔记
本周要做的 #
- 备份学长电脑
- 现在因为系统版本问题,只能在docker container中离线运行学弟程序,无法接采集卡现场演示
- 必须装新版本系统,才能正常用学弟要用的python库
- 继续阅读代码
ORBSLAM源码阅读
lines file
18911 total
[ ] 2328 ./Tracking.cc
[x] 2105 ./ORBmatcher.cc
[ ] 1746 ./Initializer.cc
[x] 1726 ./ORBextractor.cc
[x] 1621 ./Optimizer.cc
[ ] 1588 ./PnPsolver.cc
[ ] 1164 ./Frame.cc
[ ] 1142 ./LoopClosing.cc
[ ] 1032 ./LocalMapping.cc
[ ] 901 ./KeyFrame.cc
[x] 648 ./System.cc
[x] 623 ./MapPoint.cc
[ ] 571 ./Sim3Solver.cc
[ ] 414 ./KeyFrameDatabase.cc
[x] 326 ./MapDrawer.cc
[x] 306 ./Viewer.cc
[x] 273 ./FrameDrawer.cc
[x] 214 ./Converter.cc
[x] 183 ./Map.cc
解决遇到的问题
ORB-SLAM2中Optimizer::BundleAdjustment()函数代码阅读总结 #
该函数主要是在Local Mapping线程中对当前关键帧及其共视关键帧进行局部BA优化。优化的目标是最小化重投影误差和IMU测量误差(如果有的话)。
ORBslam中有个文件定义了VertexSE3Expmap,EdgeSE3ProjectXYZ,EdgeSE3ProjectXYZOnlyPose,乍一看还需要自行扩展定义点与边动手翻译这些需要大量数学知识的公式,其实g2o都翻译好了,作者只是把这几个库复制粘贴到同一个文件中。
为什么g2o需要用到指数空间优化 #
假设我们有两个旋转矩阵$\mathbf{R}_1$和$\mathbf{R}_2$: $$ \mathbf{R}_1 = \begin{bmatrix} \cos\theta_1 & -\sin\theta_1 & 0 \\ \sin\theta_1 & \cos\theta_1 & 0 \\ 0 & 0 & 1 \end{bmatrix}, \quad \mathbf{R}_2 = \begin{bmatrix} \cos\theta_2 & -\sin\theta_2 & 0 \\ \sin\theta_2 & \cos\theta_2 & 0 \\ 0 & 0 & 1 \end{bmatrix} $$ 它们都是绕$z$轴旋转的旋转矩阵,分别旋转了$\theta_1$和$\theta_2$角度。单独看$\mathbf{R}_1$和$\mathbf{R}_2$,它们都满足正交性和行列式为1的约束。 单位正交为什么行列式为1?(从几何意义来看,因为在这些单位向量张成的空间中,由这些单位正交向量包围成的"体积"是1,所以行列式为1。从行列式代表这个矩阵的伸缩因子角度解释,旋转矩阵不改变原图形的体积,所以行列式为1。
现在我们将它们相加得到一个新矩阵$\mathbf{R}_s$: $$ \mathbf{R}_s = \mathbf{R}_1 + \mathbf{R}_2 = \begin{bmatrix} \cos\theta_1+\cos\theta_2 & -\sin\theta_1-\sin\theta_2 & 0 \\ \sin\theta_1+\sin\theta_2 & \cos\theta_1+\cos\theta_2 & 0 \\ 0 & 0 & 2 \end{bmatrix} $$ 检查$\mathbf{R}_s$的性质:
行向量的模长: $$ |\mathbf{r}_1| = \sqrt{(\cos\theta_1+\cos\theta_2)^2 + (\sin\theta_1+\sin\theta_2)^2} \ne 1 $$ 列向量的模长: $$ |\mathbf{c}_1| = \sqrt{(\cos\theta_1+\cos\theta_2)^2 + (\sin\theta_1+\sin\theta_2)^2} \ne 1 $$ 行列式的值: $$ \det(\mathbf{R}_s) = 2(\cos\theta_1+\cos\theta_2)^2 \ne 1 $$ 总之,$\mathbf{R}_s$不满足旋转矩阵的约束,不能看做是一个新的旋转矩阵。
这就是为什么在优化过程中,不能直接将旋转矩阵$\mathbf{R}$作为优化变量,因为优化过程中矩阵元素的更新无法保证更新后的矩阵仍然是一个合法的旋转矩阵。
在图优化中,相机位姿通常使用李群$SE3$表示,即旋转+平移。但由于旋转矩阵有正交约束,无法直接进行优化。解决方法是$将SE3$映射到其对应的李代数$se3$,在$se3$上进行优化更新,再将更新量乘到原来的$SE3$上。 g2o的做法则是将$SE3$通过旋转的四元数表示,即将旋转矩阵换成四元数,平移照旧。这样旋转就可以直接优化了,只是优化完之后要重新归一化。
主要步骤 #

将当前关键帧及其共视关键帧的pose作为顶点(vertex)加入优化器。 #
- 使用g2o::VertexSE3Expmap表示位姿顶点,内部使用李代数表示SE3。
- 调用Converter::toSE3Quat将马氏距矩阵转换为四元数+平移向量的格式。
- 设置顶点的id,以及是否固定(fix)该顶点。
添加MapPoint作为顶点 #
除了关键帧,LocalBA还会把MapPoint也作为顶点加入到优化中。步骤如下:
- 对于每个MapPoint,构造一个g2o::VertexSBAPointXYZ表示3D点的顶点
- 使用pMP->GetWorldPos()获取3D点坐标,并转换成g2o::Vector3d类型
- 顶点的id为对应的MapPoint id + maxKFid + 1
- 将该顶点设置为边缘化的,因为MapPoint不是主要优化对象,优化中会被边缘化掉以保持稀疏性
- 将顶点加入优化器
这一步相当于告诉优化器除了关键帧位姿,3D点的坐标也是需要优化的。
添加重投影误差边 #
LocalBA最主要的误差项是MapPoint在关键帧中的重投影误差。对于每一个MapPoint:
- 遍历所有能观测到该点的关键帧(由pMP->GetObservations()得到)
- 对于每个关键帧,构造一个重投影误差边(单目为g2o::EdgeSE3ProjectXYZ,双目为g2o::EdgeStereoSE3ProjectXYZ)
- 边连接MapPoint顶点和关键帧顶点
- 边的观测值为MapPoint在该关键帧中的特征点坐标(单目为x,y;双目为x,y,disparity)
- 设置边的信息矩阵,即各维度上的误差权重
- 如果使用Huber核函数,则设置对应的鲁棒核
- 设置相机内参
- 将边加入优化器
这些重投影误差边的作用就是,优化后的3D点和关键帧位姿,尽可能的使重投影误差最小。
优化与更新 #
调用optimizer.initializeOptimization()对Problem进行初始化,再调用optimizer.optimize(nIterations)启动迭代优化过程,优化完成后再调用optimizer.vertex(id)提取出优化后的结果,并赋值给对应的关键帧位姿和3D点坐标。
至此,LocalBA的核心流程就是:
- 添加关键帧位姿顶点
- 添加3D点顶点
- 添加重投影误差边
- 调用优化器迭代优化
- 从优化结果中提取并更新位姿和3D点
g2o相关概念 #
-
g2o::VertexSE3Expmap
- 表示SE3位姿的顶点,内部使用李代数(即se3)表示。
- 优化过程中,顶点的更新量是在se3上进行的,然后左乘到当前估计值上得到新的位姿。
-
g2o::EdgeSE3ProjectXYZ
- 表示单目相机重投影误差的边,即将MapPoint通过相机投影后与实际观测点的像素坐标误差。
- 误差定义为:$e = obs - K*exp(xi^{wedge})*Xc$,其中obs为观测像素坐标,K为相机内参,xi为相机位姿的se3更新量,Xc为MapPoint在相机坐标系下的坐标。
-
g2o::EdgeStereoSE3ProjectXYZ
- 表示双目相机重投影误差的边,与单目类似,只是投影函数略有不同。
- 误差定义为:$e = obs - pi(K*exp(xi^{wedge})*Xc)$,其中pi为双目相机投影函数。
可以看出,ORB-SLAM2只定义了待优化的量(即顶点),而误差的构建则是直接使用了g2o库中的误差边,免去了自己推导、编码误差计算过程的繁琐工作。
Local Bundle Adjustment #
Local Bundle Adjustment (LBA) 是ORB-SLAM2中用于优化局部地图的关键步骤。它通过优化当前关键帧及其共视关键帧的位姿以及它们观测到的地图点来最小化重投影误差,从而提高局部地图的精度。
主要步骤 #
1. 将当前关键帧及其共视关键帧的pose作为顶点(vertex)加入优化器 #
- 遍历当前关键帧及其共视关键帧
- 使用
g2o::VertexSE3Expmap表示位姿顶点,内部使用李代数表示SE3 - 调用
Converter::toSE3Quat将马氏距矩阵转换为四元数+平移向量的格式 - 设置顶点的id,以及是否固定(fix)该顶点
-
初始关键帧
在 Local BA 中,初始关键帧 (第一个关键帧) 的位姿是固定的,不会被优化。因为整个 SLAM 系统需要一个世界坐标系的基准,通常选择初始关键帧的相机坐标系作为世界坐标系。
// Set Local KeyFrame vertices for(list<KeyFrame*>::iterator lit=lLocalKeyFrames.begin(), lend=lLocalKeyFrames.end(); lit!=lend; lit++) { KeyFrame* pKFi = *lit; g2o::VertexSE3Expmap * vSE3 = new g2o::VertexSE3Expmap(); ... vSE3->setFixed(pKFi->mnId==0); // 初始关键帧 id 为 0,需要固定 optimizer.addVertex(vSE3); ... } -
固定关键帧 (Fixed Keyframes) 除了参考关键帧,Local BA 还会固定一些关键帧不优化,它们被称为 Fixed Keyframes。Fixed Keyframes 是指能观测到 local map points,但不属于 local keyframes 的那些关键帧。它们的位姿在 Local BA 过程中是固定的,不会被优化。
// Set Fixed KeyFrame vertices for(list<KeyFrame*>::iterator lit=lFixedCameras.begin(), lend=lFixedCameras.end(); lit!=lend; lit++) { KeyFrame* pKFi = *lit; g2o::VertexSE3Expmap * vSE3 = new g2o::VertexSE3Expmap(); ... vSE3->setFixed(true); // 设置为固定,不优化 optimizer.addVertex(vSE3); ... }
2. 添加MapPoint作为顶点 #
除了共视关键帧,LocalBA还会把MapPoint也作为顶点加入到优化中。步骤如下:
- 对于每个MapPoint,构造一个
g2o::VertexSBAPointXYZ表示3D点的顶点 - 使用
pMP->GetWorldPos()获取3D点坐标,并转换成g2o::Vector3d类型 - 顶点的id为对应的MapPoint id + maxKFid + 1 4. 将该顶点设置为边缘化的,因为MapPoint不是主要优化对象,优化中会被边缘化掉以保持稀疏性
- 将顶点加入优化器
这一步相当于告诉优化器除了关键帧位姿,3D点的坐标也是需要优化的。
3. 添加边(edge) #
接下来需要构建误差函数,即边。对于每一个地图点:
- 获取该地图点的观测关键帧及其索引
- 对每一个观测关键帧:
- 如果是单目:
- 构建
g2o::EdgeSE3ProjectXYZ边 - 设置该边连接的地图点顶点和关键帧顶点
- 设置测量值为归一化平面坐标
- 设置信息矩阵,即协方差的逆
- 设置相机内参
- 添加鲁棒核函数
- 构建
- 如果是双目:
- 构建
g2o::EdgeStereoSE3ProjectXYZ边 - 设置该边连接的地图点顶点和关键帧顶点
- 设置测量值为归一化平面坐标(左目)和视差
- 设置信息矩阵,即协方差的逆
- 设置相机内参
- 添加鲁棒核函数
- 构建
- 如果是单目:
- 将构建好的边加入优化器
4. 启动优化 #
// 启动优化
optimizer.initializeOptimization();
optimizer.optimize(5);
// 剔除外点后再次优化
optimizer.initializeOptimization(0);
optimizer.optimize(10);
这里先进行5次优化,然后剔除外点后再进行10次优化,提高精度。
5. 剔除外点 #
for(size_t i=0, iend=vpEdgesMono.size(); i<iend;i++)
{
g2o::EdgeSE3ProjectXYZ* e = vpEdgesMono[i];
MapPoint* pMP = vpMapPointEdgeMono[i];
if(pMP->isBad())
continue;
if(e->chi2()>5.991 || !e->isDepthPositive())
{
KeyFrame* pKFi = vpEdgeKFMono[i];
vToErase.push_back(make_pair(pKFi,pMP));
}
}
遍历每一条边,根据其chi2误差和深度值来判断是否为外点。如果是外点,就从关键帧中删除该地图点的观测。
6. 优化结果写回 #
//Keyframes
for(list<KeyFrame*>::iterator lit=lLocalKeyFrames.begin(), lend=lLocalKeyFrames.end(); lit!=lend; lit++)
{
KeyFrame* pKF = *lit;
g2o::VertexSE3Expmap* vSE3 = static_cast<g2o::VertexSE3Expmap*>(optimizer.vertex(pKF->mnId));
g2o::SE3Quat SE3quat = vSE3->estimate();
pKF->SetPose(Converter::toCvMat(SE3quat));
}
//Points
for(list<MapPoint*>::iterator lit=lLocalMapPoints.begin(), lend=lLocalMapPoints.end(); lit!=lend; lit++)
{
MapPoint* pMP = *lit;
g2o::VertexSBAPointXYZ* vPoint = static_cast<g2o::VertexSBAPointXYZ*>(optimizer.vertex(pMP->mnId+maxKFid+1));
pMP->SetWorldPos(Converter::toCvMat(vPoint->estimate()));
pMP->UpdateNormalAndDepth();
}
最后,把优化后的结果写回到关键帧和地图点中。
Pose Optimization #
Pose Optimization 是 ORB-SLAM2 中用于优化单帧位姿的函数。通过最小化当前帧特征点与地图点之间的重投影误差,该函数可以有效地优化当前帧的位姿。
主要步骤 #
1. 构建优化器 #
第一步函数构建了一个 g2o 优化器,并设置了相应的求解器和优化算法。 (g2o::VertexSE3Expmap) 和 Levenberg-Marquardt 。
2. 添加顶点 #
函数将当前帧的位姿添加到优化器中作为一个顶点,其 ID 为 0。注意这里的位姿是可以优化的,即没有被固定。
3. 添加边 #
接下来,函数遍历当前帧中的所有地图点,为每个有效的地图点构建一条误差边:
- 如果是单目观测,构建一条 g2o::EdgeSE3ProjectXYZOnlyPose 类型的边。
- 如果是双目观测,构建一条 g2o::EdgeStereoSE3ProjectXYZOnlyPose 类型的边。
每条边都连接了位姿顶点和一个先验的 3D 点 (地图点),其误差函数为重投影误差。边的信息矩阵根据特征点所在的尺度 (invSigma2) 进行设置。同时,为了提高鲁棒性,函数还为每条边设置了一个 Huber 核函数。
4. 多次优化与外点剔除 #
在构建完误差边之后,函数会进行 4 次优化。在每次优化之后,函数会根据卡方检验的结果将误差较大的边标记为外点,在下一次优化时不再考虑这些外点。这样可以逐步剔除错误的匹配,提高优化的精度。
5. 恢复优化后的位姿 #
函数从优化器中恢复优化后的位姿,并用其更新当前帧的位姿。函数返回的是内点的数量,作为优化质量的衡量。
OpenSSH CVE-2020-15778 漏洞 #
复现这个漏洞需要有服务器凭证,如果没有服务器凭证,攻击者无法利用这个漏洞。即使升级到OpenSSH 9.4,CVE-2020-15778的命令注入漏洞依然存在,扫描结果显示没有其他安全漏洞存在,保留长期支持(LTS)版本OpenSSH 8.9是可行的。由于CVE-2020-15778漏洞仍然未被修复,实际上没有必要更新到更高的OpenSSH版本,只需维持当前LTS版本即可。
关键考虑因素 #
- 版本保留:当前环境中,保留OpenSSH 8.9(LTS版本)是理想的选择,除非出现其他的安全漏洞。
- 功能与安全的平衡:需要在保持实验室操作便利性和系统安全性之间找到平衡点。
- 漏洞管理:尽管存在CVE-2020-15778漏洞,但只要不公开敏感的服务器凭证,该漏洞的利用风险可以被有效控制。
推荐行动 #
- 继续使用OpenSSH 8.9版本,不进行不必要的升级。
- 定期进行安全扫描,确认无其他漏洞存在。
- 加强凭证管理,防止未授权访问。
以上措施将有助于保证实验室成员的正常使用,同时保持系统的安全性。
目标 #
限制对Docker容器内SSH服务的访问,确保只有通过VPN连接的用户(IP地址属于10.8.0.0/24网段)能够访问容器内的22端口。
做法 #
在宿主机添加service,在openvpn和docker启动之后执行添加iptables规则的脚本
#!/bin/bash
# file location: /admin/bootScript/setup-iptables.sh
# Flush FORWARD chain (optional, remove this if you want to keep existing rules)
# iptables -F FORWARD
# Insert iptables rules at the top of the FORWARD chain
iptables -I FORWARD -d 172.30.0.0/24 -p tcp -m tcp --dport 22 -j REJECT --reject-with icmp-port-unreachable
iptables -I FORWARD -d 172.30.0.0/24 -p tcp -m tcp --dport 22 -j LOG --log-prefix "SSH access into container out"
iptables -I FORWARD -s 10.8.0.0/24 -d 172.30.0.0/24 -p tcp -m tcp --dport 22 -j ACCEPT
添加新的iptables规则:
首先拒绝所有尝试从任何源到达172.30.0.0/24网段(Docker内网)的22端口的TCP连接。
记录所有被拒绝的尝试进入容器的SSH连接请求。
允许从10.8.0.0/24网段(VPN客户端网段)到172.30.0.0/24网段的22端口的TCP连接。
添加为系统服务 #
创建一个systemd服务,确保在openvpn和docker启动之后执行添加iptables规则的脚本。
# file location:/etc/systemd/system/iptables-rules.service
[Unit]
Description=Insert iptables rules at boot
After=network.target docker.service openvpn.service
[Service]
Type=oneshot
ExecStart=/admin/bootScript/setup-iptables.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
Docker内网 #
为了确保iptables控制的是Docker内网的流量,创建了一个docker network用于容器间通信,可以用以下命令查看docker network的配置:
docker network inspect container_system_network
可以得到包含以下内容的输入
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.30.0.0/16",
"Gateway": "172.30.0.1"
}
]
}
执行这些规则后,只有从VPN连接的用户才能访问Docker内部的SSH服务,其他所有未经授权的访问尝试都将被记录并拒绝。