Nearest Neighbour
1、暴力最近邻搜索
暴力最近邻搜索(Brute-force Nearest Neighbour Search)是最简单直观的最近邻计算方法,无需辅助数据结构。如果只搜索单个最近邻,则算法称为暴力单点最近邻搜索;如果搜索 k k k点最近邻,则算法称为暴力 k k k点近邻搜索,暴力近邻搜索准确率和召回率都为 100 % 100\% 100%,但计算时间过长。
应用暴力 k k k点最近邻搜索方法查找点 x \mathbf{x} x在点集 Y = { y 1 , ⋯ , y n } Y=\{\mathbf{y}_{1},\cdots,\mathbf{y}_{n}\} Y={y1,⋯,yn}中的 k k k点最近邻的步骤:
- 计算 x \mathbf{x} x到点集 Y Y Y中所有点的距离
- 对第1步的结果进行排序,选择距离最短的 k k k个点
2、栅格与体素方法
当查找某点在点集中的单个最近邻时,在二维情况下,可将原始空间划分为正方形栅格(Grid);在三维情况下,可将原始空间划分为立方体体素(Voxel),并仅在查询点附近栅格或体素中查找。这种方法能很大限度缩小查找的范围。下面以说明三维体素方法所涉及的函数和具体步骤,二维栅格方法相关内容可以类推得到。
通过引入哈希函数,将相同体素中的点映射为相同哈希值,便于快速确定点集中位于查询点附近或体素中的点。哈希函数 h a s h : R 3 → R \mathrm{hash}:\mathbb{R}^{3}\rightarrow\mathbb{R} hash:R3→R定义为
h
a
s
h
(
x
)
=
(
(
x
1
∗
n
1
)
x
o
r
(
x
2
∗
n
2
)
x
o
r
(
x
3
∗
n
3
)
)
m
o
d
N
\mathrm{hash}(\mathbf{x})=((x_{1}\ast{n}_{1})\mathrm{xor}(x_{2}\ast{n}_{2})\mathrm{xor}(x_{3}\ast{n}_{3}))\,\mathrm{mod}\,{N}
hash(x)=((x1∗n1)xor(x2∗n2)xor(x3∗n3))modN
其中
n
1
,
n
2
,
n
3
n_1,n_2,n_3
n1,n2,n3是三个大质数,
N
N
N是大整数,可取
n
1
=
73856093
,
n
2
=
471943
,
n
3
=
83492791
,
N
=
10000000
n_1=73856093,n_2=471943,n_3=83492791,N=10000000
n1=73856093,n2=471943,n3=83492791,N=10000000
哈希函数需要与将位置坐标映射到体素序号的函数
V
o
x
e
l
:
R
3
→
R
3
\mathbf{Voxel}:\mathbb{R}^{3}\rightarrow\mathbb{R}^{3}
Voxel:R3→R3搭配使用,其定义为
V
o
x
e
l
(
x
)
=
[
r
o
u
n
d
(
x
1
r
)
;
r
o
u
n
d
(
x
2
r
)
;
r
o
u
n
d
(
x
3
r
)
]
T
\mathbf{Voxel}(\mathbf{x})=[\mathrm{round}(\frac{x_{1}}{r});\mathrm{round}(\frac{x_{2}}{r});\mathrm{round}(\frac{x_{3}}{r})]^{T}
Voxel(x)=[round(rx1);round(rx2);round(rx3)]T
其中
r
o
u
n
d
:
R
→
Z
\mathrm{round}:\mathbb{R}\rightarrow\mathbb{Z}
round:R→Z为四舍五入函数,
r
r
r表示分辨率,即体素的边在原始空间中的欧式长度
应用体素方法三维查找点 x \mathbf{x} x在三维点集 Y = { y 1 , ⋯ , y n } Y=\{\mathbf{y}_{1},\cdots,\mathbf{y}_{n}\} Y={y1,⋯,yn}中的 k k k点最近邻的步骤:
- 将点集 Y Y Y中每个点 y \mathbf{y} y映射为 V o x e l ( y ) \mathbf{Voxel}(\mathbf{y}) Voxel(y),再计算哈希值 h a s h ( V o x e l ( y ) ) \mathrm{hash}(\mathbf{Voxel}(\mathbf{y})) hash(Voxel(y)),以此建立哈希表
- 记 V = { [ 0 , 0 , 0 ] T , [ 1 , 0 , 0 ] T , [ − 1 , 0 , 0 ] T , [ 0 , 1 , 0 ] T , [ 0 , − 1 , 0 ] T , [ 0 , 0 , 1 ] T , [ 0 , 0 , − 1 ] T } V=\{[0,0,0]^{T},[1,0,0]^{T},[-1,0,0]^{T},[0,1,0]^{T},[0,-1,0]^{T},[0,0,1]^{T},[0,0,-1]^{T}\} V={[0,0,0]T,[1,0,0]T,[−1,0,0]T,[0,1,0]T,[0,−1,0]T,[0,0,1]T,[0,0,−1]T}
- 令 x \mathbf{x} x的待查找最近邻集合 N N ( x ) = ∅ NN(\mathbf{x})=\varnothing NN(x)=∅
- 循环选取 V V V中的元素 v \mathbf{v} v,在每次循环中,记选取的元素为 v \mathbf{v} v,计算哈希值 h a s h ( V o x e l ( x ) + v ) \mathrm{hash}(\mathbf{Voxel}(\mathbf{x})+\mathbf{v}) hash(Voxel(x)+v),通过查询哈希表将点云 Y Y Y中具有相同哈希值的点加入 N N ( x ) NN(\mathbf{x}) NN(x)
- 应用暴力单点最近邻搜索方法,查找点 x \mathbf{x} x在 N N ( x ) NN(\mathbf{x}) NN(x)中的 k k k点最近邻,并将该最近邻作为点 x \mathbf{x} x在 Y Y Y中的 k k k点最近邻
3、K-D树
K-D树(K-Dimensional Tree)是借鉴了”对排序后的容器进行查找可以大幅节省时间“这一思想,
K-D树中规定:
- 每个非叶子节点都有左右两个分枝
- 叶子节点表示原始空间中的点
- 非叶子节点存储一个分割轴和一个分割阈值,来表达在节点中包含的点不完全一致的情况下,如何分割左分枝和右分枝,例如 y i = t h r e y_{i}=thre yi=thre存储为轴 i i i和阈值 t h r e thre thre,表示左分枝取节点中在第 j j j维上小于 t h r e thre thre的元素,右分枝取节点中在第 j j j维上大于等于 t h r e thre thre的元素
根据点集 Y = { y 1 , ⋯ , y n } Y=\{\mathbf{y}_{1},\cdots,\mathbf{y}_{n}\} Y={y1,⋯,yn}构建K-D树的过程可以看成执行建树函数 B u i l d K D T r e e ( Y , n u l l p t r ) \mathrm{BuildKDTree}(Y,\mathrm{nullptr}) BuildKDTree(Y,nullptr),该函数采用递归方式定义,最终可通过 r o o t \mathrm{root} root访问整个K-D树,其定义为:
B u i l d K D T r e e ( S , ∗ r o o t ) \mathrm{BuildKDTree}(S,\ast\mathrm{root}) BuildKDTree(S,∗root)
-
记当前节点 n \mathrm{n} n为根节点 r o o t \mathrm{root} root
-
若 ∣ S ∣ = 1 |S|=1 ∣S∣=1
- 记 n \mathrm{n} n为叶子节点,退出
-
若 ∣ S ∣ > 1 |S|>1 ∣S∣>1
-
记 n \mathrm{n} n为非叶子节点,计算 S S S中的点在各轴上的方差,选择方差最大的轴 i i i作为分割轴,取平均数 m i = 1 ∣ S ∣ ∑ y ∈ S y i m_{i}=\frac{1}{|S|}\sum_{\mathbf{y}\in{S}}y_{i} mi=∣S∣1∑y∈Syi作为分割阈值
-
遍历 y ∈ S \mathbf{y}\in{S} y∈S,若 y i < m i y_{i}<m_{i} yi<mi,则插入 n \mathrm{n} n的左子节点;若 y i ⩾ m i y_{i}\geqslant{m_{i}} yi⩾mi,则插入 n \mathrm{n} n的右子节点;若 S S S中的多个点完全一致,则只保留一个点,并将节点 n \mathrm{n} n记为叶子节点
-
记全部插入到 n \mathrm{n} n左子节点上的点组成的点集为 S L S_{L} SL,递归调用 B u i l d K D T r e e ( S L ) \mathrm{BuildKDTree}(S_{L}) BuildKDTree(SL),构建K-D树;记全部插入到 n \mathrm{n} n右子节点上的点组成的点集为 S R S_{R} SR,递归调用 B u i l d K D T r e e ( S R ) \mathrm{BuildKDTree}(S_{R}) BuildKDTree(SR),构建K-D树
-
通过包含 6 6 6个点的点集 { [ 2 , 3 ] T , [ 5 , 4 ] T , [ 9 , 6 ] T , [ 4 , 7 ] T , [ 8 , 1 ] T , [ 7 , 2 ] T } \{[2,3]^{T},[5,4]^{T},[9,6]^{T},[4,7]^{T},[8,1]^{T},[7,2]^{T}\} {[2,3]T,[5,4]T,[9,6]T,[4,7]T,[8,1]T,[7,2]T}构建的K-D树如下
从已构建完成的K-D树 T \mathrm{T} T中查找点 x \mathbf{x} x的 k k k个最近邻的过程可看成首先定义 D k = ∅ D_{k}=\varnothing Dk=∅, d m a x = + ∞ d_{max}=+\infty dmax=+∞,再执行 k k k点最近邻搜索树函数 S e a r c h K D T r e e _ k ( T , x , k , D k , d m a x ) \mathrm{SearchKDTree\_k}(\mathrm{T},\mathbf{x},k,D_{k},d_{max}) SearchKDTree_k(T,x,k,Dk,dmax),最终 D k D_{k} Dk成为 x \mathbf{x} x的 k k k点最近邻集合,该函数采用递归方式定义,其定义为:
S e a r c h K D T r e e _ k ( T , x , k , & D k , & d m a x ) \mathrm{SearchKDTree\_k}(\mathrm{T},\mathbf{x},k,\&D_{k},\&d_{max}) SearchKDTree_k(T,x,k,&Dk,&dmax)
-
初始化当前节点 n \mathrm{n} n为 T \mathrm{T} T的根节点
-
若 n \mathrm{n} n是叶子节点
- 计算 n \mathrm{n} n上的点 y \mathbf{y} y与 x \mathbf{x} x的距离 d ( x , y ) d(\mathbf{x},\mathbf{y}) d(x,y),若 d ( x , y ) < d m a x d(\mathbf{x},\mathbf{y})<d_{max} d(x,y)<dmax,则 D k = D k ∪ { y } D_{k}=D_{k}\cup\{\mathbf{y}\} Dk=Dk∪{y},若此时 ∣ D k ∣ > k |D_{k}|>k ∣Dk∣>k,则删除 D k D_{k} Dk中到查询点距离最大的点,并重新计算 d m a x = { max y ∈ D k d ( x , y ) ∣ D k ∣ = k + ∞ ∣ D k ∣ < k d_{max}=\left\{\begin{array}{ll}\max_{\mathbf{y}\in{D}_{k}}d(\mathbf{x},\mathbf{y})&|D_{k}|=k\\+\infty&|D_{k}|<k\end{array}\right. dmax={maxy∈Dkd(x,y)+∞∣Dk∣=k∣Dk∣<k
- 退出
-
若 n \mathrm{n} n是非叶子节点
-
确定 x \mathbf{x} x在分割超平面的侧,也即计算 x \mathbf{x} x落在的子树 n → C h i l d K D T r e e i , i ∈ { 1 , 2 } \mathrm{n{\to}ChildKDTree}_{i},i\in\{1,2\} n→ChildKDTreei,i∈{1,2}。同时得到另一侧子树 n → C h i l d K D T r e e j , j = { 1 , 2 } \ i \mathrm{n{\to}ChildKDTree}_{j},j=\{1,2\}\backslash{i} n→ChildKDTreej,j={1,2}\i
-
递归调用 S e a r c h K D T r e e _ k ( n → C h i l d K D T r e e i , x , k , D k , d m a x ) \mathrm{SearchKDTree\_k}(\mathrm{n{\to}ChildKDTree}_{i},\mathbf{x},k,D_{k},d_{max}) SearchKDTree_k(n→ChildKDTreei,x,k,Dk,dmax),在 n \mathrm{n} n的该侧子树中搜索 x \mathbf{x} x的 k k k点最近邻
-
计算 x \mathbf{x} x到分割超平面的垂直距离为 d s p l i t d_{split} dsplit,若 d s p l i t < d m a x d_{split}<d_{max} dsplit<dmax,则递归调用 S e a r c h K D T r e e _ k ( n → C h i l d K D T r e e j , x , k , D k , d m a x ) \mathrm{SearchKDTree\_k}(\mathrm{n{\to}ChildKDTree}_{j},\mathbf{x},k,D_{k},d_{max}) SearchKDTree_k(n→ChildKDTreej,x,k,Dk,dmax),在 n \mathrm{n} n的另一侧子树中搜索 x \mathbf{x} x的 k k k点最近邻;若 d s p l i t ⩾ d m a x d_{split}\geqslant{d}_{max} dsplit⩾dmax,则跳过 n \mathrm{n} n的另一侧子树
-
退出
-
查找点 x = [ 6 ; 5 ] T \mathbf{x}=[6;5]^{T} x=[6;5]T在上例点集中的两个最近邻,搜索流程如下图,结果为 { [ 5 ; 4 ] T , [ 4 ; 7 ] T } \{[5;4]^{T},[4;7]^{T}\} {[5;4]T,[4;7]T}
在上述的K-D树搜索过程中,最关键的部分是剪枝,即不继续在另一侧搜索
k
k
k点最近邻,剪枝的条件是可以判定树形结构的另一侧不存在比现有结果更近的最近邻,判定条件如下
d
s
p
l
i
t
⩾
d
m
a
x
d_{split}\geqslant{d}_{max}
dsplit⩾dmax
在该剪枝条件下,K-D树准确率和召回率方面都可以做到
100
%
100\%
100%,但可能会遇到去很远的分枝查找
k
k
k点最近邻的情况,时间成本高。为解决该问题,可以添加一个比例因子
0
<
α
<
1
0<\alpha<1
0<α<1,可将剪枝判定条件改为
d
s
p
l
i
t
⩾
α
d
m
a
x
d_{split}\geqslant\alpha{d}_{max}
dsplit⩾αdmax
可见剪枝条件被放宽,无法判定另一侧子树中是否存在比现有结果更近的最近邻。应用该剪枝条件,可使K-D树
k
k
k点最近邻查找速度加快,但不再能保证找到严格的
k
k
k点最近邻,这种做法称为近似最近邻(Approximate Nearest Neighbour,ANN)
4、八叉树
八叉树(Octo Tree,Octree)
根据点集 Y = { y 1 , ⋯ , y n } Y=\{\mathbf{y}_{1},\cdots,\mathbf{y}_{n}\} Y={y1,⋯,yn}构建八叉树树的过程可以看成执行建树函数 B u i l d O c T r e e ( Y , O , 1 , n u l l p t r ) \mathrm{BuildOcTree}(Y,\mathbf{O},1,\mathrm{nullptr}) BuildOcTree(Y,O,1,nullptr),该函数采用递归的方式定义,最终可通过 r o o t \mathrm{root} root访问整个八叉树,二者定义为:
B u i l d O c T r e e ( S , b o x , d e p t h , ∗ r o o t ) \mathrm{BuildOcTree}(S,\mathbf{box},depth,\ast\mathrm{root}) BuildOcTree(S,box,depth,∗root)
-
初始化当前节点 n \mathrm{n} n为根节点 r o o t \mathrm{root} root
-
若 d e p t h = 1 depth=1 depth=1
- 计算 Y Y Y中的点在各轴上的最小值 min i = min y ∈ Y y i , i = 1 , 2 , 3 \min_{i}=\min_{\mathbf{y}\in{Y}}y_{i},i=1,2,3 mini=miny∈Yyi,i=1,2,3和最大值 max i = max y ∈ Y y i , i = 1 , 2 , 3 \max_{i}=\max_{\mathbf{y}\in{Y}}y_{i},i=1,2,3 maxi=maxy∈Yyi,i=1,2,3,这些最大值最小值组成表示包围盒的向量 b o x = [ min 1 , max 1 , min 2 , max 2 , min 3 , max 3 ] T \mathbf{box}=[\min_{1},\max_{1},\min_{2},\max_{2},\min_{3},\max_{3}]^{T} box=[min1,max1,min2,max2,min3,max3]T
-
若 ∣ S ∣ = 0 |S|=0 ∣S∣=0
- 退出
-
若 ∣ S ∣ = 1 |S|=1 ∣S∣=1
- 记 n \mathrm{n} n为叶子节点,退出
-
若 ∣ S ∣ > 1 |S|>1 ∣S∣>1
-
赋值 { min 1 = b o x 1 , max 1 = b o x 2 min 2 = b o x 3 , max 2 = b o x 4 min 3 = b o x 5 , max 3 = b o x 6 \left\{\begin{array}{ll}\min_{1}=box_{1},\,\max_{1}=box_{2}\\\min_{2}=box_{3},\,\max_{2}=box_{4}\\\min_{3}=box_{5},\,\max_{3}=box_{6}\end{array}\right. ⎩ ⎨ ⎧min1=box1,max1=box2min2=box3,max2=box4min3=box5,max3=box6
-
计算包围盒在各个轴上的中心位置 c i = min i + max i 2 , i = 1 , 2 , 3 c_{i}=\frac{\min_{i}+\max_{i}}{2},i=1,2,3 ci=2mini+maxi,i=1,2,3
-
计算 8 8 8个子节点的表示包围盒的向量:
b o x 1 = [ min 1 , c 1 , min 2 , c 2 , min 3 , c 3 ] T \mathbf{box}_{1}=[\min_{1},c_{1},\min_{2},c_{2},\min_{3},c_{3}]^{T} box1=[min1,c1,min2,c2,min3,c3]T, b o x 2 = [ c 1 , max 1 , min 1 , c 2 , min 3 , c 3 ] T \mathbf{box}_{2}=[c_{1},\max_{1},\min_{1},c_{2},\min_{3},c_{3}]^{T} box2=[c1,max1,min1,c2,min3,c3]T
b o x 3 = [ min 1 , c 1 , c 2 , max 2 , min 3 , c 3 ] T \mathbf{box}_{3}=[\min_{1},c_{1},c_{2},\max_{2},\min_{3},c_{3}]^{T} box3=[min1,c1,c2,max2,min3,c3]T, b o x 4 = [ c 1 , max 1 , c 2 , max 2 , min 3 , c 3 ] T \mathbf{box}_{4}=[c_{1},\max_{1},c_{2},\max_{2},\min_{3},c_{3}]^{T} box4=[c1,max1,c2,max2,min3,c3]T
b o x 5 = [ min 1 , c 1 , min 2 , c 2 , c 3 , max 3 ] T \mathbf{box}_{5}=[\min_{1},c_{1},\min_{2},c_{2},c_{3},\max_{3}]^{T} box5=[min1,c1,min2,c2,c3,max3]T, b o x 6 = [ c 1 , max 1 , min 1 , c 2 , c 3 , max 3 ] T \mathbf{box}_{6}=[c_{1},\max_{1},\min_{1},c_{2},c_{3},\max_{3}]^{T} box6=[c1,max1,min1,c2,c3,max3]T
b o x 7 = [ min 1 , c 1 , c 2 , max 2 , c 3 , max 3 ] T \mathbf{box}_{7}=[\min_{1},c_{1},c_{2},\max_{2},c_{3},\max_{3}]^{T} box7=[min1,c1,c2,max2,c3,max3]T, b o x 8 = [ c 1 , max 1 , c 2 , max 2 , c 3 , max 3 ] T \mathbf{box}_{8}=[c_{1},\max_{1},c_{2},\max_{2},c_{3},\max_{3}]^{T} box8=[c1,max1,c2,max2,c3,max3]T
-
遍历 y ∈ S \mathbf{y}\in{S} y∈S,在循环内部遍历各子节点,根据各子节点的包围盒确定 y \mathbf{y} y应当落在的子节点:若 { b o x j 1 ⩽ y 1 ⩽ b o x j 2 b o x j 3 ⩽ y 2 ⩽ b o x j 4 b o x j 5 ⩽ y 3 ⩽ b o x j 6 \left\{\begin{array}{l}{box_{j}}_{1}\leqslant{y}_{1}\leqslant{box_{j}}_{2}\\{box_{j}}_{3}\leqslant{y}_{2}\leqslant{box_{j}}_{4}\\{box_{j}}_{5}\leqslant{y}_{3}\leqslant{box_{j}}_{6}\end{array}\right. ⎩ ⎨ ⎧boxj1⩽y1⩽boxj2boxj3⩽y2⩽boxj4boxj5⩽y3⩽boxj6,则插入第 j j j个子节点
-
记插入到各子节点的点组成的点集为 S j , j = 1 , ⋯ , 8 S_{j},j=1,\cdots,8 Sj,j=1,⋯,8,对 8 8 8个子节点分别递归调用 B u i l d O c T r e e ( S j , b o x j , d e p t h + 1 ) \mathrm{BuildOcTree}(S_{j},\mathbf{box}_{j},depth+1) BuildOcTree(Sj,boxj,depth+1),构建八叉树
-
从已构建完成的八叉树 T \mathrm{T} T中查找点 x \mathbf{x} x的 k k k个最近邻的过程可以看成看成首先定义 D k = ∅ D_{k}=\varnothing Dk=∅, d m a x = + ∞ d_{max}=+\infty dmax=+∞,再执行 k k k点最近邻搜索树函数 S e a r c h O c T r e e _ k ( T , x , k , D k , d m a x ) \mathrm{SearchOcTree\_k}(\mathrm{T},\mathbf{x},k,D_{k},d_{max}) SearchOcTree_k(T,x,k,Dk,dmax),最终 D k D_{k} Dk成为 x \mathbf{x} x的 k k k点最近邻集合,该函数采用递归方式定义,其定义为:
S e a r c h O c T r e e _ k ( T , x , k , & D k , & d m a x ) \mathrm{SearchOcTree\_k}(\mathrm{T},\mathbf{x},k,\&D_{k},\&d_{max}) SearchOcTree_k(T,x,k,&Dk,&dmax)
-
初始化当前节点 n \mathrm{n} n为 T \mathrm{T} T的根节点, b o x \mathbf{box} box为表示 n \mathrm{n} n的包围盒的向量
-
若 n \mathrm{n} n是叶子节点
- 计算 n \mathrm{n} n上的点 y \mathbf{y} y与 x \mathbf{x} x的距离 d ( x , y ) d(\mathbf{x},\mathbf{y}) d(x,y),若 d ( x , y ) < d m a x d(\mathbf{x},\mathbf{y})<d_{max} d(x,y)<dmax,则 D k = D k ∪ { y } D_{k}=D_{k}\cup\{\mathbf{y}\} Dk=Dk∪{y},若此时 ∣ D k ∣ > k |D_{k}|>k ∣Dk∣>k,则删除 D k D_{k} Dk中到查询点距离最大的点,并重新计算 d m a x = { max y ∈ D k d ( x , y ) ∣ D k ∣ = k + ∞ ∣ D k ∣ < k d_{max}=\left\{\begin{array}{ll}\max_{\mathbf{y}\in{D}_{k}}d(\mathbf{x},\mathbf{y})&|D_{k}|=k\\+\infty&|D_{k}|<k\end{array}\right. dmax={maxy∈Dkd(x,y)+∞∣Dk∣=k∣Dk∣<k
- 退出
-
若 n \mathrm{n} n是非叶子节点
-
若 x \mathbf{x} x落在 n \mathrm{n} n的包围盒的外面
- 遍历 n \mathrm{n} n的每个子节点,递归调用 S e a r c h O c T r e e _ k ( n → C h i l d O c T r e e i , x , k , D k , d m a x ) \mathrm{SearchOcTree\_k}(\mathrm{n}{\to}\mathrm{ChildOcTree}_{i},\mathbf{x},k,D_{k},d_{max}) SearchOcTree_k(n→ChildOcTreei,x,k,Dk,dmax)
- 退出
-
若 x \mathbf{x} x落在 n \mathrm{n} n的包围盒的里面
-
计算 x \mathbf{x} x落在哪个子节点的包围盒中,也即确定 x \mathbf{x} x落在的子树 n → C h i l d O c T r e e i , i ∈ { 1 , ⋯ , 8 } \mathrm{n}\mathrm{\to}\mathrm{ChildOcTree}_{i},i\in\{1,\cdots,8\} n→ChildOcTreei,i∈{1,⋯,8}。同时得到其他子树 n . C h i l d O c T r e e j , j ∈ { 1 , ⋯ , 8 } \ i \mathrm{n}.\mathrm{ChildOcTree}_{j},j\in\{1,\cdots,8\}\backslash{i} n.ChildOcTreej,j∈{1,⋯,8}\i
-
递归调用 S e a r c h O c T r e e _ k ( n → C h i l d O c T r e e i , x , k , D k , d m a x ) \mathrm{SearchOcTree\_k}(\mathrm{n{\to}ChildOcTree}_{i},\mathbf{x},k,D_{k},d_{max}) SearchOcTree_k(n→ChildOcTreei,x,k,Dk,dmax),在 n \mathrm{n} n的该子树中搜索 x \mathbf{x} x的 k k k点最近邻
-
遍历 n \mathrm{n} n的其他子节点,计算 x \mathbf{x} x到其他各子节点包围盒各面距离的最大值 d b o x j = max { ∣ x 1 − b o x j 1 ∣ , ∣ x 2 − b o x j 2 ∣ , ∣ x 2 − b o x j 3 ∣ , ∣ x 2 − b o x j 4 ∣ , ∣ x 3 − b o x j 5 ∣ , ∣ x 3 − b o x j 6 ∣ } {d_{box}}_{j}=\max\{|x_{1}-{box_{j}}_{1}|,|x_{2}-{box_{j}}_{2}|,|x_{2}-{box_{j}}_{3}|,|x_{2}-{box_{j}}_{4}|,|x_{3}-{box_{j}}_{5}|,|x_{3}-{box_{j}}_{6}|\} dboxj=max{∣x1−boxj1∣,∣x2−boxj2∣,∣x2−boxj3∣,∣x2−boxj4∣,∣x3−boxj5∣,∣x3−boxj6∣},若 d b o x j < d m a x {d_{box}}_{j}<d_{max} dboxj<dmax,则递归调用 S e a r c h O c T r e e _ k ( n → C h i l d O c T r e e , x , k , D k , d m a x ) \mathrm{SearchOcTree\_k}(\mathrm{n{\to}ChildOcTree},\mathbf{x},k,D_{k},d_{max}) SearchOcTree_k(n→ChildOcTree,x,k,Dk,dmax),在 n \mathrm{n} n的子树中搜索 x \mathbf{x} x的 k k k点最近邻;若 d b o x j ⩾ d m a x {d_{box}}_{j}\geqslant{d}_{max} dboxj⩾dmax,则跳过 n \mathrm{n} n的子树 n → C h i l d O c T r e e j \mathrm{n{\to}ChildOcTree}_{j} n→ChildOcTreej
-
退出
-
-
上述八叉树搜索过程也涉及剪枝,即不继续在对应子树中搜索
k
k
k点最近邻,剪枝判定条件如下
d
b
o
x
j
⩾
d
m
a
x
{d_{box}}_{j}\geqslant{d}_{max}
dboxj⩾dmax
在该剪枝条件下,八叉树准确率和召回率方面都可以做到
100
%
100\%
100%,但可能会遇到去很远的分枝查找
k
k
k点最近邻的情况,时间成本高。为解决该问题,同样可以添加一个比例因子
0
<
α
<
1
0<\alpha<1
0<α<1,可将剪枝判定条件改为
d
b
o
x
j
⩾
α
d
m
a
x
{d_{box}}_{j}\geqslant\alpha{d}_{max}
dboxj⩾αdmax
剪枝条件被放宽,无法判定对应子树中是否存在比现有结果更近的最近邻。应用该剪枝条件,可使八叉树
k
k
k点最近邻查找速度加快,但不再能保证找到严格的
k
k
k点最近邻。
实验:不同单点最近邻搜索方法对比
现有两组三维点云,采用多线程版本的不同方法查找第一组点云中每个点在第二组点云中的最近邻所需的时间,准确率,召回率如下
暴力搜索 | 体素方法( r = 0.1 r=0.1 r=0.1) | K-D树( α = 0.1 \alpha=0.1 α=0.1) | 八叉树( α = 0.1 \alpha=0.1 α=0.1) | |
---|---|---|---|---|
时间(毫秒) | 345 345 345 | 1.13 1.13 1.13 | 1.61 1.61 1.61 | 4.77525 4.77525 4.77525 |
准确率 | 1 1 1 | 0.90 0.90 0.90 | 0.83 0.83 0.83 | 0.60 0.60 0.60 |
召回率 | 1 1 1 | 0.41 0.41 0.41 | 0.83 0.83 0.83 | 0.60 0.60 0.60 |