Scan Context / Scan Context ++ 论文和源码阅读

Title: Scan Context / Scan Context ++ 论文和源码阅读

文章目录

  • 前言
  • I. Scan Context 的构造
    • A. 论文内容
    • B. 源码内容
  • 2. Ring Key/Retrieval Key 的计算
    • A. Ring Key
    • B. Retrieval Key
  • 3. KD 树搜索 —— 第一阶段搜索
    • A. KD 树构建
    • B. KD 树搜索
  • 4. Scan Context 距离搜索 —— 第二阶段搜索
    • A. 帧间距离搜索
    • B. 角度偏移粗搜索
    • C. 位置重访精搜索
  • 总结和讨论
  • 参考文献


前言

Scan Context/Scan Context ++ 论文[1], [2]和源码[3]理解起来并不难, 但还是记录一下以备忘.


I. Scan Context 的构造

A. 论文内容

Scan Context 用于激光点云的回环检测或者重访识别. 该方法从特殊的空间分割与点云处理开始:

以机器人/激光雷达为中心, 将周围区域以极坐标形式划分为径向 N r = 20 N_r=20 Nr=20 等分和周向 N s = 60 N_s=60 Ns=60 等分, 并限制传感器的在径向的最大有效扫描范围为 L max ⁡ L_{\max} Lmax.

这样划分形成了一个一个区域称为 bin (水桶), 属于径向第 i ∈ { 1 , 2 , … , N r } i\in \{1,2,\dots,N_r\} i{1,2,,Nr} 圈和周向第 j ∈ { 1 , 2 , … , N s } j \in \{1,2,\dots,N_s\} j{1,2,,Ns} 扇区的 Bin 的指标为 { i j } \{ij\} {ij}. 每个有效扫描点属于一个 bin. 落到 bin { i j } \{ij\} {ij} 中扫描点集合记为 P i j \mathcal{P}_{ij} Pij. 每个水桶都映射为一个值, 记为 ϕ ( P i j ) \phi(\mathcal{P}_{ij}) ϕ(Pij), 为所有落入该水桶中点的最高值 (pt.z 最大值).

根据这种空间划分和映射关系, 由每帧扫描点云就产生了 N r × N s N_r \times N_s Nr×Ns 矩阵, 这个矩阵就是这一帧扫描对应的 Scan Context (下面简记为 SC).

本质上, 这个 Bin (水桶) 是对稠密点云的粗颗粒化, 为一个降采样过程.


B. 源码内容

“cpp/module/Scancontext/Scancontext.cpp” 中 C++ 源码如下[3]. 注意和论文中理论描述不同, 程序中编号是从 0 开始的.

MatrixXd SCManager::makeScancontext( pcl::PointCloud<SCPointType> & _scan_down )
{TicToc t_making_desc;int num_pts_scan_down = _scan_down.points.size();// mainconst int NO_POINT = -1000;MatrixXd desc = NO_POINT * MatrixXd::Ones(PC_NUM_RING, PC_NUM_SECTOR);  // 初始化 水桶 BinSCPointType 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;pt.z = _scan_down.points[pt_idx].z + LIDAR_HEIGHT; // naive adding is ok (all points should be > 0).// 保证 pt.z > 0, 也就是以地平面为 0// xyz to ring, sectorazim_range = sqrt(pt.x * pt.x + pt.y * pt.y);   // 扫描点的径向坐标azim_angle = xy2theta(pt.x, pt.y);              // 扫描点的周向坐标// if range is out of roi, passif( azim_range > PC_MAX_RADIUS )    // 判断是否有效扫描点continue;// 确定点所落水桶的极坐标指标 (径向指标和周向指标)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 );      // 点落到某水桶中, 对应水桶的周向指标// taking maximum 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 )   // 没有点落进来的水桶的值保持为初始化值, 需设为 0desc(row_idx, col_idx) = 0;t_making_desc.toc("PolarContext making");return desc;
} // SCManager::makeScancontext

2. Ring Key/Retrieval Key 的计算

根据上述 Scan Context 的定义, 就可以构建历史扫描点云的 SC 以及当前扫描点云的 SC. 要知道是否重新到访了历史到访过的地点 (loop closure), 就要将当前 SC 和历史上的各个 SC 进行比较. 但是这就成暴力搜索了, 随着历史扫描的累计, 就非常耗计算资源而且慢.

原论文中就采用了两阶段启发式搜索方法来减小计算量. 其中第一段就是构建更浓缩的 Ring Key/Retrieval Key 键值来进行粗略的搜索与定位.


A. Ring Key

在论文 “Scan Context”[1] 中称为 Ring Key. 已知 Scan Context 构建的是划分了径向 N r N_r Nr 圈, 每圈又可以细分为周向分布的 N s N_s Ns 个水桶 (Bins), 每个水桶对应一个编码值. Ring Key 试图把每一圈压缩为一个元素. 极坐标每圈上的 N s N_s Ns 个编码值组成了 SC 矩阵的行向量, 用该行向量 L 0 L0 L0 范数作为 Ring Key 的一个元素 ( L 0 L0 L0 范数就是向量中非零元素的数量). 一帧扫描数据划分为 N r N_r Nr 圈, 这样每帧扫描对应一个 N r N_r Nr 维的向量 Ring Key. 由原来 SC 矩阵的搜索匹配降维到向量 Ring Key 的搜索匹配, 计算量就大大降低了.

如 “fast_evaluator/src/ringkey.m” 中 Matlab 源码:

function [ ring_key ] = ringkey(sc)num_rings = size(sc, 1);ring_key = zeros(1, num_rings);
for ith=1:num_rings   %%% 每一行即极坐标中的每一圈 ringith_ring = sc(ith,:);
%     ring_key(ith) = mean(ith_ring);ring_key(ith) = nnz(ith_ring);
endend

B. Retrieval Key

对应于 Ring Key, 在论文 “Scan Context ++”[2] 中称为 Retrieval Key. 其实是类似东西, 只是改成了 L 1 L1 L1 范数除以 N s N_s Ns. 因为每个 bin 的编码值都大于 0 的缘故, 源码中直接算了每一圈各个编码值的平均值作为这个 Retrieval Key 元素.

“cpp/module/Scancontext/Scancontext.cpp” 中 C++ 源码:

MatrixXd SCManager::makeRingkeyFromScancontext( Eigen::MatrixXd &_desc )
{/* * summary: rowwise mean vector*/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);  // 每一行就对应极坐标中的每一圈 ringinvariant_key(row_idx, 0) = curr_row.mean();}return invariant_key;
} // SCManager::makeRingkeyFromScancontext

3. KD 树搜索 —— 第一阶段搜索

A. KD 树构建

以 Ring Key/Retrieval Key 为元素构建 KD 树. 这里最主要的考虑是排除时间上最新的历史帧, 因为当前位置的重新到访是需要隔一段时间的. 源码中对应为 keyframe gap 的设定 (NUM_EXCLUDE_RECENT = 50).

“cpp/module/Scancontext/Scancontext.cpp” 中 C++ 源码:

    /* * step 1: candidates from ringkey tree_*/if( polarcontext_invkeys_mat_.size() < NUM_EXCLUDE_RECENT + 1)  // 事实上 polarcontext_invkeys_mat_ 是按照一帧一帧扫描的先后顺序 push_back 的// 为避免把刚刚扫描的地方当作回环, 需要忽略紧挨着当前帧的历史扫描帧, 通过 keyframe gap (NUM_EXCLUDE_RECENT 帧)实现// 如果历史扫描帧数小于这个 keyframe gap, 就意味刚启动呢, 不用考虑回环,直接返回结果{std::pair<int, float> result {loop_id, 0.0};return result; // Early return }// tree_ reconstruction (not mandatory to make everytime)  // KD 树的构造if( tree_making_period_conter % TREE_MAKING_PERIOD_ == 0) // to save computation cost  // KD 树构造频率, 每隔 TREE_MAKING_PERIOD_ 扫描周期才重构一次 KD 树{TicToc t_tree_construction;polarcontext_invkeys_to_search_.clear();polarcontext_invkeys_to_search_.assign( polarcontext_invkeys_mat_.begin(), polarcontext_invkeys_mat_.end() - NUM_EXCLUDE_RECENT ) ;// 将所有历史帧的 Ring Key 导入用于构建 KD 树// 但是紧挨着当前帧之前的 NUM_EXCLUDE_RECENT 帧需要排除在外, 因为时间上太近了, 不用考虑如此短时间内的回环, 所以不用放到 KD 树中去polarcontext_tree_.reset(); polarcontext_tree_ = std::make_unique<InvKeyTree>(PC_NUM_RING /* dim */, polarcontext_invkeys_to_search_, 10 /* max leaf */ );// tree_ptr_->index->buildIndex(); // inernally called in the constructor of InvKeyTree (for detail, refer the nanoflann and KDtreeVectorOfVectorsAdaptor)// std::make_unique 模板函数, 用于动态分配指定类型的内存, 并返回一个指向分配内存的唯一指针 (std::unique_ptr )// 构造了 InvKeyTree = KDTreeVectorOfVectorsAdaptor< KeyMat, float >, 并返回指向其的指针// 调用完毕接口函数后, KD 树的具体构造执行就交给外部库 nanoflann 处理了 t_tree_construction.toc("Tree construction");}tree_making_period_conter = tree_making_period_conter + 1;

B. KD 树搜索

接着, 可以基于构造的 KD 树利用外部库 nanoflann 进行最近领搜索了.

利用 ring key 找到和当前扫描帧最接近的历史扫描帧若干, 作为候选帧.

程序中设定为共 NUM_CANDIDATES_FROM_TREE = 10 个最近邻候选帧.

    double min_dist = 10000000; // init with somthing largeint nn_align = 0;int nn_idx = 0;// knn searchstd::vector<size_t> candidate_indexes( NUM_CANDIDATES_FROM_TREE ); std::vector<float> out_dists_sqr( NUM_CANDIDATES_FROM_TREE );TicToc t_tree_search;nanoflann::KNNResultSet<float> knnsearch_result( NUM_CANDIDATES_FROM_TREE );knnsearch_result.init( &candidate_indexes[0], &out_dists_sqr[0] );polarcontext_tree_->index->findNeighbors( knnsearch_result, &curr_key[0] /* query */, nanoflann::SearchParams(10) ); // nanoflann 进行 KD 树搜索t_tree_search.toc("Tree search");

4. Scan Context 距离搜索 —— 第二阶段搜索

A. 帧间距离搜索

第二阶段搜索为精准搜索匹配, 从已由 Ring key 粗搜索得到的 NUM_CANDIDATES_FROM_TREE 个候选历史扫描中, 通过 SC 精确距离计算找到和当前帧最近邻的历史扫描帧, 从而判断回环或重访问.

而这个 SC 精确距离搜索又细分为两小步, 具体思路如下.

考虑到同一个地点的重访, 可能扫描时机器人的航向角度改变了 (甚至逆向 180° 重新到访历史位置). 航向角度的改变在 SC 矩阵中的体现为列向量的偏移 (以 360° 为模). 所以 Scan Context 方法为了实现角度不变, 在计算精确计算 SC 距离之前,

- 先通过对候选扫描帧进行角度偏移 (或列偏移), 找出每一个候选扫描帧的粗偏移匹配

- 再以粗偏移匹配为中心向两边扩展角度偏移作为精确搜索的偏移范围, 进行计算获得查询帧 (当前帧) 与候选历史帧之间的精确距离

角度偏移搜索用数学公式描述比语言描述更直达本意
D ( I q , I c ) = min ⁡ n ∈ [ N s ] d ( I q , I n c ) n ∗ = arg ⁡ min ⁡ n ∈ [ N s ] d ( I q , I n c ) (1) \begin{aligned} D(I^{q}, I^{c}) & = \min_{n\in[N_s]} d(I^q, I_n^c)\\ n^{\ast} & = \underset{n\in [N_s]}{\arg \min}\, d(I^q, I_n^c) \end{aligned} \tag{1} D(Iq,Ic)n=n[Ns]mind(Iq,Inc)=n[Ns]argmind(Iq,Inc)(1)
其中

I q I^q Iq 是查询帧 (当前帧) 的 SC 或者 Sector key (下面描述);

I c I^c Ic 是历史帧 (候选帧) 的 SC 或者 Sector key;

I n c I_n^c Inc 是历史帧的 SC 或者 Sector key 以列向量切换 n n n 列后的值;

[ N s ] [N_s] [Ns] 可以取值 { 1 , 2 , … , N s } \{1,2,\ldots,N_s\} {1,2,,Ns}.

无论候选帧进行角度切换或者查询帧进行角度切换都是一样的, 因为角度的相对关系.

角度偏移/列偏移像钟表一样进行, 模为 360°.

重访时除了有角度的偏移, 也有横向位置的偏移. 论文对将横向偏移的处理命名为 root shifting, 将每一帧的扫描数据向两边偏移并拷贝, 再以当前位置及偏移拷贝的点云来构造 SC, 以此来模拟合成机器人以不同车道线到访扫描同一位置. 虽然作者假设了横向偏移不大幅改变扫描点云, 给我的感觉是有点牵强, 当然如果实践中好用的话就自有其道理 (关于横向偏移没找到具体源码).


B. 角度偏移粗搜索

对照 Ring key, 我们对 SC 按列压缩得到 Sector Key, 以 SC 中每列的均值代替列向量. 源码如下

MatrixXd SCManager::makeSectorkeyFromScancontext( Eigen::MatrixXd &_desc )
{/* * summary: columnwise mean vector*/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(); //以 SC 中每列的均值代替列向量}return variant_key;
} // SCManager::makeSectorkeyFromScancontext

角度偏移/列偏移的实现源码:

MatrixXd circshift( MatrixXd &_mat, int _num_shift )
{// shift columns to right direction assert(_num_shift >= 0);if( _num_shift == 0 ){MatrixXd shifted_mat( _mat );  // 列偏移 0, 就是不变return shifted_mat; // Early return }MatrixXd shifted_mat = MatrixXd::Zero( _mat.rows(), _mat.cols() );for ( int col_idx = 0; col_idx < _mat.cols(); col_idx++ ){int new_location = (col_idx + _num_shift) % _mat.cols();  // 列指标的偏移shifted_mat.col(new_location) = _mat.col(col_idx);}return shifted_mat;} // circshift

压缩 SC 的列向信息得到 Sector Key 是为了角度偏移所搜的快速粗略计算. 利用 Sector key 的角度偏移粗搜索匹配的源码:

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++ ){MatrixXd vkey2_shifted = circshift(_vkey2, shift_idx);  // 角度偏移/列偏移MatrixXd vkey_diff = _vkey1 - vkey2_shifted;  // 比较 sector key 的距离double cur_diff_norm = vkey_diff.norm();if( cur_diff_norm < min_veky_diff_norm )  // 找出同一帧不同角度偏移后的 sector key 与查询帧之间的最小距离{argmin_vkey_shift = shift_idx;min_veky_diff_norm = cur_diff_norm;}}return argmin_vkey_shift;  // 最优的角度偏移} // fastAlignUsingVkey

C. 位置重访精搜索

以粗偏移匹配为中心向两边扩展角度偏移量, 作为即将进行的位置重访精准搜索的角度偏移/列偏移的范围, 源码如下:

    // 1. fast align using variant key (not in original IROS18)MatrixXd vkey_sc1 = makeSectorkeyFromScancontext( _sc1 );MatrixXd vkey_sc2 = makeSectorkeyFromScancontext( _sc2 );int argmin_vkey_shift = fastAlignUsingVkey( vkey_sc1, vkey_sc2 );  // 角度偏移/列偏移的粗匹配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());  // 偏移指标范围排序

在做距离搜索前, 先看一下两 SC 之间距离的精准计算公式
d ( I q , I c ) = 1 N s ∑ j = 1 N s ( 1 − c j q ⋅ c j c ∥ c j q ∥ ∥ c j c ∥ ) = 1 − 1 N s ∑ j = 1 N s c j q ⋅ c j c ∥ c j q ∥ ∥ c j c ∥ (2) \begin{aligned} d(I^q, I^c) & = \frac{1}{N_s}\sum_{j=1}^{N_s}\left(1-\frac{c_j^q \cdot c_j^c}{\|c_j^q\| \, \|c_j^c\|}\right)\\ & = 1 - \frac{1}{N_s}\sum_{j=1}^{N_s}\frac{c_j^q \cdot c_j^c}{\|c_j^q\| \, \|c_j^c\|} \end{aligned} \tag{2} d(Iq,Ic)=Ns1j=1Ns(1cjqcjccjqcjc)=1Ns1j=1Nscjqcjccjqcjc(2)
以及实现代码:

double SCManager::distDirectSC ( MatrixXd &_sc1, MatrixXd &_sc2 )
{int num_eff_cols = 0; // i.e., to exclude all-nonzero sectordouble sum_sector_similarity = 0;for ( int col_idx = 0; col_idx < _sc1.cols(); col_idx++ ){VectorXd col_sc1 = _sc1.col(col_idx);VectorXd col_sc2 = _sc2.col(col_idx);if( col_sc1.norm() == 0 | col_sc2.norm() == 0 )continue; // don't count this sector pair. double sector_similarity = col_sc1.dot(col_sc2) / (col_sc1.norm() * col_sc2.norm());sum_sector_similarity = sum_sector_similarity + sector_similarity;num_eff_cols = num_eff_cols + 1;}double sc_sim = sum_sector_similarity / num_eff_cols;return 1.0 - sc_sim;} // distDirectSC

针对一个候选帧计算与查询帧之间的距离计算, 需对候选帧进行角度偏移后进行精准计算匹配, 其中角度偏移的范围以粗偏移匹配为中心向两边扩展得到.

查询帧与候选帧之间的距离为查询帧与该候选帧的角度切换 (偏移) 后的各个数据帧之间的距离值的最小值, 如式 (1) 数学描述.

    // 2. fast columnwise diff int argmin_shift = 0;double min_sc_dist = 10000000;for ( int num_shift: shift_idx_search_space )  // 按照已得到每一候选帧的角度偏移范围进行循环计算{MatrixXd sc2_shifted = circshift(_sc2, num_shift);  // 根据偏移指标, 进行角度偏移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;}}return make_pair(min_sc_dist, argmin_shift);

完成所有候选帧与查询帧之间距离计算后, 再根据查询帧和不同候选帧之间的距离, 从中选出最短的距离.

该最短距离对应的候选帧就与当前帧形成了潜在的回环或位置重访.

最后再做最近邻搜索的门限判定, 如果符合要求就得到了回访地点或者回环.

数学上面表达为
c ∗ = arg ⁡ min ⁡ c k ∈ C D ( I q , I c k ) = arg ⁡ min ⁡ c k ∈ C ( min ⁡ n ∈ [ N s ] d ( I q , I n c ) ) s.t. D < τ (3) c^{\ast} = \underset{c_k \in \mathcal{C}}{\arg\min}\, D(I^q, I^{c_k}) = \underset{c_k \in \mathcal{C}}{\arg\min}\, \left( \min_{n\in[N_s]} d(I^q, I_n^c)\right)\\ \text{s.t.}\; D <\tau \tag{3} c=ckCargminD(Iq,Ick)=ckCargmin(n[Ns]mind(Iq,Inc))s.t.D<τ(3)
其中

C \mathcal{C} C 是候选帧的指标;

c ∗ c^{\ast} c 是与查询帧最近邻的候选帧的指标;

τ \tau τ 最近邻搜索距离的门限 (源码中设为 SC_DIST_THRES = 0.13).

以上计算的源代码:

    /* *  step 2: pairwise distance (find optimal columnwise best-fit using cosine distance)*/TicToc t_calc_dist;   for ( int candidate_iter_idx = 0; candidate_iter_idx < NUM_CANDIDATES_FROM_TREE; candidate_iter_idx++ )  // 所有候选帧循环{MatrixXd polarcontext_candidate = polarcontexts_[ candidate_indexes[candidate_iter_idx] ];  // 抽出某一候选帧进行计算std::pair<double, int> sc_dist_result = distanceBtnScanContext( curr_desc, polarcontext_candidate ); // 计算候选帧与查询帧之间的最短距离以及最佳角度切换double candidate_dist = sc_dist_result.first;  // 候选帧对应的距离int candidate_align = sc_dist_result.second;   // 候选帧对应的角度切换if( candidate_dist < min_dist ) // 选择所有候选帧对应距离中的最短距离{min_dist = candidate_dist;nn_align = candidate_align;nn_idx = candidate_indexes[candidate_iter_idx];  // 候选帧的指标}}t_calc_dist.toc("Distance calc");/* * loop threshold check*/if( min_dist < SC_DIST_THRES )  // 最近邻搜索距离的门限判定{loop_id = nn_idx; // std::cout.precision(3); cout << "[Loop found] Nearest distance: " << min_dist << " btn " << polarcontexts_.size()-1 << " and " << nn_idx << "." << endl;cout << "[Loop found] yaw diff: " << nn_align * PC_UNIT_SECTORANGLE << " deg." << endl;}else{std::cout.precision(3); cout << "[Not loop] Nearest distance: " << min_dist << " btn " << polarcontexts_.size()-1 << " and " << nn_idx << "." << endl;cout << "[Not loop] yaw diff: " << nn_align * PC_UNIT_SECTORANGLE << " deg." << endl;}// To do: return also nn_align (i.e., yaw diff)float yaw_diff_rad = deg2rad(nn_align * PC_UNIT_SECTORANGLE);   // 计算偏移的角度std::pair<int, float> result {loop_id, yaw_diff_rad};return result;

总结和讨论

以上为对 Scan Context 内容的整理笔记.

论文提到了部分点云特征描述子需要依赖法线, 而自动驾驶环境比较杂乱获得稳定地法线比较困难;

论文也提到了点云稀疏程度和扫描距离有关, 而极坐标形式能够很好的兼容这种问题.

认可和赞赏论文的这些描述并得到了较好的结果.

但是框架上或者思路上 Scan Context 和普通的点云特征描述子并无区别, 都是在人为构造适合的特征描述形式.

所以总体上感觉还是将传统点云特征描述子应用于大场景.

当然要有真正的改变应该要用深度学习了.

(不知道是否正确, 如有问题请指出)


参考文献

[1] G. Kim and A. Kim, “Scan Context: Egocentric Spatial Descriptor for Place Recognition Within 3D Point Cloud Map,” 2018 IEEE/RSJ International Conference on Intelligent Robots and Systems (IROS), Madrid, Spain, 2018, pp. 4802-4809

[2] G. Kim, S. Choi and A. Kim, “Scan Context++: Structural Place Recognition Robust to Rotation and Lateral Variations in Urban Environments,” in IEEE Transactions on Robotics, vol. 38, no. 3, pp. 1856-1874, June 2022

[3] Scan Context, https://github.com/irapkaist/scancontext

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/188750.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于mps的pytorch 多实例并行推理

背景 大模型训练好后&#xff0c;进行部署时&#xff0c;发现可使用的显卡容量远大于模型占用空间 。是否可以同时加载多个模型实例到显存空间&#xff0c;且能实现多个实例同时并发执行&#xff1f;本次实验测试基于mps的方案&#xff0c;当请求依次过来时&#xff0c;多个相…

一种LED驱动专用控制电路方案

一、基本的概述 TM1651 是一种带键盘扫描接口的LED&#xff08;发光二极管显示器&#xff09;驱动控制专用电路&#xff0c;内部集成有MCU 数字接口、数据锁存器、LED 高压驱动、键盘扫描等电路。本产品性能优良&#xff0c;质量可靠。采用SOP16/DIP16的封装形式。 二、特性说…

整体迁移SVN仓库到新的windows服务器

一、背景 公司原有的SVN服务器年代比较久远经常出现重启情况&#xff0c;需要把SVN仓库重新迁移到新的服务器上&#xff0c;在网上也搜到过拷贝Repositories文件直接在新服务器覆盖的迁移方案&#xff0c;但考虑到原有的操作系统和现有的操作系统版本不一致&#xff0c;SVN版本…

获取窗口中文标题栏GetWindowText()

GetWindowText(hwnd, lpString, len)会根据传入的第二个参数lpString 判断是ASCII或者Unicode编码选择调用GetWindowTextA(hwnd, lpString, len)&#xff0c;还是GetWindowTextW(hwnd, lpString, len)&#xff0c;所以想获取中文标题直接调用GetWindowTextW() ASCII GetWindowT…

frp 配置内网访问

frp介绍 frp 是一个开源、简洁易用、高性能的内网穿透软件&#xff0c;支持 tcp, udp, http, https 等协议。frp 项目官网是 https://github.com/fatedier/frp 下载地址&#xff1a; https://github.com/fatedier/frp/releases frp工作原理 服务端运行&#xff0c;监听一个…

分治算法——75. 颜色分类

文章目录 &#x1f33f;0. 分治&#x1f33b;1. 题目&#x1f33c;2. 算法原理&#x1f334;3. 代码实现 &#x1f33f;0. 分治 分治分治&#xff0c;顾名思义分而治之&#xff0c;将一个大问题转换成若干个子问题&#xff0c;再将这些子问题的基础上继续划分成更小的子问题&a…

【新手解答3】深入探索 C 语言:头文件提供必要的接口、源文件保持实现细节的私有性 + 进一步学习的方向 + 如何快速编写程序并最终能制作小游戏

C语言的相关问题解答 写在最前面问题1&#xff1a;头文件提供必要的接口、源文件保持实现细节的私有性封装在头文件中的作用源文件中的“封装”总结 问题2&#xff1a;接下来的学习方向问题3&#xff1a;如何快速编写程序并最终能制作小游戏1. 基本编程概念2. 数组和基本算法3.…

PyQt基础_011_对话框类控件QMessage

基本功能 import sys from PyQt5.QtCore import * from PyQt5.QtGui import * from PyQt5.QtWidgets import *class WinForm( QWidget): def __init__(self): super(WinForm,self).__init__() self.setWindowTitle("QMessageBox") self.resize(300, 100) self.myButt…

EG20网口远程下载程序使用案例

EG20网口远程下载程序使用案例 前言&#xff1a;本文档主要说明了使用蓝蜂虚拟网络工具通过EG20网关的网口&#xff08;LAN口&#xff09;远程给PLC下载程序的步骤及其注意事项。使用蓝蜂虚拟网络工具&#xff0c;不仅支持程序的远程下载&#xff0c;同样支持程序的远程上传与…

爬虫源代码

public class Spider implements Runnable {private ArrayList urls; //URL列表private HashMap indexedURLs; //已经检索过的URL列表private int threads ; //初始化线程数 public static void main(String argv[]) throws Exception {if(argv[0] null){System.out.printl…

【SA8295P 源码分析】136 - QNX 如何抓取系统 log 方法 之 网络部分日志抓取方法

【SA8295P 源码分析】136 - QNX 如何抓取系统 log 方法 之 网络部分日志抓取方法 一、slog2info二、获取当前系统网络信息三、tracelogger四、qscan.sh : 用于收集 qnx 文件系统 权限、checksums 等信息系列文章汇总见:《【SA8295P 源码分析】00 - 系列文章链接汇总》 本文链…

【Node.js后端架构:MVC模式】基于expres讲解

Node.js后端架构&#xff1a;MVC模式 什么是MVC MVC (Model-View-Controller) 是一种软件设计模式&#xff0c;用于将应用程序的逻辑分离成三个不同的组件&#xff1a;模型、视图和控制器。 模型&#xff08;Model&#xff09;负责处理应用程序的数据逻辑。它负责从数据库或其…

21、LED点阵屏显示图形动画

c51的sfr、sbit sfr:特殊功能寄存器声明 例如&#xff1a;sfr P00x80 声明P0口寄存器&#xff0c;物理地址为0x80 sbit:特殊位声明 例如&#xff1a;sbit p0_1 0x81; 或 sbit p0_1 p0^1; 声明P0寄存器的第一位 可位寻址、不可位寻址&#xff1a;在单片机系统中&#xff0c;操…

JS事件循环详解

前言 进程 程序运行需要其专属的内存空间&#xff0c;可以把这块内存空间简单理解为进程。 下图每个颜色块都可视为一个应用&#xff0c;每个应用至少应有一个进程&#xff0c;进程之间相互独立&#xff0c;即使想要通信&#xff0c;也需要双方同意。 线程 有了进程后&…

二次元检测设备导轨修复指南

二次元检测设备是一种高精度的测量仪器&#xff0c;用于检测物体表面的形状、尺寸和精度等。直线导轨是二次元检测设备中最重要的组成部分之一&#xff0c;它的精度和稳定性直接影响到设备的测量结果和可靠性&#xff0c;因此&#xff0c;对导轨进行修复和保养是非常重要的。 直…

基于Java SSM移动电源租赁系统

涉及的知识点&#xff1a;Java程序设计基础知识、类的创建、对象的使用、面向对象继承、面向对象多态性、抽象类和接口、集合与泛型、文件与输入输出流操作、异常处理与日志记录、Java GUI 、事件处理、Java数据库编程。 一、实验目的 &#xff08;1&#xff09;掌握Java编程…

Linux删除了大文件为什么磁盘空间没有释放?

某天&#xff0c;收到监控系统的告警信息&#xff0c;说磁盘空间占用过高&#xff0c;登上服务器&#xff0c;使用 df -h 一看&#xff0c;发现磁盘占用率已经 96%了&#xff1a; 通过查看 /usr/local/nginx/conf/vhost/xxx.conf 找到 access_log 和 error_log 的路径&#x…

Android 7.1 点击清空全部按钮清空一切运行进程(包括后台在播音乐)

Android 7.1 点击清空全部按钮清空一切运行进程&#xff08;包括后台在播音乐&#xff09; 近来收到客户反馈说音乐在后台播放时点击“清空全部”按钮后台音乐没有被kill掉&#xff0c;仍在播放&#xff0c;需要在点击“清空全部”后kill掉一切后台运行进程&#xff0c;具体修…

SDX12 上层应用gdb调试及环境搭建

SDX12 上层应用gdb调试及环境搭建 1. 问题背景2 环境搭建3 操作步骤4. 调试过程5 实现效果6 附录 1. 问题背景 上层应用在问题定位的过程中&#xff0c;现有手段只能有有限的log打印&#xff0c;通常情况下很难定位问题。如果想在应用程序执行的过程中查看任意调用关系或者数据…

6.1810: Operating System Engineering <Lab2 syscall: System calls>

课程链接&#xff1a;6.1810 / Fall 2023 一、本节任务 二、要点 操作系统要满足三要素&#xff1a;并发、隔离、交互&#xff08;multiplexing, isolation, and interaction&#xff09;。 宏内核&#xff08;monolithic kernel&#xff09;&#xff1a;是操作系统核心架构…