空间数据结构(二)-OCTree

1. 概述

八叉树结构是由Hunter博士于1978年首次提出的一种数据模型。八叉树结构通过对三维空间的几何实体进行体元剖分,每个体元具有相同的时间和空间复杂度,通过循环递归的划分方法对大小为(2n∗2n∗2n的三维空间的几何对象进行剖分,从而构成一个具有根节点的方向图。在八叉树结构中如果被划分的体元具有相同的属性,则该体元构成一个叶节点;否则继续对该体元剖分成8个子立方体,依次递剖分,对于(2n∗2n∗2n)大小的空间对象,最多剖分n次,如下图所示:

image-20210404173029797

八叉树(Octree)是一种用于描述三维空间的树状数据结构。八叉树的每个节点表示一个正方体的体积元素,每个节点有八个子节点,这八个子节点所表示的体积元素加在一起就等于父节点的体积。一般中心点作为节点的分叉中心。八叉树若不为空树的话,树中任一节点的子节点恰好只会有八个,或零个,也就是子节点不会有0与8以外的数目。八叉树叶子节点代表了分辨率最高的情况。例如分辨率设成0.01m,那么每个叶子就是一个1cm见方的小方块。

2. 算法详解

八叉树是将空间平均得分为8个部分,每个节点就是一个部分,我们假设三维空间是一个正立方体,每个节点就是一个小立方体。划分到最后,一个叶子结点代表一个最小的空间范围,我们称之为体素。八叉树的建立过程也就是空间划分的过程。

实现原理:

  • 设定最大递归深度
  • 找出场景的最大尺寸,并以此尺寸建立第一个立方体
  • 依序将单位元元素丢入能被包含且没有子节点的立方体
  • 若没有达到最大递归深度,就进行细分八等份,再将该立方体所装的单位元元素全部分担给八个子立方体
  • 若发现子立方体所分配到的单位元元素数量不为零且跟父立方体是一样的,则该子立方体停止细分,因为跟据空间分割理论,细分的空间所得到的分配必定较少,若是一样数目,则再怎么切数目还是一样,会造成无穷切割的情形。
  • 重复,直到达到最大递归深度。

基于八叉树的近邻搜索可以分为三种:

  • 范围搜索
  • k最近邻搜索
  • 体素内搜索

3. 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import random
import time
from random import random
from math import sqrt


class Point3D():
"""A point in 3 dimensions"""
def __init__(self, x, y, z, data=None):
self.x = x
self.y = y
self.z = z
self.data = data

def __add__(self, point):
return Point3D(self.x + point.x, self.y + point.y, self.z + point.z)

def __sub__(self, point):
return Point3D(self.x - point.x, self.y - point.y, self.z - point.z)

def __mul__(self, value):
if type(value) == tuple:
return Point3D(self.x * value[0], self.y * value[1], self.z * value[2])
else:
return Point3D(self.x * value, self.y * value, self.z * value)

def __truediv__(self, value):
if type(value) == Point3D:
raise NotImplementedError()
else:
return Point3D(self.x / value, self.y / value, self.z / value)

def __repr__(self):
'Return a nicely formatted representation string'
return self.__class__.__name__ + '(x='+str(self.x)+', y='+str(self.y)+', z='+str(self.z)+')'

def dist(self, point):
return sqrt(self.x**2 + self.y**2 + self.z**2)


def index2mask(idx: int):
return (idx & 1, (idx >> 1) & 1, (idx >> 2) & 1)


def mask2index(idx: tuple):
x, y, z = [int(bool(idx[i])) for i in range(3)]
return (z << 2) + (y << 1) + x


def invMask(mask: tuple):
return tuple([mask[i] ^ 1 for i in range(3)])


class Octree():
"""Representation of a Node in the Octree."""

def __init__(self, lower_bound: Point3D, upper_bound: Point3D, max_levels=-1, max_size=10):
self.has_children = False
self.children: Octree = []
self.points: Point3D = []
self.lower_bound = lower_bound
self.upper_bound = upper_bound
self.max_levels = max_levels
self.max_size = max_size
self.center = (lower_bound + upper_bound) / 2
self.numPoints = 0

def extend(self):
if (self.max_levels == 0):
return
else:
self.has_children = True
self.children = list([self._generateChildTree(idx) for idx in range(8)])

def _generateChildTree(self, idx):
mask = index2mask(idx)
imask = invMask(mask)
new_lower_bound = self.lower_bound * imask + self.center * mask
new_upper_bound = self.center * imask + self.upper_bound * mask
return Octree(new_lower_bound, new_upper_bound, self.max_levels - 1, self.max_size)

def addPoint(self, pt: Point3D):
if self.has_children:
self._addPointToChild(pt)
elif len(self.points) >= self.max_size:
self.extend()
for point in self.points:
self._addPointToChild(point)
self._addPointToChild(pt)
self.points = []
else:
self.points.append(pt)
self.numPoints += 1

def _addPointToChild(self, pt: Point3D):
x = int(pt.x >= self.center.x)
y = int(pt.y >= self.center.y)
z = int(pt.z >= self.center.z)
self.children[mask2index((x, y, z))].addPoint(pt)


if __name__ == '__main__':
ot = Octree(Point3D(0, 0, 0), Point3D(1, 1, 1))

for i in range(20):
x, y, z = random(), random(), random()
ot.addPoint(Point3D(x, y, z))

参考:https://github.com/LukasACH/Octree-Py/blob/master/Octree/Octree.py

4. 总结和讨论

5. 参考