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 key
void SCManager::makeAndSaveScancontextAndKeys( pcl::PointCloud<SCPointType> & _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<float> 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 context
MatrixXd SCManager::makeScancontext( pcl::PointCloud<SCPointType> & _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 planeint 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 0desc(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,Nx1
MatrixXd 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,1xM
MatrixXd 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<int, float> SCManager::detectLoopClosureID ( void );/*** 计算两个scan context之间的距离
*/
std::pair<double, int> SCManager::distanceBtnScanContext( MatrixXd &_sc1, MatrixXd &_sc2 )
{// 1. fast align using variant key (not in original IROS18)// 计算sector Key,也就是sector最大高度均值组成的数组,1xNMatrixXd 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<int> 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

Scan Context 学习记录相关推荐

  1. (回环检测)Scan Context++: Structural Place Recognition Robust to Rotation and Lateral Variations in Urba

    论文创新基本和之前一致,只是比之前表达更加规范化,此外添加了很多有趣实验验证,没有余力可以不看这篇论文,之前那篇看完就够了 摘要 位置识别是机器人导航中的一个关键模块.现有的研究路线主要集中在视觉位置 ...

  2. ASP.NETCore学习记录(一)

    ASP.NETCore学习记录(一) asp.net core介绍  Startup.cs  ConfigureServices  Configure  0. ASP.NETCore 介绍 ASP.N ...

  3. 架构与设计 之一 C 嵌入式设计模式(Design Patterns for Embedded Systems in C)的学习记录

    唉   时至今日,已经不知道在嵌入式的道路上到底挣扎了多少个岁月,总感觉要"病入膏肓"了.此间总是不时出现一些疑惑:人家搞 Java.搞 C# 的动不动就是什么架构 / 框架的,搞 ...

  4. Android socket 学习记录 之 执行new socket(ip, port)程序崩溃

    这段时间在学习Android的socket编程,我不是专做APP的,做的是bootloader.驱动.hal.framework这个线的,也就是系统搭建和功能优化设计.为了打通这整条线,为此学习了不少 ...

  5. seq2seq模型_Pytorch学习记录-Seq2Seq模型对比

    Pytorch学习记录-torchtext和Pytorch的实例4 0. PyTorch Seq2Seq项目介绍 在完成基本的torchtext之后,找到了这个教程,<基于Pytorch和tor ...

  6. 【转】BLE 学习记录

    原文网址:http://m.blog.csdn.net/blog/chiooo/43985401 BLE 学习记录 ANROID BLE 开发,基于 bluetoothlegatt 分析 mBluet ...

  7. Shiro学习记录(详细)

    文章目录 Shiro学习记录 shiro核心组件 Spring Boot 整合 Shiro Shiro 整合 Thymeleaf Shiro学习记录 什么是 Shiro 官网:http://shiro ...

  8. Spring学习记录(九)---通过工厂方法配置bean

    1. 使用静态工厂方法创建Bean,用到一个工厂类 例子:一个Car类,有brand和price属性. 1 package com.guigu.spring.factory; 2 3 public c ...

  9. eclipse maven配置_Gradle学习记录015 声明仓库,检查依赖,管理依赖的配置

    本片由三部分组成,第一部分声明仓库的参考链接如下: https://docs.gradle.org/current/userguide/repository_types.html 第二部分检查依赖的依 ...

  10. Java设计模式学习记录-解释器模式

    前言 这次介绍另一个行为模式,解释器模式,都说解释器模式用的少,其实只是我们在日常的开发中用的少,但是一些开源框架中还是能见到它的影子,例如:spring的spEL表达式在解析时就用到了解释器模式,以 ...

最新文章

  1. MySQL分布式事务(XA事务)
  2. 最简单的Sublime插件开发教程
  3. JavaScript中HTML的DOM
  4. 班尼机器人怎样下载_我的超级助手——G7算账机器人
  5. [概述] SAP增强基本含义及分类
  6. OpenGL 深度测试depth test 的实例
  7. ORA-01795: 列表中的最大表达式数为 1000
  8. 纯虚函数的类为什么不能被实例化
  9. 解决前端跨域的八种方案
  10. Gauss消去法、列主元素消去法及LU分解法的MATLAB实现
  11. php mysql 博客,PHP操作MySQL
  12. Win10专业工作站版本激活并获得数字权利的方法和密钥
  13. 如何阅读看懂datasheet
  14. MOOC创新创业学第十二章单元测试题及答案
  15. Eclipse如何调试代码
  16. linux 源码搭建lnmp_LINUX CENTOS 6.5下源码搭建LNMP
  17. 搭建一个vue小页面
  18. C++计算带利息存款
  19. redis主从+哨兵
  20. 04 现实生活中,你应该如何套利?

热门文章

  1. 流式传输 android,通过RTSP将视频流式传输至Android
  2. Class和class? 类对象和类的对象? 一篇文章让你摸到头脑
  3. JavaScript总结(二:基础知识)
  4. HTML文档的基本结构
  5. 如何修改计算机的ip地址在哪里设置路由器,win7系统电脑无线路由器IP地址如何修改?电脑修改路由器IP地址的方法...
  6. 2020程序员工资排行:腾讯阿里全部落榜,字节跳动高薪实锤
  7. 【ArcGIS小技巧视频教程】(5):在ArcGIS中挂接其他数据
  8. 从面试官的角度聊聊培训班对程序员的帮助,同时给培训班出身的程序员一些建议
  9. html js 获取this,JavaScript中的this引用(推荐)
  10. 飞阳物联平台,智能开关2.0版本支持凌动开关