文章目录
一、算法介绍
摘要——在这项工作中,我们提出了一种新颖的全局描述符,称为稳定三角形描述符(STD),用于三维地点识别。对于一个三角形,其形状由边长或夹角唯一确定。此外,三角形的形状对刚性变换完全不变。基于这一特性,我们首先设计了一种算法,从三维点云中高效提取局部关键点,并将这些关键点编码为三角形描述符。然后,通过匹配点云之间描述符的边长(以及其他一些信息)来实现地点识别。通过描述符匹配对获得的点对应关系可以进一步用于几何验证,从而大大提高地点识别的准确性。在我们的实验中,我们将所提出的系统与其他先进系统(即M2DP、ScanContext)在公共数据集(即KITTI、NCLT和Complex-Urban)以及我们自收集的数据集(使用非重复扫描的固态激光雷达)上进行了广泛比较。所有定量结果表明,STD在适应性方面更强,并且在精度上有显著提升。
算法框架
二、代码解析
在关键帧中提取关键点后,构建了一个k-D树,并为每个点搜索20个邻近点来形成三角形描述符。具有相同边长的冗余描述符将被消除。每个三角形描述符包含三个顶点 p 1 , p 2 p_{1}, p_{2} p1,p2 和 p 3 p_{3} p3,以及投影法线向量 n 1 , n 2 n_{1}, n_{2} n1,n2 和 n 3 n_{3} n3。此外,三角形的顶点按照边长升序排列(见图3)。我们总结三角形描述符 Δ \Delta Δ 包含以下内容:
- p 1 , p 2 , p 3 p1, p2, p3 p1,p2,p3:三个顶点,
- n 1 , n 2 , n 3 n1, n2, n3 n1,n2,n3:三个投影法线向量,
- l 12 , l 23 , l 13 l_{12}, l_{23}, l_{13} l12,l23,l13:三条边,且 l 12 ≤ l 23 ≤ l 13 l_{12}\leq l_{23}\leq l_{13} l12≤l23≤l13,
- q q q:三角形的质心,
- k k k:与描述符对应的帧编号。
除了描述符,我们还将保存从这个关键帧中提取的所有 n n n 个平面 Π = ( π 1 , π 2 , … , π n ) \Pi=\left(\pi_{1},\pi_{2},\ldots,\pi_{n}\right) Π=(π1,π2,…,πn),以备后续的几何验证步骤使用。对应的代码为:
1.三角描述子定义
// 定义三角形描述符结构体STDesc,用于存储三角形的几何和属性信息
typedef struct STDesc {
// 三角形的三条边长,从短到长依次排列
Eigen::Vector3d side_length_;
// 顶点之间的投影角度(即法向量之间的夹角)
Eigen::Vector3d angle_;
// 三角形质心坐标
Eigen::Vector3d center_;
// 该描述符所属的帧编号
unsigned int frame_id_;
// 三角形的三个顶点坐标
Eigen::Vector3d vertex_A_;
Eigen::Vector3d vertex_B_;
Eigen::Vector3d vertex_C_;
// 每个顶点的附加信息,例如强度值(intensity)
Eigen::Vector3d vertex_attached_;
} STDesc;
2.平面的计算
代码首先初始化了协方差矩阵 C \ C C,中心点 P ⃗ \vec{P} P 和法向量 N ⃗ \vec{N} N。然后,它遍历所有的体素点,计算协方差矩阵和中心点。使用特征值分解来找到协方差矩阵的三个特征向量和特征值,这些特征向量代表了数据的主方向,而特征值代表了这些方向上的分散程度。最小的特征值对应的特征向量被认为是平面的法向量,因为平面上的点在这个方向上的分散程度最小。如果最小特征值小于一个预设的阈值,那么认为这些点确实构成了一个平面,并计算平面的半径和截距。最后,根据这些计算结果设置平面模型的属性。
-
初始化协方差矩阵 C \ C C 和中心点 P ⃗ \vec{P} P 为零向量,法向量 N ⃗ \vec{N} N 也为零向量。
C = 0 , P ⃗ = 0 , N ⃗ = 0 C = \mathbf{0}, \quad \vec{P} = \mathbf{0}, \quad \vec{N} = \mathbf{0} C=0,P=0,N=0 -
计算点的数量 n \ n n。
-
对于每个点 p ⃗ i \vec{p}_i pi 在体素点集合中:
- 累加协方差矩阵 C \ C C。
C = C + p ⃗ i p ⃗ i T C = C + \vec{p}_i \vec{p}_i^T C=C+pipiT - 累加中心点 P ⃗ \vec{P} P。
P ⃗ = P ⃗ + p ⃗ i \vec{P} = \vec{P} + \vec{p}_i P=P+pi
- 累加协方差矩阵 C \ C C。
-
计算平均中心点 P ⃗ \vec{P} P。
P ⃗ = P ⃗ n \vec{P} = \frac{\vec{P}}{n} P=nP -
计算协方差矩阵 C \ C C。
C = C n − P ⃗ P ⃗ T C = \frac{C}{n} - \vec{P} \vec{P}^T C=nC−PPT -
对协方差矩阵 C \ C C 进行特征值分解,得到特征向量 E \ E E 和特征值 Λ \ \Lambda Λ。
E , Λ = EigenSolver ( C ) E, \Lambda = \text{EigenSolver}(C) E,Λ=EigenSolver(C) -
提取实数部分的特征值 Λ real \ \Lambda_{\text{real}} Λreal。
Λ real = Re ( Λ ) \Lambda_{\text{real}} = \text{Re}(\Lambda) Λreal=Re(Λ) -
找到最小和最大的特征值对应的索引 min \text{min} min 和 max \text{max} max。
min = arg min ( Λ real ) , max = arg max ( Λ real ) \text{min} = \arg\min(\Lambda_{\text{real}}), \quad \text{max} = \arg\max(\Lambda_{\text{real}}) min=argmin(Λreal),max=argmax(Λreal) -
如果最小特征值小于平面检测阈值 plane_detection_thre \text{plane\_detection\_thre} plane_detection_thre:
- 设置法向量 N ⃗ \vec{N} N 为对应最小特征值的特征向量。
N ⃗ = E real ( : , min ) \vec{N} = E_{\text{real}}(:, \text{min}) N=Ereal(:,min) - 设置最小特征值为 Λ min \Lambda_{\text{min}} Λmin。
Λ min = Λ real ( min ) \Lambda_{\text{min}} = \Lambda_{\text{real}}(\text{min}) Λmin=Λreal(min) - 计算半径 r \ r r 为最大特征值的平方根。
r = Λ real ( max ) r = \sqrt{\Lambda_{\text{real}}(\text{max})} r=Λreal(max) - 设置平面标志 is_plane \text{is\_plane} is_plane 为真。
is_plane = true \text{is\_plane} = \text{true} is_plane=true - 计算截距 b \ b b。
b = − ( N ⃗ ( 0 ) ⋅ P ⃗ ( 0 ) + N ⃗ ( 1 ) ⋅ P ⃗ ( 1 ) + N ⃗ ( 2 ) ⋅ P ⃗ ( 2 ) ) b = -(\vec{N}(0) \cdot \vec{P}(0) + \vec{N}(1) \cdot \vec{P}(1) + \vec{N}(2) \cdot \vec{P}(2)) b=−(N(0)⋅P(0)+N(1)⋅P(1)+N(2)⋅P(2)) - 设置平面中心点 P ⃗ center \vec{P}_{\text{center}} Pcenter 的坐标和法向量。
P ⃗ center . x = P ⃗ ( 0 ) , P ⃗ center . y = P ⃗ ( 1 ) , P ⃗ center . z = P ⃗ ( 2 ) \vec{P}_{\text{center}}.x = \vec{P}(0), \quad \vec{P}_{\text{center}}.y = \vec{P}(1), \quad \vec{P}_{\text{center}}.z = \vec{P}(2) Pcenter.x=P(0),Pcenter.y=P(1),Pcenter.z=P(2)
P ⃗ center . n o r m a l x = N ⃗ ( 0 ) , P ⃗ center . n o r m a l y = N ⃗ ( 1 ) , P ⃗ center . n o r m a l z = N ⃗ ( 2 ) \vec{P}_{\text{center}}.normal_x = \vec{N}(0), \quad \vec{P}_{\text{center}}.normal_y = \vec{N}(1), \quad \vec{P}_{\text{center}}.normal_z = \vec{N}(2) Pcenter.normalx=N(0),Pcenter.normaly=N(1),Pcenter.normalz=N(2) - 否则,设置平面标志 is_plane \text{is\_plane} is_plane 为假。
is_plane = false \text{is\_plane} = \text{false} is_plane=false
- 设置法向量 N ⃗ \vec{N} N 为对应最小特征值的特征向量。
// 定义一个函数 init_plane,用于初始化平面模型
void OctoTree::init_plane() {
// 初始化协方差矩阵为零矩阵
plane_ptr_->covariance_ = Eigen::Matrix3d::Zero();
// 初始化平面的中心点为零向量
plane_ptr_->center_ = Eigen::Vector3d::Zero();
// 初始化平面的法向量为零向量
plane_ptr_->normal_ = Eigen::Vector3d::Zero();
// 记录体素点的数量
plane_ptr_->points_size_ = voxel_points_.size();
// 初始化平面的半径为0
plane_ptr_->radius_ = 0;
// 遍历所有的体素点
for (auto pi : voxel_points_) {
// 累加每个点的外积,用于计算协方差矩阵
plane_ptr_->covariance_ += pi * pi.transpose();
// 累加每个点的坐标,用于计算中心点
plane_ptr_->center_ += pi;
}
// 计算平均中心点
plane_ptr_->center_ = plane_ptr_->center_ / plane_ptr_->points_size_;
// 计算协方差矩阵,减去中心点的贡献
plane_ptr_->covariance_ = plane_ptr_->covariance_ / plane_ptr_->points_size_ -
plane_ptr_->center_ * plane_ptr_->center_.transpose();
// 对协方差矩阵进行特征值分解
Eigen::EigenSolver<Eigen::Matrix3d> es(plane_ptr_->covariance_);
// 获取特征向量和特征值
Eigen::Matrix3cd evecs = es.eigenvectors();
Eigen::Vector3cd evals = es.eigenvalues();
// 提取特征值的实数部分
Eigen::Vector3d evalsReal = evals.real();
// 找到最小和最大特征值的索引
Eigen::Matrix3d::Index evalsMin, evalsMax;
evalsReal.rowwise().sum().minCoeff(&evalsMin);
evalsReal.rowwise().sum().maxCoeff(&evalsMax);
// 计算中间特征值的索引
int evalsMid = 3 - evalsMin - evalsMax;
// 如果最小特征值小于预设的平面检测阈值,则认为找到了一个平面
if (evalsReal(evalsMin) < config_setting_.plane_detection_thre_) {
// 设置平面的法向量为对应最小特征值的特征向量
plane_ptr_->normal_ << evecs.real()(0, evalsMin), evecs.real()(1, evalsMin),
evecs.real()(2, evalsMin);
// 记录最小特征值
plane_ptr_->min_eigen_value_ = evalsReal(evalsMin);
// 计算平面的半径为最大特征值的平方根
plane_ptr_->radius_ = sqrt(evalsReal(evalsMax));
// 设置平面标志为真
plane_ptr_->is_plane_ = true;
// 计算平面的截距
plane_ptr_->intercept_ = -(plane_ptr_->normal_(0) * plane_ptr_->center_(0) +
plane_ptr_->normal_(1) * plane_ptr_->center_(1) +
plane_ptr_->normal_(2) * plane_ptr_->center_(2));
// 设置平面中心点的坐标和法向量
plane_ptr_->p_center_.x = plane_ptr_->center_(0);
plane_ptr_->p_center_.y = plane_ptr_->center_(1);
plane_ptr_->p_center_.z = plane_ptr_->center_(2);
plane_ptr_->p_center_.normal_x = plane_ptr_->normal_(0);
plane_ptr_->p_center_.normal_y = plane_ptr_->normal_(1);
plane_ptr_->p_center_.normal_z = plane_ptr_->normal_(2);
} else {
// 如果最小特征值不小于阈值,则认为没有找到平面
plane_ptr_->is_plane_ = false;
}
}
3.提取角点、特征点
图 5. (a) 边界体素中的点用黄色表示。 (b) 这些点投影到相邻平面上(蓝色点)。 © 平面图像,其中每个像素表示边界体素中点到平面的最大距离(以厘米为单位)。如果一个点在其 5×5 邻域内具有最大像素值,它将被视为关键点(红色点)。
(1)建立连接关系build_connection
函数 STDescManager::build_connection
主要用于构建一个体素映射中各个体素之间的连接关系。具体功能如下:
-
遍历体素映射:函数遍历输入的
voxel_map
,该映射将VOXEL_LOC
类型的位置映射到对应的OctoTree
对象。 -
检查平面体素:对于每个体素,如果其对应的
OctoTree
对象的plane_ptr_->is_plane_
属性为true
,则认为该体素代表一个平面。 -
遍历相邻方向:对于每个平面体素,函数遍历其六个可能的相邻方向(上、下、左、右、前、后)。
-
查找相邻体素:对于每个方向,函数在
voxel_map
中查找对应的相邻体素。 -
建立连接:
- 如果相邻体素存在,并且也代表一个平面,则根据两个平面的法向量是否接近(通过范数比较)来决定是否建立连接。
- 如果法向量的差或和的范数小于预设阈值
config_setting_.plane_merge_normal_thre_
,则认为两个平面相近,可以合并,并建立双向连接。 - 如果相邻体素不代表一个平面,则只将当前体素指向相邻体素,但不形成双向连接。
-
标记连接状态:函数更新
OctoTree
对象的connect_
、is_check_connect_
和connect_tree_
属性来记录连接状态和指向相邻体素的指针。
通过这个函数,可以构建一个体素之间的连接网络,为后续的处理(如平面合并、特征提取等)提供基础。
void STDescManager::build_connection(
std::unordered_map<VOXEL_LOC, OctoTree *> &voxel_map)
{
// 遍历体素映射中的每一个元素
for (auto iter = voxel_map.begin(); iter != voxel_map.end(); iter++)
{
// 如果当前八叉树代表一个平面
if (iter->second->plane_ptr_->is_plane_)
{
OctoTree *current_octo = iter->second; // 获取当前八叉树的指针
// 遍历当前八叉树的6个相邻方向
for (int i = 0; i < 6; i++)
{
VOXEL_LOC neighbor = iter->first; // 复制当前体素的位置作为相邻体素的位置
// 根据i的值设置相邻体素的位置
// i的0-2分别代表x, y, z轴的正方向,3-5分别代表负方向
if (i == 0) {
neighbor.x += 1; }
else if (i == 1) {
neighbor.y += 1; }
else if (i == 2) {
neighbor.z += 1; }
else if (i == 3) {
neighbor.x -= 1; }
else if (i == 4) {
neighbor.y -= 1; }
else if (i == 5) {
neighbor.z -= 1; }
// 在体素映射中查找相邻体素
auto near = voxel_map.find(neighbor);
// 如果没有找到相邻体素
if (near == voxel_map.end())
{
// 标记当前八叉树在i方向上的连接为未连接且已检查
current_octo->is_check_connect_[i] = true;
current_octo->connect_[i] = false;
}
else
{
// 如果当前八叉树在i方向上还未检查过连接
if (!current_octo->is_check_connect_[i])
{
OctoTree *near_octo = near->second; // 获取相邻八叉树的指针
// 标记当前八叉树和相邻八叉树在相应方向上的连接为已检查
current_octo->is_check_connect_[i] = true;
int j = (i >= 3) ? (i - 3) : (i + 3); // 计算相邻八叉树对应的反向索引
near_octo->is_check_connect_[j] = true;
// 如果相邻八叉树也代表一个平面
if (near_octo->plane_ptr_->is_plane_)
{
// 计算两个平面法向量的差和和
Eigen::Vector3d normal_diff = current_octo->plane_ptr_->normal_ - near_octo->plane_ptr_->normal_;
Eigen::Vector3d normal_add = current_octo->plane_ptr_->normal_ + near_octo->plane_ptr_->normal_;
// 如果法向量的差或和的范数小于某个阈值,则认为两个平面相近,可以合并
if (normal_diff.norm() < config_setting_.plane_merge_normal_thre_ ||
normal_add.norm() < config_setting_.plane_merge_normal_thre_)
{
// 标记两个八叉树在相应方向上的连接为已连接,并互相指向对方
current_octo->connect_[i] = true;
near_octo->connect_[j] = true;
current_octo->connect_tree_[i] = near_octo;
near_octo->connect_tree_[j] = current_octo;
}
else
{
// 否则,标记为未连接
current_octo->connect_[i] = false;
near_octo->connect_[j] = false;
}
}
else
{
// 如果相邻八叉树不代表一个平面,则只将当前八叉树指向相邻八叉树,但不形成双向连接
current_octo->connect_[i] = false;
near_octo->connect_[j] = true;
near_octo->connect_tree_[j] = current_octo;
}
}
}
}
}
}
}
(2)角点提取 (STDescManager::corner_extractor)
函数 STDescManager::corner_extractor
旨在从给定的体素映射和点云数据中提取角点。其主要步骤和功能如下:
-
初始化临时点云:创建一个临时点云
prepare_corner_points
用于存储准备提取的角点。 -
定义体素偏移量:为了避免不同视角导致的体素切割不一致,定义了一个体素偏移量的列表
voxel_round
,包含-1到1的x、y、z方向上的偏移量。 -
遍历体素映射:遍历输入的体素映射
voxel_map
,对于每个不包含平面的体素,获取其位置和对应的OctoTree
指针。 -
检查体素连接:对于每个体素,遍历其六个方向的连接,如果找到有连接的体素且该连接体素代表一个平面,则进行进一步处理。
-
投影和角点提取:
- 将当前体素及其周围偏移体素投影到相邻平面上。
- 使用
extract_corner
函数在投影点上提取角点,并将提取的角点添加到prepare_corner_points
点云中。
-
非极大值抑制:对准备的角点进行非极大值抑制,以减少角点数量并增强角点的显著性。
-
选择角点:
- 如果准备的角点数量小于配置的最大角点数量,则直接将这些角点赋值给输出的角点云
corner_points
。 - 如果准备的角点数量大于配置的最大角点数量,则根据角点的强度进行排序,并选择前
maximum_corner_num_
个角点作为最终的角点。
- 如果准备的角点数量小于配置的最大角点数量,则直接将这些角点赋值给输出的角点云
void STDescManager::corner_extractor(
std::unordered_map<VOXEL_LOC, OctoTree *> &voxel_map, // 输入的体素映射,键是体素位置,值是OctoTree指针
const pcl::PointCloud<pcl::PointXYZI>::Ptr &input_cloud, // 输入的点云数据
pcl::PointCloud<pcl::PointXYZINormal>::Ptr &corner_points) {
// 输出的角点云数据
pcl::PointCloud<pcl::PointXYZINormal>::Ptr prepare_corner_points(
new pcl::PointCloud<pcl::PointXYZINormal>); // 用于准备角点的临时点云
// 避免由于不同视角导致的体素切割不一致
std::vector<Eigen::Vector3i> voxel_round; // 存储体素周围偏移量的向量
for (int x = -1; x <= 1; x++) {
// 遍历x方向的偏移量
for (int y = -1; y <= 1; y++) {
// 遍历y方向的偏移量
for (int z = -1; z <= 1; z++) {
// 遍历z方向的偏移量
Eigen::Vector3i voxel_inc(x, y, z); // 创建当前偏移量的Eigen向量
voxel_round.push_back(voxel_inc); // 将偏移量添加到列表中
}
}
}
for (auto iter = voxel_map.begin(); iter != voxel_map.end(); iter++)
{
// 遍历体素映射
if (!iter->second->plane_ptr_->is_plane_)
{
// 如果当前体素不包含平面
VOXEL_LOC current_position = iter->first; // 获取当前体素的位置
OctoTree *current_octo = iter->second; // 获取当前OctoTree指针
int connect_index = -1; // 初始化连接索引
for (int i = 0; i < 6; i++)
{
// 遍历6个方向的连接
if (current_octo->connect_[i])
{
// 如果当前方向有连接
connect_index = i; // 记录连接索引
OctoTree *connect_octo = current_octo->connect_tree_[connect_index]; // 获取连接的OctoTree
bool use = false; // 初始化使用标志
for (int j = 0; j < 6; j++)
{
// 遍历连接OctoTree的6个方向的连接
if (connect_octo->is_check_connect_[j])
{
// 如果当前方向需要检查连接
if (connect_octo->connect_[j])
{
// 如果该方向有连接
use = true; // 设置使用标志为true
}
}
}
// 如果没有相邻平面,跳过
if (use == false)
{
continue;
}
// 只投影包含超过10个点的体素
if (current_octo->voxel_points_.size() > 10)
{
Eigen::Vector3d projection_normal =
current_octo->connect_tree_[connect_index]->plane_ptr_->normal_; // 获取投影平面的法向量
Eigen::Vector3d projection_center =
current_octo->connect_tree_[connect_index]->plane_ptr_->center_; // 获取投影平面的中心点
std::vector<Eigen::Vector3d> proj_points; // 存储投影点的向量
// 将边界体素和附近体素投影到相邻平面上
for (auto voxel_inc : voxel_round)
{
VOXEL_LOC connect_project_position = current_position; // 获取当前体素位置
connect_project_position.x += voxel_inc[0]; // 应用x方向偏移
connect_project_position.y += voxel_inc[1]; // 应用y方向偏移
connect_project_position.z += voxel_inc[2]; // 应用z方向偏移
auto iter_near = voxel_map.find(connect_project_position); // 在体素映射中查找投影位置
if (iter_near != voxel_map.end())
{
// 如果找到了投影位置
bool skip_flag = false; // 初始化跳过标志
if (!voxel_map[connect_project_position]
->plane_ptr_->is_plane_) {
// 如果投影位置不包含平面
if (voxel_map[connect_project_position]->is_project_)
{
// 如果已经投影过
for (auto normal : voxel_map[connect_project_position]
->proj_normal_vec_) {
// 遍历已投影的法向量
Eigen::Vector3d normal_diff = projection_normal - normal; // 计算法向量差异
Eigen::Vector3d normal_add = projection_normal + normal; // 计算法向量和
// 检查是否重复投影
if (normal_diff.norm() < 0.5 || normal_add.norm() < 0.5)
{
skip_flag = true; // 设置跳过标志为true
}
}
}
if (skip_flag)
{
continue; // 如果需要跳过,继续下一次循环
}
// 将投影点添加到列表中,并标记为已投影
for (size_t j = 0; j < voxel_map[connect_project_position]
->voxel_points_.size();
j++) {
proj_points.push_back(
voxel_map[connect_project_position]->voxel_points_[j]);
voxel_map[connect_project_position]->is_project_ = true;
voxel_map[connect_project_position]
->proj_normal_vec_.push_back(projection_normal);
}
}
}
}
// 在这里进行2D投影和角点提取
pcl::PointCloud<pcl::PointXYZINormal>::Ptr sub_corner_points(
new pcl::PointCloud<pcl::PointXYZINormal>); // 创建临时角点点云
extract_corner(projection_center, projection_normal, proj_points,
sub_corner_points); // 提取角点
for (auto pi : sub_corner_points->points) {
// 将提取的角点添加到准备角点的点云中
prepare_corner_points->push_back(pi);
}
}
}
}
}
}
//强度处理
non_maxi_suppression(prepare_corner_points); // 进行非极大值抑制
// 根据配置设置的最大角点数量,选择是否直接赋值或进行排序选择
if (config_setting_.maximum_corner_num_ > prepare_corner_points->size())
{
corner_points = prepare_corner_points; // 如果准备角点的数量小于最大数量,直接赋值
}
else
{
std::vector<std::pair<double, int>> attach_vec; // 存储角点强度和索引的向量
for (size_t i = 0; i < prepare_corner_points->size(); i++)
{
// 遍历准备角点的点云
attach_vec.push_back(std::pair<double, int>(prepare_corner_points->points[i].intensity, i)); // 将角点强度和索引添加到向量中
}
std::sort(attach_vec.begin(), attach_vec.end(), attach_greater_sort); // 按强度降序排序
for (size_t i = 0; i < config_setting_.maximum_corner_num_; i++)
{
// 根据最大角点数量选择角点
corner_points->points.push_back(prepare_corner_points->points[attach_vec[i].second]);
}
}
}
(3)角点提取核心函数extract_corner
该函数STDescManager::extract_corner主要执行以下任务:它根据提供的投影中心和法线计算出用于定义投影平面的参数,并根据这些参数将3D点投影到2D平面上。函数通过梯度计算识别出潜在的角点,并排除那些可能是直线上的部分。它将这些角点从2D平面重新映射回3D空间,并存储在输出的点云对象中,以便后续处理。这个过程涉及到距离阈值的设定、2D坐标的转换、梯度的计算以及角点的筛选和重新投影。
-
初始化和参数准备:
- 获取投影图像的分辨率 resolution \text{resolution} resolution、最小距离阈值 dis_threshold_min \text{dis\_threshold\_min} dis_threshold_min 和最大距离阈值 dis_threshold_max \text{dis\_threshold\_max} dis_threshold_max。
- 提取投影法线的系数 A = proj_normal [ 0 ] \text{A} = \text{proj\_normal}[0] A=proj_normal[0], B = proj_normal [ 1 ] \text{B} = \text{proj\_normal}[1] B=proj_normal[1], C = proj_normal [ 2 ] \text{C} = \text{proj\_normal}[2] C=proj_normal[2],并计算 D = − ( A ⋅ proj_center [ 0 ] + B ⋅ proj_center [ 1 ] + C ⋅ proj_center [ 2 ] ) \text{D} = -(\text{A} \cdot \text{proj\_center}[0] + \text{B} \cdot \text{proj\_center}[1] + \text{C} \cdot \text{proj\_center}[2]) D=−(A⋅proj_center[0]+B⋅proj_center[1]+C⋅proj_center[2])。
-
计算点到平面的距离和投影:
- 计算每个点到投影平面的距离 dis = ∣ x ⋅ A + y ⋅ B + z ⋅ C + D ∣ \text{dis} = |x \cdot \text{A} + y \cdot \text{B} + z \cdot \text{C} + \text{D}| dis=∣x⋅A+y⋅B+z⋅C+D∣,并过滤超出阈值的点。
- 对有效的点计算其在投影平面上的投影坐标,并转换为2D坐标。
-
确定2D点集边界和分割:
- 找出2D点集的边界,计算分割的数量,并初始化存储图像数据的容器。
-
分配点到分割并计算均值坐标和梯度:
- 将2D点分配到对应的分割中,并计算每个分割的均值坐标和计数。
- 计算每个分割的梯度,用于后续的角点检测。
-
提取角点并过滤直线:
- 在每个分割中寻找梯度最大的点作为角点,并过滤掉直线上的点。
-
将角点重新投影到3D空间并输出:
- 将2D角点坐标重新投影到3D空间,并添加到输出点云中。对于每个角点,计算其在3D空间中的坐标 coord = p y ⋅ x_axis + p x ⋅ y_axis + proj_center \text{coord} = py \cdot \text{x\_axis} + px \cdot \text{y\_axis} + \text{proj\_center} coord=py⋅x_axis+px⋅y_axis+proj_center。
void STDescManager::extract_corner(
const Eigen::Vector3d &proj_center, const Eigen::Vector3d proj_normal,
const std::vector<Eigen::Vector3d> proj_points,
pcl::PointCloud<pcl::PointXYZINormal>::Ptr &corner_points)
{
// 从配置中获取投影图像的分辨率、最小和最大距离阈值
double resolution = config_setting_.proj_image_resolution_;
double dis_threshold_min = config_setting_.proj_dis_min_;
double dis_threshold_max = config_setting_.proj_dis_max_;
// 提取投影法线的系数
double A = proj_normal[0];
double B = proj_normal[1];
double C = proj_normal[2];
double D = -(A * proj_center[0] + B * proj_center[1] + C * proj_center[2]);
// 定义x轴向量,并根据投影法线的方向调整x轴向量
Eigen::Vector3d x_axis(1, 1, 0);
if (C != 0)
{
x_axis[2] = -(A + B) / C;
}
else if (B != 0)
{
x_axis[1] = -A / B;
}
else
{
x_axis[0] = 0;
x_axis[1] = 1;
}
x_axis.normalize(); // 归一化x轴向量
Eigen::Vector3d y_axis = proj_normal.cross(x_axis); // 计算与投影法线正交的y轴向量
y_axis.normalize(); // 归一化y轴向量
// 计算2D坐标转换所需的系数
double ax = x_axis[0];
double bx = x_axis[1];
double cx = x_axis[2];
double dx = -(ax * proj_center[0] + bx * proj_center[1] + cx * proj_center[2]);
double ay = y_axis[0];
double by = y_axis[1];
double cy = y_axis[2];
double dy = -(ay * proj_center[0] + by * proj_center[1] + cy * proj_center[2]);
// 存储2D点的容器
std::vector<Eigen::Vector2d> point_list_2d;
for (size_t i = 0; i < proj_points.size(); i++)
{
double x = proj_points[i][0];
double y = proj_points[i][1];
double z = proj_points[i][2];
double dis = fabs(x * A + y * B + z * C + D); // 计算点到平面的距离
if (dis < dis_threshold_min || dis > dis_threshold_max)
{
continue; // 根据距离阈值过滤点
}
Eigen::Vector3d cur_project; // 当前点在投影平面上的投影
// 计算点在投影平面上的投影坐标
cur_project[0] = (-A * (B * y + C * z + D) + x * (B * B + C * C)) / (A * A + B * B + C * C);
cur_project[1] = (-B * (A * x + C * z + D) + y * (A * A + C * C)) / (A * A + B * B + C * C);
cur_project[2] = (-C * (A * x + B * y + D) + z * (A * A + B * B)) / (A * A + B * B + C * C);
pcl::PointXYZ p; // PCL点类型
p.x = cur_project[0];
p.y = cur_project[1];
p.z = cur_project[2];
double project_x = cur_project[0] * ay + cur_project[1] * by + cur_project[2] * cy + dy; // 计算2D坐标
double project_y = cur_project[0] * ax + cur_project[1] * bx + cur_project[2] * cx + dx;
Eigen::Vector2d p_2d(project_x, project_y); // 存储2D坐标
point_list_2d.push_back(p_2d); // 添加到2D点容器中
}
// 找出2D点集的边界
double min_x = 10;
double max_x = -10;
double min_y = 10;
double max_y = -10;
if (point_list_2d.size() <= 5)
{
return; // 如果2D点的数量太少,则直接返回
}
for (auto pi : point_list_2d)
{
if (pi[0] < min_x)
{
min_x = pi[0];
}
if (pi[0] > max_x)
{
max_x = pi[0];
}
if (pi[1] < min_y)
{
min_y = pi[1];
}
if (pi[1] > max_y)
{
max_y = pi[1];
}
}
// 根据分辨率和边界计算分割的数量
int segmen_base_num = 5;
double segmen_len = segmen_base_num * resolution;
int x_segment_num = (max_x - min_x) / segmen_len + 1;
int y_segment_num = (max_y - min_y) / segmen_len + 1;
int x_axis_len = (int)((max_x - min_x) / resolution + segmen_base_num);
int y_axis_len = (int)((max_y - min_y) / resolution + segmen_base_num);
// 初始化用于存储图像数据的容器
std::vector<Eigen::Vector2d> img_container[x_axis_len][y_axis_len];
double img_count_array[x_axis_len][y_axis_len] = {
0};
double gradient_array[x_axis_len][y_axis_len] = {
0};
double mean_x_array[x_axis_len][y_axis_len] = {
0};
double mean_y_array[x_axis_len][y_axis_len] = {
0};
for (int x = 0; x < x_axis_len; x++)
{
for (int y = 0; y < y_axis_len; y++)
{
img_count_array[x][y] = 0;
mean_x_array[x][y] = 0;
mean_y_array[x][y] = 0;
gradient_array[x][y] = 0;
std::vector<Eigen::Vector2d> single_container;
img_container[x][y] = single_container;
}
}
// 分配点到分割中,并计算每个分割的均值坐标和计数
for (size_t i = 0; i < point_list_2d.size(); i++)
{
int x_index = (int)((point_list_2d[i][0] - min_x) / resolution);
int y_index = (int)((point_list_2d[i][1] - min_y) / resolution);
mean_x_array[x_index][y_index] += point_list_2d[i][0];
mean_y_array[x_index][y_index] += point_list_2d[i][1];
img_count_array[x_index][y_index]++;
img_container[x_index][y_index].push_back(point_list_2d[i]);
}
// 计算梯度
for (int x = 0; x < x_axis_len; x++)
{
for (int y = 0; y < y_axis_len; y++)
{
double gradient = 0;
int cnt = 0;
int inc = 1;
for (int x_inc = -inc; x_inc <= inc; x_inc++)
{
for (int y_inc = -inc; y_inc <= inc; y_inc++)
{
int xx = x + x_inc;
int yy = y + y_inc;
if (xx >= 0 && xx < x_axis_len && yy >= 0 && yy < y_axis_len)
{
if (xx != x || yy != y)
{
if (img_count_array[xx][yy] >= 0)
{
gradient += img_count_array[x][y] - img_count_array[xx][yy];
cnt++;
}
}
}
}
}
if (cnt != 0)
{
gradient_array[x][y] = gradient * 1.0 / cnt;
}
else
{
gradient_array[x][y] = 0;
}
}
}
// 通过梯度提取角点
std::vector<int> max_gradient_vec; // 存储最大梯度值的向量
std::vector<int> max_gradient_x_index_vec; // 存储最大梯度对应的x坐标索引的向量
std::vector<int> max_gradient_y_index_vec; // 存储最大梯度对应的y坐标索引的向量
// 在每个分割中寻找梯度最大的点
for (int x_segment_index = 0; x_segment_index < x_segment_num; x_segment_index++)
{
for (int y_segment_index = 0; y_segment_index < y_segment_num; y_segment_index++)
{
double max_gradient = 0; // 初始化最大梯度值
int max_gradient_x_index = -10; // 初始化最大梯度的x坐标索引
int max_gradient_y_index = -10; // 初始化最大梯度的y坐标索引
// 在当前分割的每个子区域内寻找梯度最大的点
for (int x_index = x_segment_index * segmen_base_num; x_index < (x_segment_index + 1) * segmen_base_num; x_index++)
{
for (int y_index = y_segment_index * segmen_base_num; y_index < (y_segment_index + 1) * segmen_base_num; y_index++)
{
// 如果当前点的梯度值大于已知的最大梯度值,则更新最大梯度值及其坐标索引
if (img_count_array[x_index][y_index] > max_gradient)
{
max_gradient = img_count_array[x_index][y_index];
max_gradient_x_index = x_index;
max_gradient_y_index = y_index;
}
}
}
// 如果最大梯度值大于配置的角点阈值,则认为该点是角点
if (max_gradient >= config_setting_.corner_thre_)
{
max_gradient_vec.push_back(max_gradient);
max_gradient_x_index_vec.push_back(max_gradient_x_index);
max_gradient_y_index_vec.push_back(max_gradient_y_index);
}
}
}
// 过滤掉直线
// 计算直线或非直线
std::vector<Eigen::Vector2i> direction_list; // 存储方向向量的向量
Eigen::Vector2i d(0, 1); // 方向向量(0,1)
direction_list.push_back(d);
d << 1, 0; // 方向向量(1,0)
direction_list.push_back(d);
d << 1, 1; // 方向向量(1,1)
direction_list.push_back(d);
d << 1, -1; // 方向向量(1,-1)
direction_list.push_back(d);
// 对于每个最大梯度值对应的点,检查其是否为直线
for (size_t i = 0; i < max_gradient_vec.size(); i++)
{
bool is_add = true; // 标记是否添加该点作为角点
for (int j = 0; j < 4; j++)
{
// 检查四个方向
Eigen::Vector2i p(max_gradient_x_index_vec[i], max_gradient_y_index_vec[i]); // 当前点的坐标
Eigen::Vector2i p1 = p + direction_list[j]; // 沿方向j的点
Eigen::Vector2i p2 = p - direction_list[j]; // 沿方向-j的点
int threshold = img_count_array[p[0]][p[1]] / 2; // 阈值
// 如果沿方向j和-j的点的梯度值都大于阈值,则认为当前点可能是直线上的点
if (img_count_array[p1[0]][p1[1]] >= threshold &&
img_count_array[p2[0]][p2[1]] >= threshold)
{
// is_add = false; // 注释掉,因为我们不在这里直接修改is_add
}
else
{
continue; // 如果不是直线,则跳过
}
}
// 如果当前点不是直线,则将其添加到角点集合中
if (is_add)
{
double px = mean_x_array[max_gradient_x_index_vec[i]][max_gradient_y_index_vec[i]] / img_count_array[max_gradient_x_index_vec[i]][max_gradient_y_index_vec[i]]; // 计算平均x坐标
double py = mean_y_array[max_gradient_x_index_vec[i]][max_gradient_y_index_vec[i]] / img_count_array[max_gradient_x_index_vec[i]][max_gradient_y_index_vec[i]]; // 计算平均y坐标
// 将2D角点重新投影到3D空间
Eigen::Vector3d coord = py * x_axis + px * y_axis + proj_center;
pcl::PointXYZINormal pi; // PCL点类型,用于存储3D角点信息
pi.x = coord[0];
pi.y = coord[1];
pi.z = coord[2];
pi.intensity = max_gradient_vec[i]; // 角点强度设置为梯度值
pi.normal_x = proj_normal[0]; // 法线x分量
pi.normal_y = proj_normal[1]; // 法线y分量
pi.normal_z = proj_normal[2]; // 法线z分量
corner_points->points.push_back(pi); // 将3D角点添加到输出点云中
}
}
return; // 函数结束
}
4.三角描述符计算过程(STDescManager::build_stdesc)
-
参数初始化
定义比例因子 scale \text{scale} scale,近邻数量 near_num \text{near\_num} near_num,边长最大/最小阈值:scale = 1.0 config_setting_ . s t d _ s i d e _ r e s o l u t i o n _ \text{scale} = \frac{1.0}{\text{config\_setting\_}.std\_side\_resolution\_} scale=config_setting_.std_side_resolution_1.0
near_num = config_setting_ . d e s c r i p t o r _ n e a r _ n u m _ \text{near\_num} = \text{config\_setting\_}.descriptor\_near\_num\_ near_num=config_setting_.descriptor_near_num_
max_dis_threshold = config_setting_ . d e s c r i p t o r _ m a x _ l e n _ \text{max\_dis\_threshold} = \text{config\_setting\_}.descriptor\_max\_len\_ max_dis_threshold=config_setting_.descriptor_max_len_
min_dis_threshold = config_setting_ . d e s c r i p t o r _ m i n _ l e n _ \text{min\_dis\_threshold} = \text{config\_setting\_}.descriptor\_min\_len\_ min_dis_threshold=config_setting_.descriptor_min_len_ -
邻近点搜索
使用 k-D 树查找每个角点的最近near_num
个邻居点,形成多个三角形组合。 -
三角形顶点与边长计算
对于每个组合的三个点 p 1 p1 p1、 p 2 p2 p2 和 p 3 p3 p3,计算三角形的边长 a a a、 b b b、 c c c:a = ( p 1. x − p 2. x ) 2 + ( p 1. y − p 2. y ) 2 + ( p 1. z − p 2. z ) 2 a = \sqrt{(p1.x - p2.x)^2 + (p1.y - p2.y)^2 + (p1.z - p2.z)^2} a=(p1.x−p2.x)2+(p1.y−p2.y)2+(p1.z−p2.z)2
b = ( p 1. x − p 3. x ) 2 + ( p 1. y − p 3. y ) 2 + ( p 1. z − p 3. z ) 2 b = \sqrt{(p1.x - p3.x)^2 + (p1.y - p3.y)^2 + (p1.z - p3.z)^2} b=(p1.x−p3.x)2+(p1.y−p3.y)2+(p1.z−p3.z)2
c = ( p 3. x − p 2. x ) 2 + ( p 3. y − p 2. y ) 2 + ( p 3. z − p 2. z ) 2 c = \sqrt{(p3.x - p2.x)^2 + (p3.y - p2.y)^2 + (p3.z - p2.z)^2} c=(p3.x−p2.x)2+(p3.y−p2.y)2+(p3.z−p2.z)2 -
边长筛选与排序
- 筛选出满足最大/最小阈值条件的三角形。
- 按照边长顺序将 a a a、 b b b、 c c c 排序,使 a ≤ b ≤ c a \leq b \leq c a≤b≤c。
-
三角形描述符构建
-
顶点排序与法向量计算:根据边长和点法向量,定义三角形的三个顶点 A A A、 B B B、 C C C 及其对应的法向量。
-
质心计算:
center = A + B + C 3 \text{center} = \frac{A + B + C}{3} center=3A+B+C -
边长存储:
side_length = ( scale ⋅ a , scale ⋅ b , scale ⋅ c ) \text{side\_length} = \left( \text{scale} \cdot a, \text{scale} \cdot b, \text{scale} \cdot c \right) side_length=(scale⋅a,scale⋅b,scale⋅c) -
投影角度计算:
计算相邻法向量的点积,得到三角形的角度属性:
angle [ 0 ] = ∣ 5 ⋅ normal_1 ⋅ normal_2 ∣ \text{angle}[0] = |5 \cdot \text{normal\_1} \cdot \text{normal\_2}| angle[0]=∣5⋅normal_1⋅normal_2∣
angle [ 1 ] = ∣ 5 ⋅ normal_1 ⋅ normal_3 ∣ \text{angle}[1] = |5 \cdot \text{normal\_1} \cdot \text{normal\_3}| angle[1]=∣5⋅normal_1⋅normal_3∣
angle [ 2 ] = ∣ 5 ⋅ normal_3 ⋅ normal_2 ∣ \text{angle}[2] = |5 \cdot \text{normal\_3} \cdot \text{normal\_2}| angle[2]=∣5⋅normal_3⋅normal_2∣
-
-
结果存储
生成的三角形描述符single_descriptor
包含顶点、质心、边长、角度和 frame_id,将其添加到stds_vec
中。
// 函数STDescManager::build_stdesc:用于构建三角形描述符
void STDescManager::build_stdesc(
const pcl::PointCloud<pcl::PointXYZINormal>::Ptr &corner_points, // 输入关键点
std::vector<STDesc> &stds_vec) {
// 输出三角形描述符的向量
stds_vec.clear(); // 清空输出向量,准备存储新的描述符
// 配置参数
double scale = 1.0 / config_setting_.std_side_resolution_; // 描述符的边长缩放因子
int near_num = config_setting_.descriptor_near_num_; // 每个点附近的邻居点数
double max_dis_threshold = config_setting_.descriptor_max_len_; // 边长的最大阈值
double min_dis_threshold = config_setting_.descriptor_min_len_; // 边长的最小阈值
std::unordered_map<VOXEL_LOC, bool> feat_map; // 用于存储已存在的三角形描述符
pcl::KdTreeFLANN<pcl::PointXYZINormal>::Ptr kd_tree(new pcl::KdTreeFLANN<pcl::PointXYZINormal>); // k-D树用于最近邻搜索
kd_tree->setInputCloud(corner_points); // 将关键点集合设置为kd树输入
std::vector<int> pointIdxNKNSearch(near_num); // 存储最近邻点的索引
std::vector<float> pointNKNSquaredDistance(near_num); // 存储最近邻点的平方距离
// 遍历每个关键点,找到附近的20个邻近点构成三角形描述符
for (size_t i = 0; i < corner_points->size(); i++) {
pcl::PointXYZINormal searchPoint = corner_points->points[i]; // 当前搜索点
// 执行最近邻搜索
if (kd_tree->nearestKSearch(searchPoint, near_num, pointIdxNKNSearch, pointNKNSquaredDistance) > 0) {
// 遍历组合邻近点,以构造三角形
for (int m = 1; m < near_num - 1; m++) {
for (int n = m + 1; n < near_num; n++) {
// 选取三角形的三个顶点p1、p2、p3
pcl::PointXYZINormal p1 = searchPoint;
pcl::PointXYZINormal p2 = corner_points->points[pointIdxNKNSearch[m]];
pcl::PointXYZINormal p3 = corner_points->points[pointIdxNKNSearch[n]];
// 计算法线向量差值与和
Eigen::Vector3d normal_inc1(p1.normal_x - p2.normal_x, p1.normal_y - p2.normal_y, p1.normal_z - p2.normal_z);
Eigen::Vector3d normal_inc2(p3.normal_x - p2.normal_x, p3.normal_y - p2.normal_y, p3.normal_z - p2.normal_z);
Eigen::Vector3d normal_add1(p1.normal_x + p2.normal_x, p1.normal_y + p2.normal_y, p1.normal_z + p2.normal_z);
Eigen::Vector3d normal_add2(p3.normal_x + p2.normal_x, p3.normal_y + p2.normal_y, p3.normal_z + p2.normal_z);
// 计算边长a, b, c
double a = sqrt(pow(p1.x - p2.x, 2) + pow(p1.y - p2.y, 2) + pow(p1.z - p2.z, 2));
double b = sqrt(pow(p1.x - p3.x, 2) + pow(p1.y - p3.y, 2) + pow(p1.z - p3.z, 2));
double c = sqrt(pow(p3.x - p2.x, 2) + pow(p3.y - p2.y, 2) + pow(p3.z - p2.z, 2));
// 判断边长是否在阈值范围内,不在则跳过
if (a > max_dis_threshold || b > max_dis_threshold || c > max_dis_threshold ||
a < min_dis_threshold || b < min_dis_threshold || c < min_dis_threshold) {
continue;
}
// 按边长对顶点重新排序,使得 a <= b <= c
double temp;
Eigen::Vector3d A, B, C;
Eigen::Vector3i l1, l2, l3, l_temp;
l1 << 1, 2, 0;
l2 << 1, 0, 3;
l3 << 0, 2, 3;
if (a > b) {
temp = a; a = b; b = temp; l_temp = l1; l1 = l2; l2 = l_temp; }
if (b > c) {
temp = b; b = c; c = temp; l_temp = l2; l2 = l3; l3 = l_temp; }
if (a > b) {
temp = a; a = b; b = temp; l_temp = l1; l1 = l2; l2 = l_temp; }
// 计算体素位置并检查重复
pcl::PointXYZ d_p;
d_p.x = a * 1000;
d_p.y = b * 1000;
d_p.z = c * 1000;
VOXEL_LOC position((int64_t)d_p.x, (int64_t)d_p.y, (int64_t)d_p.z);
auto iter = feat_map.find(position);
if (iter == feat_map.end()) {
// 新的三角形,构建描述符
Eigen::Vector3d vertex_attached;
if (l1[0] == l2[0]) {
A << p1.x, p1.y, p1.z; normal_1 << p1.normal_x, p1.normal_y, p1.normal_z; vertex_attached[0] = p1.intensity; }
else if (l1[1] == l2[1]) {
A << p2.x, p2.y, p2.z; normal_1 << p2.normal_x, p2.normal_y, p2.normal_z; vertex_attached[0] = p2.intensity; }
else {
A << p3.x, p3.y, p3.z; normal_1 << p3.normal_x, p3.normal_y, p3.normal_z; vertex_attached[0] = p3.intensity; }
if (l1[0] == l3[0]) {
B << p1.x, p1.y, p1.z; normal_2 << p1.normal_x, p1.normal_y, p1.normal_z; vertex_attached[1] = p1.intensity; }
else if (l1[1] == l3[1]) {
B << p2.x, p2.y, p2.z; normal_2 << p2.normal_x, p2.normal_y, p2.normal_z; vertex_attached[1] = p2.intensity; }
else {
B << p3.x, p3.y, p3.z; normal_2 << p3.normal_x, p3.normal_y, p3.normal_z; vertex_attached[1] = p3.intensity; }
if (l2[0] == l3[0]) {
C << p1.x, p1.y, p1.z; normal_3 << p1.normal_x, p1.normal_y, p1.normal_z; vertex_attached[2] = p1.intensity; }
else if (l2[1] == l3[1]) {
C << p2.x, p2.y, p2.z; normal_3 << p2.normal_x, p2.normal_y, p2.normal_z; vertex_attached[2] = p2.intensity; }
else {
C << p3.x, p3.y, p3.z; normal_3 << p3.normal_x, p3.normal_y, p3.normal_z; vertex_attached[2] = p3.intensity; }
// 保存描述符信息
STDesc single_descriptor;
single_descriptor.vertex_A_ = A;
single_descriptor.vertex_B_ = B;
single_descriptor.vertex_C_ = C;
single_descriptor.center_ = (A + B + C) / 3; // 质心
single_descriptor.vertex_attached_ = vertex_attached; // 顶点附加信息
single_descriptor.side_length_ << scale * a, scale * b, scale * c; // 边长
single_descriptor.angle_[0] = fabs(5 * normal_1.dot(normal_2));
single_descriptor.angle_[1] = fabs(5 * normal_1.dot(normal_3));
single_descriptor.angle_[2] = fabs(5 * normal_3.dot(normal_2));
single_descriptor.frame_id_ = current_frame_id_; // 帧编号
feat_map[position] = true; // 记录已处理位置
stds_vec.push_back(single_descriptor); // 存储描述
}
}