常见的点云邻域查找方法包括:K-NN 查找和Radius-NN 查找。两者的区别主要在于:
- K-NN查找是给定待查找点 q,寻找点集距离 q 点最近的 k 个点;
- Radius-NN查找是给定待查找点 q,以及查找半径 r,寻找 q 点附近半径为 r 距离内的所有点;
1. KD-Tree的近邻查找
KD-Tree 近邻查找的基本思想:先确认待查找数据点所在的子节点后,根据目标不断向相邻子节点扩展,直到找到满足要求的近邻点数据后结束查找。而且KD-Tree 本身是一个带有约束条件的二分查找树,不需要计算所有数据点和待查找点之间的距离,可以提高查找效率。
1.1 K-NN查找
查找方法:给定待查找点 q,寻找点集距离 q 点最近的 k 个点。
查找流程:
- 从根节点开始,查找 q 点所在的叶子节点;
- 判断叶子节点内的点是否满足查找目标,若满足,则将查找到的 k 个点记录下来;否则,继续扩大查找范围,寻找相邻的叶子节点;
- 直到查找到满足要求的 k 个点后结束。
对于上面流程中的第2点,如何扩大查找范围呢?
这里用到了KD-Tree当前子节点的切分值以及查找过程中的最远查找距离。最远查找距离(w)表示已经查找到的 q 的近邻点中距离第 k 大的值。对于非叶子节点:
- 如果 待查点 q 在该维度上的值小于/大于 当前节点的切分值,则需要到当前节点的左节点/右节点继续查找;
- 进一步,如果 待查点 q 到子节点切分值的距离小于最远查找距离,则说明相邻子节点中可能存在距离 待查点 q 更近的点,需要查找该节点的另一部分子树;
1.2 Radius-NN查找
相比于前面的 K-NN 查找,Radius-NN取消了查找数量 K 的约束,同时对查找过程中的最远查找距离(w)进行了约束,因此两种方法的查找过程类似。
1.3 代码练习
#pragma once
#include "KD_Tree.h"
#include "KNN_result.h"
class KDTreeAVLNearestNeighbors {
public:
bool set_data(const Eigen::MatrixXd& input_matrix, int leaf_size);
bool KNN_search_number(const Eigen::MatrixXd& key, KNNResultNumber& knn_result);
bool KNN_search_radius(const Eigen::MatrixXd& key, KNNResultRadius& knn_result);
private:
bool KNN_search_number(const std::shared_ptr<PCDTREE::KDTreeNode>& root,
const Eigen::MatrixXd& key, KNNResultNumber& knn_result);
bool KNN_search_radius(const std::shared_ptr<PCDTREE::KDTreeNode>& root,
const Eigen::MatrixXd& key, KNNResultRadius& knn_result);
private:
Eigen::MatrixXd _input_matrix;
PCDTREE::KDTree _kd_tree;
};
#include "nearest_neighbors.h"
// kd-tree的近邻查找部分
bool KDTreeAVLNearestNeighbors::set_data(const Eigen::MatrixXd& input_matrix, int leaf_size){
_kd_tree.input(input_matrix);
_input_matrix = input_matrix;
return _kd_tree.create_kd_tree(leaf_size);
}
bool KDTreeAVLNearestNeighbors::KNN_search_number(const Eigen::MatrixXd& key, KNNResultNumber& knn_result){
return KNN_search_number(_kd_tree.get_root(), key, knn_result);
}
bool KDTreeAVLNearestNeighbors::KNN_search_radius(const Eigen::MatrixXd& key, KNNResultRadius& knn_result){
return KNN_search_radius(_kd_tree.get_root(), key, knn_result);
}
bool KDTreeAVLNearestNeighbors::KNN_search_number(const std::shared_ptr<PCDTREE::KDTreeNode>& root,
const Eigen::MatrixXd& key, KNNResultNumber& knn_result){
// 判断当前节点是否为空
if(root == nullptr){
return false;
}
// 判断当前节点是否为叶子节点
if(root->is_leaf){
// 遍历判断每个点到key的距离
for (size_t i = 0; i < root->value_indices.size(); ++i){
Eigen::MatrixXd vdiff = key.col(0) - _input_matrix.col(root->value_indices[i]);
double diff = vdiff.norm();
knn_result.add_result(diff, root->value_indices[i]);
}
return true;
}
// 正常判断该节点的左右子节点
if(key(root->axis, 0) <= root->key){
KNN_search_number(root->left, key, knn_result);
// 判断最远查找距离是否超过root分割线
if(abs(key(root->axis, 0) - root->key) < knn_result.worst_distance()){
KNN_search_number(root->right, key, knn_result);
}
}else{
KNN_search_number(root->right, key, knn_result);
if(abs(key(root->axis, 0) - root->key) < knn_result.worst_distance()){
KNN_search_number(root->left, key, knn_result);
}
}
return true;
}
bool KDTreeAVLNearestNeighbors::KNN_search_radius(const std::shared_ptr<PCDTREE::KDTreeNode>& root,
const Eigen::MatrixXd& key, KNNResultRadius& knn_result){
// 判断当前节点是否为空
if(root == nullptr){
return false;
}
// 判断当前节点是否为叶子节点
if(root->is_leaf){
// 遍历判断每个点到key的距离
for(size_t i = 0; i < root->value_indices.size(); ++i){
Eigen::Vector3d vdiff = key.col(0) - _input_matrix.col(root->value_indices[i]);
double diff = vdiff.norm();
knn_result.add_result(diff, root->value_indices[i]);
}
return true;
}
// 正常判断该节点的左右子节点
if(key(root->axis, 0) < root->key) {
KNN_search_radius(root->left, key, knn_result);
// 判断最远查找距离是否超过root分割线
if(abs((key(root->axis) - root->key)) < knn_result.worst_distance()){
KNN_search_radius(root->right, key, knn_result);
}
}else{
KNN_search_radius(root->right, key, knn_result);
if(abs(key(root->axis, 0) - root->key) < knn_result.worst_distance()){
KNN_search_radius(root->left, key, knn_result);
}
}
return true;
}
2. Octree的近邻查找
Octree 近邻查找的基本思想:先确认待查找数据点所在的子节点后,根据目标不断向相邻子节点扩展,直到找到满足要求的近邻点数据后结束查找。因每个节点都是一个小的立方体,而待查找点 q 邻域是一个球体,扩展搜索也就涉及到球和立方体之间位置关系的判断,如不相交、部分相交、包含等。
2.1 位置关系判断
球和立方体的空间位置关系主要可以分为包含、不相交、部分相交等情况。
2.1.1 球在立方体内(包含)
这种情况最直观的判断条件就是:立方体中心点到球邻域的最远端的距离有没有超出立方体的边长的一半。从图上看就是 立方体中心点(绿色点)到球体中心(红色点)的距离 加上 球体半径 的长度 是否小于 立方体边长的一半。
// 判断 待查找点邻域 是否被包含于 立方体中
// 输入:待查找点及其邻域半径,需要比较的立方体
bool Octree::inside(const Eigen::Vector3d& query,
const std::shared_ptr<Octant>& octant,
double radius){
Eigen::Vector3d query_offset = (query - octant->center).cwiseAbs();
Eigen::Vector3d possible_space = query_offset + Eigen::Vector3d(radius, radius, radius);
if (possible_space(0) < octant->extent &&
possible_space(1) < octant->extent &&
possible_space(2) < octant->extent) {
return true;
}else{
return false;
}
}
2.1.2 球和立方体相交的情况
主要分为完全不相交(球在立方体外)、部分相交(球和立方体的面相交)、部分相交(球和立方体的顶点/边相交)。
-
球在立方体外
这种情况最直观的判断条件就是:任意维度上 查询点到立方体中心的距离 超过 球邻域半径+立方体边长的一半。 -
球和立方体的面相交
如果我们知道球和立方体之间的距离不是特别远,即排除了两者完全不相交的情况,那么如何判断两者的相交程度呢?
这种情况最直观的判断条件就是:任意两个维度及以上,查询点到立方体中心的距离 小于 立方体边长的一半。(=2 表示球与立方体相切,=3 表示相交) -
球和立方体的顶点/边相交
这种情况最直观的判断条件就是:判断 查询点 到立方体端点的距离 是否小于 球邻域半径。
bool Octree::overlap(const Eigen::Vector3d& query,
const std::shared_ptr<Octant>& octant,
double radius){
Eigen::Vector3d query_offset = (query - octant->center).cwiseAbs();
double max_dist = octant->extent + radius;
// case 1: completely outside
// 球和体距离太远:任意维度 查询点到体中心的距离 超过 球邻域半径+体半径
for (int i = 0; i < 3; ++i){
if(query_offset(i) > max_dist){
return false;
}
}
// case 2: contacting the face of the octant
// 球和面的接触: 任意两个维度 查询点到体中心的距离 小于 体半径
Eigen::Vector3d is_contact_face;
for (int i = 0; i < 3; ++i){
is_contact_face(i) = query_offset(i) < octant->extent ? 1 : 0;
}
if (is_contact_face.sum() >= 2) { // =2 表示query ball与立方体相贴,=3 表示相交
return true;
}
// case 3: query ball is contacting the edge or corner of octant
// 球和顶点/边的接触 判断 查询点到体端点的距离 是否小于 球邻域半径
double x_diff = std::max(query_offset(0) - octant->extent, 0.);
double y_diff = std::max(query_offset(1) - octant->extent, 0.);
double z_diff = std::max(query_offset(2) - octant->extent, 0.);
return x_diff * x_diff + y_diff * y_diff + z_diff * z_diff < radius * radius;
}
2.2 K-NN查找
查找方法:给定待查找点 q,寻找点集距离 q 点最近的 k 个点。
查找流程:
- 从根节点开始,查找 q 点所在的叶子节点;
- 判断叶子节点内的点是否满足查找目标,若满足,则将查找到的 k 个点记录下来;否则,继续扩大查找范围,寻找相邻的叶子节点;
- 直到查找到满足要求的 k 个点后结束。
Octree扩大查找判断的方式:如果查找点所在的立方体无法满足查找目标,那么就会遍历查找相邻的7个子节点;如果能够找到满足条件的近邻点,则结束查找,否则继续查找上一级节点,直到找到可以包含 查找点球邻域 的立方体为止。
2.3 Radius-NN查找
相比于前面的 K-NN 查找,Radius-NN取消了查找数量 K 的约束,同时对查找过程中的最远查找距离(w)进行了约束,因此其查找核心在于如何找到包含 查找点球邻域 的最小立方体。
2.4 代码练习
#pragma once
#include "Octree.h"
#include "KNN_result.h"
class OctreeAVLNearestNeighbors {
public:
bool set_data(const Eigen::MatrixXd& input_matrix, int leaf_size, double min_length);
bool KNN_search_number(const Eigen::MatrixXd& key, KNNResultNumber& knn_result);
bool KNN_search_radius(const Eigen::MatrixXd& key, KNNResultRadius& knn_result);
private:
bool KNN_search_number(const std::shared_ptr<PCDTREE::Octant>& root,
const Eigen::MatrixXd& key, KNNResultNumber& knn_result);
bool KNN_search_radius(const std::shared_ptr<PCDTREE::Octant>& root,
const Eigen::MatrixXd& key, KNNResultRadius& knn_result);
private:
Eigen::MatrixXd _input_matrix;
PCDTREE::Octree _octree;
};
#include "nearest_neighbors.h"
bool OctreeAVLNearestNeighbors::set_data(const Eigen::MatrixXd& input_matrix, int leaf_size, double min_length){
_input_matrix = input_maxtix;
_octree.input(_input_matrix);
return _octree.build(leaf_size, min_length);
}
bool OctreeAVLNearestNeighbors::KNN_search_number(const Eigen::MatrixXd& key, KNNResultNumber& knn_result){
return KNN_search_number(_octree.get_root(), key, knn_result);
}
bool OctreeAVLNearestNeighbors::KNN_search_radius(const Eigen::MatrixXd& key, KNNResultRadius& knn_result){
return KNN_search_radius(_octree.get_root(), key, knn_result);
}
bool OctreeAVLNearestNeighbors::KNN_search_number(
const std::shared_ptr<Octant>& root,
const Eigen::MatrixXd& key,
KNNResultNumber& knn_result){
// 判断当前节点是否为空
if(root == nullptr){
return false;
}
// 判断当前是否是叶子节点
if(root->is_leaf && root->value_indices.size() > 0){
// 将key与叶子节点内的所有点进行比较
for(size_t i = 0; i < root->value_indices.size(); ++i){
Eigen::Vector3d vdiff = key.col(0) - _input_matrix.col(root->value_indices[i]);
double diff = vdiff.norm();
knn_result.add_result(diff, root->value_indices[i]);
}
return Octree::inside(key.col(0), root, knn_result.worst_distance());
}
// 计算key所在的子节点
u_char morton_mode = 0;
if(key.col(0)(0) > root->center(0)) morton_mode |= 1;
if(key.col(0)(1) > root->center(1)) morton_mode |= 2;
if(key.col(0)(2) > root->center(2)) morton_mode |= 4;
if(KNN_search_number(root->children[int(morton_mode)], key, knn_result){
return true;
}
// 检查其他子节点
for(int i = 0; i < 8; ++i){
// 遇到key所在的子节点或者空的子节点,跳过
if(i == int(morton_mode) || root->children[i] == nullptr){
continue;
}
// 遇到与key邻域不相交的子节点,跳过
if(!Octree::overlap(key.col(0), root->children[i], knn_result.worst_distance())){
continue;
}
// 正常查找key的邻域点
if(KNN_search_number(root->children[i], key, knn_result){
return true;
}
}
// 检查搜索是否可以终止,即是否找到了 包含 key所在球邻域 的立方体
return Octree::inside(key.col(0), root, knn_result.worst_distance());
}
bool OctreeAVLNearestNeighbors::KNN_search_radius(
const std::shared_ptr<Octant>& root,
const Eigen::MatrixXd& key,
KNNResultNumber& knn_result){
// 判断当前节点是否为空
if(root == nullptr){
return false;
}
// 判断当前节点是否为叶子节点
if(root->is_leaf && root->value_indices.size() > 0){
// 将key与叶子中的所有点比较
for(size_t i = 0; i < root->value_indices.size(); ++i){
Eigen::Vector3d vdiff = key.col(0) - _input_matrix.col(root->value_indices[i]);
double diff = vdiff.norm();
knn_result.add_result(diff, root->value_indices[i]);
}
return Octree::inside(key.col(0), root, knn_result.worst_distance());
}
// 计算key所在的子节点并查找
u_char morton_mode = 0;
if(key.col(0)(0) > root->center(0)) morton_mode |= 1;
if(key.col(0)(1) > root->center(1)) morton_mode |= 2;
if(key.col(0)(2) > root->center(2)) morton_mode |= 4;
if(KNN_search_radius(root->children[int(morton_mode)], key, knn_result.worst_distance()){
return true;
}
// 遍历判断其他子节点
for(int i = 0; i < 8; ++i){
// 遇到key所在子节点或者当前子节点为空时,跳过
if(i == int(morton_mode) || root->children[i] == nullptr){
continue;
}
// 遇到 与key查找邻域不相交 的子节点时,跳过
if(!Octree::overlap(key.col(0), root->children[i], knn_result.worst_distance())){
continue;
}
// 正常处理其他节点
if(KNN_search_radius(root->children[i], key, knn_result.worst_distance())){
return true;
}
}
// 检查搜索是否可以终止,即是否找到了 包含 key所在球邻域 的立方体
return Octree::inside(key.col(0), root, knn_result.worst_distance());
}
声明:以上公式和图片来自课程上的PPT部分,部分参考借鉴了其他博主,仅作为学习、交流使用。