0. 简介
我们刚刚了解过DLIO的整个流程,我们发现相比于Point-LIO而言,这个方法更适合我们去学习理解,同时官方给出的结果来看DLIO的结果明显好于现在的主流方法,当然指的一提的是,这个DLIO是必须需要六轴IMU的,所以如果没有IMU的画,那只有DLO可以使用了。
1. computeMetrics、computeSpaciousness、computeSpaciousness—计算度量指标
下面的代码包含了计算稀疏度和密度的两个函数。这里面的GICP相关的值都是上一帧的结果传入的。computeSpaciousness函数通过计算点云数据中点到原点的距离,求出其中位数,并对其进行平滑处理,最终将结果存入metrics.spaciousness中。而computeDensity函数则根据是否完成GICP优化,来获取机器人当前的密度值,并同样进行平滑处理,最终将结果存入metrics.density中。这两个指标都是用于评价机器人定位精度的重要指标。
void dlio::OdomNode::computeMetrics() {
this->computeSpaciousness(); //计算稀疏度
this->computeDensity(); //计算密度
}
void dlio::OdomNode::computeSpaciousness() {
// 计算点的范围
std::vector<float> ds;
for (int i = 0; i <= this->original_scan->points.size();
i++) { //根据getScanFromROS拿到的原始点云数据
float d = std::sqrt(
pow(this->original_scan->points[i].x, 2) +
pow(this->original_scan->points[i].y, 2)); //计算点到原点的距离
ds.push_back(d); //将距离存入ds
}
// 求中值
std::nth_element(
ds.begin(), ds.begin() + ds.size() / 2,
ds.end()); // 用于在一个序列中找到第k小的元素,其中k由第二个参数指定
float median_curr = ds[ds.size() / 2]; //对应的中值的索引
static float median_prev = median_curr; //存入到上一个时刻的中值
float median_lpf =
0.95 * median_prev + 0.05 * median_curr; //?算出来还是一个值
median_prev = median_lpf; //同理
// push
this->metrics.spaciousness.push_back(median_lpf);
}
void dlio::OdomNode::computeDensity() {
float density;
if (!this->geo
.first_opt_done) { //如果第一次优化未完成(没有完成GICP),则认为没有密度
density = 0.;
} else {
density = this->gicp.source_density_; //将GICP累计的density传入
}
static float density_prev = density;
float density_lpf = 0.95 * density_prev + 0.05 * density;
density_prev = density_lpf;
this->metrics.density.push_back(density_lpf);
}
2. buildSubmap—构建子图
构建子地图的函数。子地图是由一组关键帧组成的,这些关键帧被用于后续的GICP匹配。该函数首先计算当前姿态与关键帧集合中的姿态之间的距离,以确定哪些关键帧应该被包含在子地图中。然后,它使用计算出的距离来获取前K个最近邻关键帧姿态的索引。接下来,它获取凸包索引,即提取一些不必要的关键帧。然后,它获取凸包上每个关键帧之间的距离,并获取凸包的前k个最近邻的索引。接下来,它获取凹包索引,即提取一些不必要的关键帧。然后,它获取凹包上每个关键帧之间的距离,并获取凹包的前k个最近邻的索引。最后,它连接所有子地图的点云和法向量,重新初始化子地图云和法线,并将当前子地图云赋值给子地图的点云,将当前子地图的法向量赋值给子地图的法向量,将子地图的点云赋值给gicp_temp的目标点云,将gicp_temp的目标点云的kd树赋值给子地图的kd树,将当前帧的索引赋值给上一帧的索引。如果子地图与上一次迭代时发生了变化,则将标志位置为true。并将相应的值传给GICP
void dlio::OdomNode::buildSubmap(State vehicle_state) {
// 清除用于子地图的关键帧索引向量
this->submap_kf_idx_curr.clear();
// 计算当前姿态与关键帧集合中的姿态之间的距离
std::unique_lock<decltype(this->keyframes_mutex)> lock(
this->keyframes_mutex); //通过decltype关键字可以获得变量的类型,并加上互斥锁
std::vector<float> ds; //用于存储当前帧与关键帧之间的距离
std::vector<int> keyframe_nn; //用于存储当前帧与关键帧之间的索引
for (int i = 0; i < this->num_processed_keyframes;
i++) { //获取当前时刻所有的关键帧
float d =
sqrt(pow(vehicle_state.p[0] - this->keyframes[i].first.first[0], 2) +
pow(vehicle_state.p[1] - this->keyframes[i].first.first[1], 2) +
pow(vehicle_state.p[2] - this->keyframes[i].first.first[2],
2)); //计算当前帧与关键帧之间的距离
ds.push_back(d); //将距离存入ds
keyframe_nn.push_back(i); //将索引存入keyframe_nn
}
lock.unlock();
// 获取前K个最近邻关键帧姿态的索引
this->pushSubmapIndices(ds, this->submap_knn_, keyframe_nn);
// 获取凸包索引,其实就是提取一些不必要的关键帧
this->computeConvexHull();
// 获取凸包上每个关键帧之间的距离
std::vector<float> convex_ds;
for (const auto &c : this->keyframe_convex) {
convex_ds.push_back(ds[c]); //根据对应的索引将结果压入
}
// 获取凸包的前k个最近邻的索引
this->pushSubmapIndices(convex_ds, this->submap_kcv_, this->keyframe_convex);
// 获取凹包索引,其实就是提取一些不必要的关键帧
this->computeConcaveHull();
// 获取凸包上每个关键帧之间的距离
std::vector<float> concave_ds;
for (const auto &c : this->keyframe_concave) {
concave_ds.push_back(ds[c]);
}
// 获取凹包的前k个最近邻的索引
this->pushSubmapIndices(concave_ds, this->submap_kcc_,
this->keyframe_concave);
// 连接所有子地图的点云和法向量
std::sort(this->submap_kf_idx_curr.begin(),
this->submap_kf_idx_curr.end()); //对当前帧的索引进行排序
auto last = std::unique(this->submap_kf_idx_curr.begin(),
this->submap_kf_idx_curr.end()); //去除重复的元素
this->submap_kf_idx_curr.erase(
last, this->submap_kf_idx_curr.end()); //删除重复的元素
// 对当前和之前的子地图的索引列表进行排序
std::sort(this->submap_kf_idx_curr.begin(), this->submap_kf_idx_curr.end());
std::sort(this->submap_kf_idx_prev.begin(), this->submap_kf_idx_prev.end());
// 检查子地图是否与上一次迭代时发生了变化
if (this->submap_kf_idx_curr != this->submap_kf_idx_prev) {
this->submap_hasChanged = true; //如果发生了变化,则将标志位置为true
// 暂停以防止从主循环中窃取资源,如果主循环正在运行。
this->pauseSubmapBuildIfNeeded();
// 重新初始化子地图云和法线
pcl::PointCloud<PointType>::Ptr submap_cloud_(
boost::make_shared<pcl::PointCloud<PointType>>());
std::shared_ptr<nano_gicp::CovarianceList> submap_normals_(
std::make_shared<nano_gicp::CovarianceList>());
for (auto k : this->submap_kf_idx_curr) { //遍历当前帧的索引
// 创建当前子地图云
lock.lock();
*submap_cloud_ += *this->keyframes[k].second; //将当前帧的点云压入
lock.unlock();
// 获取相应子地图云点的法向量
submap_normals_->insert(std::end(*submap_normals_),
std::begin(*(this->keyframe_normals[k])),
std::end(*(this->keyframe_normals[k])));
}
this->submap_cloud = submap_cloud_; //将当前帧的点云赋值给子地图的点云
this->submap_normals =
submap_normals_; //将当前帧的法向量赋值给子地图的法向量
// 如果主循环正在运行,请暂停以防止窃取资源
this->pauseSubmapBuildIfNeeded();
this->gicp_temp.setInputTarget(
this->submap_cloud); //将子地图的点云赋值给gicp_temp的目标点云
this->submap_kdtree =
this->gicp_temp
.target_kdtree_; //将gicp_temp的目标点云的kd树赋值给子地图的kd树
this->submap_kf_idx_prev =
this->submap_kf_idx_curr; // 将当前帧的索引赋值给上一帧的索引
}
}
3. buildKeyframesAndSubmap—创建子图点云和法线的创建
下面代码的主要功能是创建子图,包括子图的点云和法线的创建。首先,程序获取未处理的关键帧,然后将其转换到世界坐标系下。在转换过程中,程序使用了关键帧的变换矩阵和点云数据,通过调用pcl库中的transformPointCloud函数,将关键帧点云转换到世界坐标系下。同时,程序也需要更新关键帧的协方差(法向量),将其同样转换到世界坐标系下。
在转换完成后,程序会发布关键帧。这里使用了std::thread创建了一个线程,并将发布关键帧的函数publishKeyframe作为线程函数,同时传递了关键帧数据和时间戳。这样做是为了避免在主循环中窃取资源。
最后,程序会暂停以防止从主循环中窃取资源,并调用buildSubmap函数创建子图。该函数会从当前的车辆状态中获取位姿信息,并将位姿信息与关键帧的点云数据进行匹配,生成子图。在生成子图的过程中,程序会使用nano_gicp库中的函数进行点云配准,以提高子图的精度。
/**
* @brief 通过这个函数完成子图的创建,其中包括了子图的点云和法线的创建
*
* @param vehicle_state 当前的车辆状态
*/
void dlio::OdomNode::buildKeyframesAndSubmap(State vehicle_state) {
// 转换新的关键帧和相关的协方差列表
std::unique_lock<decltype(this->keyframes_mutex)> lock(this->keyframes_mutex);
for (int i = this->num_processed_keyframes; i < this->keyframes.size();
i++) { //遍历未处理的关键帧,并完成局部点云向全局点云的转换
pcl::PointCloud<PointType>::ConstPtr raw_keyframe =
this->keyframes[i].second; //获取关键帧的点云
std::shared_ptr<const nano_gicp::CovarianceList> raw_covariances =
this->keyframe_normals[i]; //获取关键帧的协防差
Eigen::Matrix4f T =
this->keyframe_transformations[i]; //获取关键帧的变换矩阵
lock.unlock();
Eigen::Matrix4d Td =
T.cast<double>(); //将float类型的变换矩阵转换为double类型的变换矩阵
pcl::PointCloud<PointType>::Ptr transformed_keyframe(
boost::make_shared<pcl::PointCloud<PointType>>());
pcl::transformPointCloud(*raw_keyframe, *transformed_keyframe,
T); //将关键帧点云转换到世界坐标系下
std::shared_ptr<nano_gicp::CovarianceList> transformed_covariances(
std::make_shared<nano_gicp::CovarianceList>(
raw_covariances->size())); //创建一个新的协方差列表
std::transform(raw_covariances->begin(), raw_covariances->end(),
transformed_covariances->begin(),
[&Td](Eigen::Matrix4d cov) {
return Td * cov * Td.transpose();
}); //将关键帧的协方差转换到世界坐标系下
++this->num_processed_keyframes; //更新已经处理的关键帧的数量
lock.lock();
this->keyframes[i].second = transformed_keyframe; //更新关键帧的点云
this->keyframe_normals[i] = transformed_covariances; //更新关键帧的协方差(法向量)
this->publish_keyframe_thread =
std::thread(&dlio::OdomNode::publishKeyframe, this, this->keyframes[i],
this->keyframe_timestamps[i]); //发布关键帧
this->publish_keyframe_thread.detach();
}
lock.unlock();
// 暂停以防止从主循环中窃取资源,如果主循环正在运行
this->pauseSubmapBuildIfNeeded();
this->buildSubmap(vehicle_state); //创建子图
}
4. pushSubmapIndices—最近子图索引
下面的函数会检查输入的距离信息是否为空,如果为空则直接返回。然后,函数会维护一个最大堆,最多包含k个元素。堆的元素是距离当前帧的距离。对于每个距离信息,如果堆已经包含k个元素,并且当前距离小于堆顶的元素,则将当前距离压入堆中,并弹出堆顶元素。如果堆的元素个数少于k个,则直接将当前距离压入堆中。
然后,函数获取第k小的元素,即堆顶的元素。接着,函数将所有距离小于或等于第k小元素的元素的对应索引压入一个数组中。
该函数的实现比较简单,但是具有一定的实用性。在SLAM等应用中,需要将当前帧和最近的关键帧进行匹配,从而确定当前帧的位姿。该函数可以帮助我们快速找到距离当前帧最近的关键帧,从而提高匹配的效率。
/**
* @brief 压入子图索引
*
* @param dists 距离信息
* @param k knn的值
* @param frames 对应的索引
*/
void dlio::OdomNode::pushSubmapIndices(std::vector<float> dists, int k,
std::vector<int> frames) {
// 确保dist不为空
if (!dists.size()) {
return;
}
// 维护最多包含 k
// 个元素的最大堆,由于d是当前帧和所有关键帧求得,所以一开始的关键帧应该距离更远,如果处于回环的时候,有可能就达不到K个的要求
std::priority_queue<float> pq;
for (auto d : dists) {
//一直和堆顶比较,如果比堆顶小,就弹出堆顶,压入新的
if (pq.size() >= k && pq.top() > d) {
pq.push(d);
pq.pop();
} else if (pq.size() < k) {
pq.push(d);
}
}
// 获取第k小的元素,它应该在堆的顶部
float kth_element = pq.top();
// 获取所有小于或等于第k小元素的元素进行压入
for (int i = 0; i < dists.size(); ++i) {
if (dists[i] <= kth_element)
this->submap_kf_idx_curr.push_back(frames[i]);
}
}
5. computeConvexHull&computeConcaveHull—凹凸包检测
下面的代码包括了计算凸包和凹包的函数。在SLAM系统中,关键帧是指一些特殊的帧,它们包含有足够多的特征点,可以用来计算相机的位姿。凸包和凹包则是一些形状描述工具,用于描述这些关键帧的空间分布情况。
在计算凸包和凹包时,首先需要将所有处理过的关键帧的位置信息放到一个点云中,然后对这个点云进行凸包或凹包的计算。在计算凸包和凹包时,需要设置至少4个关键帧或5个关键帧。计算出凸包和凹包之后,需要获取凸包或凹包上关键帧的索引,这些索引可以用于后续的位姿优化和地图构建。
凸包和凹包的计算是SLAM系统中的重要步骤,它们可以用于约束雷达的位姿,同时也可以提供关键帧的空间分布信息,用于后续的地图构建和路径规划。
void dlio::OdomNode::computeConvexHull() {
// 至少需要4个凸包关键帧
if (this->num_processed_keyframes < 4) {
return;
}
// 创建一个点云,在关键帧处放置点
pcl::PointCloud<PointType>::Ptr cloud = pcl::PointCloud<PointType>::Ptr(
boost::make_shared<pcl::PointCloud<PointType>>());
std::unique_lock<decltype(this->keyframes_mutex)> lock(this->keyframes_mutex);
for (int i = 0; i < this->num_processed_keyframes; i++) {
PointType pt;
pt.x = this->keyframes[i].first.first[0]; //关键帧的位置
pt.y = this->keyframes[i].first.first[1];
pt.z = this->keyframes[i].first.first[2];
cloud->push_back(pt);
}
lock.unlock();
// 计算关键帧的凸包
this->convex_hull.setInputCloud(cloud);
// 获取凸包上关键帧的索引
pcl::PointCloud<PointType>::Ptr convex_points =
pcl::PointCloud<PointType>::Ptr(
boost::make_shared<pcl::PointCloud<PointType>>());
this->convex_hull.reconstruct(
*convex_points); // 通过传入的点集来重新构建当前对象中的凸包
pcl::PointIndices::Ptr convex_hull_point_idx =
pcl::PointIndices::Ptr(boost::make_shared<pcl::PointIndices>());
this->convex_hull.getHullPointIndices(
*convex_hull_point_idx); //获取凸包上的点的索引,这个索引是在cloud中的索引
this->keyframe_convex.clear();
for (int i = 0; i < convex_hull_point_idx->indices.size(); ++i) {
this->keyframe_convex.push_back(
convex_hull_point_idx->indices[i]); //然后压入到keyframe_convex
}
}
void dlio::OdomNode::computeConcaveHull() {
// 至少需要4个凹包关键帧
if (this->num_processed_keyframes < 5) {
return;
}
// 创建一个点云,在关键帧处放置点
pcl::PointCloud<PointType>::Ptr cloud = pcl::PointCloud<PointType>::Ptr(
boost::make_shared<pcl::PointCloud<PointType>>());
std::unique_lock<decltype(this->keyframes_mutex)> lock(this->keyframes_mutex);
for (int i = 0; i < this->num_processed_keyframes; i++) {
PointType pt;
pt.x = this->keyframes[i].first.first[0]; //关键帧的位置
pt.y = this->keyframes[i].first.first[1];
pt.z = this->keyframes[i].first.first[2];
cloud->push_back(pt);
}
lock.unlock();
// 计算关键帧的凹包
this->concave_hull.setInputCloud(cloud);
// 获取凹包上关键帧的索引
pcl::PointCloud<PointType>::Ptr concave_points =
pcl::PointCloud<PointType>::Ptr(
boost::make_shared<pcl::PointCloud<PointType>>());
this->concave_hull.reconstruct(
*concave_points); // 通过传入的点集来重新构建当前对象中的凹包
pcl::PointIndices::Ptr concave_hull_point_idx =
pcl::PointIndices::Ptr(boost::make_shared<pcl::PointIndices>());
this->concave_hull.getHullPointIndices(
*concave_hull_point_idx); //获取凹包上的点的索引,这个索引是在cloud中的索引
this->keyframe_concave.clear();
for (int i = 0; i < concave_hull_point_idx->indices.size(); ++i) {
this->keyframe_concave.push_back(
concave_hull_point_idx->indices[i]); //然后压入到keyframe_concave
}
}
评论(0)
您还未登录,请登录后发表或查看评论