Scan Context 学习记录
发布日期:2022-02-06 00:27:07 浏览次数:25 分类:技术文章

本文共 7904 字,大约阅读时间需要 26 分钟。

Scan Context 学习记录

知乎上看到一篇有关scan context的文章,感觉内容不错,便进行了学习与转载,文章链接附在最后

scan context 是一篇论文中提出的,通过激光点云做场景识别或者定位,当然也可以用来做闭环检测。定位,通常是在历史帧中找到与搜索帧pose最接近的一帧,当然这只用到了pose。如果用点云去做匹配,找到最相似的那一帧点云,怎么做呢,直接3d-3d匹配是可以的,但是不够快。那么降维,把3D点云变成二维,然后去匹配。scan context的做法就是从3D点云上空俯视,得到一张俯视图,当然图上每个点有高度信息,用这个俯视图去做匹配。

1. 生成scan context

首先看这两张图,两张图一定要结合起来看,包括图的注释和文字。

在这里插入图片描述
在这里插入图片描述
第一幅图是一帧点云的俯视图。黄色的一圈叫做ring(环),从中心点开始,有一个很小的环,慢慢往外扩大,最大到80米的距离,一层一层的环加起来就是整个点云了,一共20个环。青色的那个径向轴叫做sector(扇形),那么很显然这个径向轴划分成了20段,每段对应一个环。一共有60个sector,也就是把360°的ring划分成了60份。

const int    PC_NUM_RING = 20; // 20 in the original paper (IROS 18)const int    PC_NUM_SECTOR = 60; // 60 in the original paper (IROS 18)const double PC_MAX_RADIUS = 80.0; // 80 meter max in the original paper (IROS 18)

那么整个点云就可以用20*60=1200个bin(格子),一个二维矩阵来表示。那么每个格子的值是什么呢?是落在这个格子里面所有点的最大高度(z值)。对照第二幅图像看一下,把俯视的圆环图剪开,变成矩形,横向对应一个环,纵向对应距离从近到远。不同的颜色,表示这个格子中点的最大高度。想象成一个带高度的俯视图,或者地形图。

// scan context是一个二维矩阵MatrixXd desc = NO_POINT * MatrixXd::Ones(PC_NUM_RING, PC_NUM_SECTOR);

上面就把一帧点云用二维的形式表达了,称之为scan context。下面就是算法的第一个接口,根据一帧点云,生成scan context。

// 接口一,一帧点云生成scan context,计算ring key, sector keyvoid SCManager::makeAndSaveScancontextAndKeys( pcl::PointCloud
& _scan_down ){
// scancontext 带高度的俯视图 Eigen::MatrixXd sc = makeScancontext(_scan_down); // v1 // 每个环ring的最大高度平均值 Eigen::MatrixXd ringkey = makeRingkeyFromScancontext( sc ); // 每个轴向sector的最大高度平均值 Eigen::MatrixXd sectorkey = makeSectorkeyFromScancontext( sc ); // 换成数组,这玩意作为scancontext的key,也就是在历史帧里面通过找相同的key来得到候选匹配,然后计算scan context的距离 std::vector
polarcontext_invkey_vec = eig2stdvec( ringkey ); // 历史数据,全部存起来,供后面查找匹配 polarcontexts_.push_back( sc ); polarcontext_invkeys_.push_back( ringkey ); polarcontext_vkeys_.push_back( sectorkey ); polarcontext_invkeys_mat_.push_back( polarcontext_invkey_vec );} // 一帧点云生成scan contextMatrixXd SCManager::makeScancontext( pcl::PointCloud
& _scan_down ){
// 激光点数量 int num_pts_scan_down = _scan_down.points.size(); const int NO_POINT = -1000; // 这里矩阵的记法跟paper的示意图是一致的 MatrixXd desc = NO_POINT * MatrixXd::Ones(PC_NUM_RING, PC_NUM_SECTOR); SCPointType pt; float azim_angle, azim_range; // wihtin 2d plane int ring_idx, sctor_idx; // 遍历激光点 for (int pt_idx = 0; pt_idx < num_pts_scan_down; pt_idx++) {
pt.x = _scan_down.points[pt_idx].x; pt.y = _scan_down.points[pt_idx].y; // 让高度大于0,所有点的高度都加2,不影响匹配结果 pt.z = _scan_down.points[pt_idx].z + LIDAR_HEIGHT; // naive adding is ok (all points should be > 0). // 距离 azim_range = sqrt(pt.x * pt.x + pt.y * pt.y); // 角度,0~360° azim_angle = xy2theta(pt.x, pt.y); // 距离超过80米的点不考虑了 if( azim_range > PC_MAX_RADIUS ) continue; // 计算这个点落在哪个bin里面,下标从1开始数 ring_idx = std::max( std::min( PC_NUM_RING, int(ceil( (azim_range / PC_MAX_RADIUS) * PC_NUM_RING )) ), 1 ); sctor_idx = std::max( std::min( PC_NUM_SECTOR, int(ceil( (azim_angle / 360.0) * PC_NUM_SECTOR )) ), 1 ); // 用z值,也就是高度来更新这个格子,存最大的高度; if ( desc(ring_idx-1, sctor_idx-1) < pt.z ) // -1 means cpp starts from 0 desc(ring_idx-1, sctor_idx-1) = pt.z; // update for taking maximum value at that bin } // reset no points to zero (for cosine dist later) for ( int row_idx = 0; row_idx < desc.rows(); row_idx++ ) for ( int col_idx = 0; col_idx < desc.cols(); col_idx++ ) if( desc(row_idx, col_idx) == NO_POINT ) desc(row_idx, col_idx) = 0; return desc;}

2. 用scan context 搜索匹配

用scan context来表达一帧点云之后,要做的就是用scan context在历史帧里面找到最相近的scan context。为了加速查找,通常都是把历史帧数据构建kd-tree,直接用scan context消耗计算机内存较大(主要是相似度计算耗时)。进一步的,把scan context再降维,得到一个ring key(向量),用这个去构造kd-tree,以及搜索。ring key是scan context中每一个ring的最大高度的均值,然后组合在一起,组成20*1的矩阵。直观上ring key就是20个环的最大高度的均值所组合在一起的矩阵。可以想到ring key跟scan context是一对多的关系。但是没关系,通过ring key做一个初步筛选,找到很多候选scan context,然后再精细比较scan context得到最后的结果。

// 由scan context计算ring key,Nx1MatrixXd SCManager::makeRingkeyFromScancontext( Eigen::MatrixXd &_desc ){
// 每行计算一个均值 Eigen::MatrixXd invariant_key(_desc.rows(), 1); for ( int row_idx = 0; row_idx < _desc.rows(); row_idx++ ) {
Eigen::MatrixXd curr_row = _desc.row(row_idx); invariant_key(row_idx, 0) = curr_row.mean(); } return invariant_key;}

通过上面这个ring key在kd-tree里面找到了最邻近的几帧,有对应的scan context,接下来就要用当前帧的scan context与这些scan context进行比较,计算距离。这里呢,又要引入一个sector key,与ring key对应,这里每列计算一个均值,组成一个1*60的矩阵。

// 由scan context计算sector key,1xMMatrixXd SCManager::makeSectorkeyFromScancontext( Eigen::MatrixXd &_desc ){
// 每列计算一个均值 Eigen::MatrixXd variant_key(1, _desc.cols()); for ( int col_idx = 0; col_idx < _desc.cols(); col_idx++ ) {
Eigen::MatrixXd curr_col = _desc.col(col_idx); variant_key(0, col_idx) = curr_col.mean(); } return variant_key;}

为什么要按列计算均值呢?因为在场景中同一个位置,旋转一下,看向不同的方向,得到scan context是不一样的,对应的scan context上的表现就是矩阵左右偏移了。如果直接用scan context去比,显然就认为两帧是不匹配的,但是实际上是在同一个位置。所以就要对scan context左右循环偏移,找一个最佳的匹配位置,然后用偏移后的scan context去作比较。那么实际上用sector key去计算偏移量,然后施加到scan context上,最后用偏移后的scan context作比较。

// _vkey1、_vkey2是两个sector key// 对_vkey2做循环偏移,计算与_vkey1最佳匹配时的偏移量int SCManager::fastAlignUsingVkey( MatrixXd & _vkey1, MatrixXd & _vkey2){
int argmin_vkey_shift = 0; double min_veky_diff_norm = 10000000; for ( int shift_idx = 0; shift_idx < _vkey1.cols(); shift_idx++ ) {
// 矩阵的列,循环右移shift个单位 MatrixXd vkey2_shifted = circshift(_vkey2, shift_idx); // 直接相减,sector key是1xN的矩阵 MatrixXd vkey_diff = _vkey1 - vkey2_shifted; // 范数 double cur_diff_norm = vkey_diff.norm(); // 记录距离最小时对应的循环偏移量 if( cur_diff_norm < min_veky_diff_norm ) {
argmin_vkey_shift = shift_idx; min_veky_diff_norm = cur_diff_norm; } } return argmin_vkey_shift;}

第二个接口,闭环检测

// 接口二,执行闭环检测std::pair
SCManager::detectLoopClosureID ( void );/** * 计算两个scan context之间的距离*/std::pair
SCManager::distanceBtnScanContext( MatrixXd &_sc1, MatrixXd &_sc2 ){
// 1. fast align using variant key (not in original IROS18) // 计算sector Key,也就是sector最大高度均值组成的数组,1xN MatrixXd vkey_sc1 = makeSectorkeyFromScancontext( _sc1 ); MatrixXd vkey_sc2 = makeSectorkeyFromScancontext( _sc2 ); // 这里将_vkey2循环右移,然后跟_vkey1作比较,找到一个最相似(二者做差最小)的时候,记下循环右移的量 int argmin_vkey_shift = fastAlignUsingVkey( vkey_sc1, vkey_sc2 ); // 上面用sector key匹配,找到一个初始的偏移量,但肯定不是准确的,再在这个偏移量左右扩展一下搜索空间 const int SEARCH_RADIUS = round( 0.5 * SEARCH_RATIO * _sc1.cols() ); // a half of search range std::vector
shift_idx_search_space {
argmin_vkey_shift }; for ( int ii = 1; ii < SEARCH_RADIUS + 1; ii++ ) {
shift_idx_search_space.push_back( (argmin_vkey_shift + ii + _sc1.cols()) % _sc1.cols() ); shift_idx_search_space.push_back( (argmin_vkey_shift - ii + _sc1.cols()) % _sc1.cols() ); } std::sort(shift_idx_search_space.begin(), shift_idx_search_space.end()); // 2. fast columnwise diff int argmin_shift = 0; double min_sc_dist = 10000000; for ( int num_shift: shift_idx_search_space ) {
// 把scan context循环右移一下 MatrixXd sc2_shifted = circshift(_sc2, num_shift); // 计算两个scan context之间的距离 double cur_sc_dist = distDirectSC( _sc1, sc2_shifted ); if( cur_sc_dist < min_sc_dist ) {
argmin_shift = num_shift; min_sc_dist = cur_sc_dist; } } // 最小scan context距离,偏移量 return make_pair(min_sc_dist, argmin_shift);}

小结:

  1. 给定一帧点云,划分成20个环,每个环分成60等份,一共1200个格子
  2. 每个格子存里面点的最大高度值(z值),这样一帧点云就用一个二维图像表示了,想象成一个带高度的俯视图,或者地形图,记为scan context
  3. scan context进一步计算列的均值,得到一个1x60的向量,记为ring key;计算行的均值,得到一个20x1的向量,记为sector key
  4. 用ring key构造kd-tree,并且执行knn搜索
  5. 对于候选匹配scan context,首先要左右循环偏移一下,对齐,实际会用sector key去对齐,得到一个偏移量
  6. 对候选匹配scan context,施加偏移量,然后作比较

文章链接:https://zhuanlan.zhihu.com/p/359523177

转载地址:https://blog.csdn.net/weixin_46181372/article/details/116130105 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:C++ 中的static关键字
下一篇:std::thread(线程) 学习记录

发表评论

最新留言

初次前来,多多关照!
[***.217.46.12]2024年04月05日 17时25分38秒