先介绍体素哈希。
隐式体积融合
我们考虑隐式SDF(implicit signed distance field)方式存储物体的三维模型,即一个稠密体素网格(voxel grid),每个体素存储两个值:1带符号距离(表达了从体素中心到被观测表面的距离,正值代表体素位于表面的前方),2权值。物体的表面由体素的零值面隐式地表达。
对于每个输入的深度图,我们扫描整个模型,保留视锥范围内(当前帧的视锥)的体素,并将体素映射到深度图平面上,带权更新体素的SDF值。
该过程即为隐式SDF方法的融合过程。
为了减少计算代价,通过引入TSDF(Truncated signed distance field),即当体素距离某个表面的距离大于一个阈值后,我们忽略该体素的SDF值。
采用TSDF后,大部分体素不再存储有效数据,包括在表面前面的自由空间(free space)或者是表面后面的无法观测区域。因此设计了一个数据结构可以有效地利用这种稀疏性。
哈希表和哈希函数
我们方法的核心是一个哈希表(hash table)数据结构,由
n
n
n个哈希条目组成,每个哈希条目(hash entries)记录着体素块指针
p
o
i
n
t
e
r
pointer
pointer和对应的空间位置
[
x
,
y
,
z
]
[x,y,z]
[x,y,z]。
world被均匀的网格分为一个一个的体素,每个体素包含一个TSDF值、颜色和权值。
8
3
8^3
83个体素(或者说网格)组成一个体素块(voxel block)。
我们将定义一个哈希函数(hash function)实现从一个空间位置
(
x
,
y
,
z
)
(x,y,z)
(x,y,z)到哈希条目(hash entries)的映射,而该条目将会存储指向
(
x
,
y
,
z
)
(x,y,z)
(x,y,z)处体素块的指针。
哈希函数为:
H
(
x
,
y
,
z
)
=
(
x
⋅
p
1
⊕
y
⋅
p
2
⊕
z
⋅
p
3
)
m
o
d
n
H(x, y, z)=\left(x \cdot p_{1} \oplus y \cdot p_{2} \oplus z \cdot p_{3}\right) \bmod n
H(x,y,z)=(x⋅p1⊕y⋅p2⊕z⋅p3)modn
其中,
p
1
p_1
p1,
p
2
p_2
p2,
p
3
p_3
p3为三个大质数(项目中实际赋值为 73856093,
19349669, 83492791),
n
n
n是哈希表大小。
处理冲突——哈希桶
因为无法保证不同位置映射到不同的哈希值,所以我们将每个哈希条目拓展为一个哈希桶(hash bucket),每个哈希桶中包含多个哈希条目,但对应同一个哈希值。
当冲突发生时(即此时哈希桶中第一个哈希条目已经被占用),我们将体素块指针存储在哈希桶中的下一个可用的哈希条目中。
哈希桶的溢出
一般,如果哈希表和哈希桶的尺寸选择合理,那么哈希桶将会很少溢出。为了处理溢出情况,我们扩展哈希条目,增添一个偏移量offset,用来指示下一个哈希条目的位置。
即当插入新的哈希条目
a
a
a时,且对应的哈希桶
A
A
A已经满时,我们顺着哈希表,寻找其他哈希桶中的空闲位置,如果找到,在该空闲位置插入该哈希条目,并在哈希桶
A
A
A中的最后一个哈希条目的offset记录新插入的哈希条目的位置。
如果该哈希桶
A
A
A还有新的哈希条目
b
b
b,我们使用之前新插入的哈希条目
a
a
a的offset记录哈希条目
b
b
b的位置。
这样,由溢出的哈希条目及其offset字段形成了链表,我们称为linked list。
其中,注意,溢出的哈希条目不能占用哈希桶的最后一个条目位置,该位置需要存储该哈希桶的linked list的表头。
哈希条目结构如下:
struct HashEntry {
short position[3];//偏移
short offset;//体素块位置
int pointer;//体素块指针
};
哈希操作
插入/检索
我们首先计算哈希函数以确定需要插入/检索的哈希桶。然后我们迭代哈希桶中的所有元素,包括哈希桶的linked list。如果我们找到一个条目具有相同的position,即可返回检索结果。如果我们在哈希桶中找到了空闲条目,那么插入新的哈希条目。如果哈希桶已满,那么我们将哈希条目放入linked list。
注意,为了避免内存冲突,在确定插入位置时,锁定该哈希桶。
删除
如果删除对象在该哈希桶中,且不在最后一个位置(该位置存储linked list表头),那么直接删除。
如果删除对象在哈希桶中的最后一个条目位置,那么,将linked list的中的第二个哈希条目移动到哈希桶中的最后一个条目位置,然后修改相应的offset值,以保证linked list的正确性。
如果删除对象在linked list(不包括哈希桶中的最后一个条目位置),那么直接删除对象,并修改相应的offset值。
上图描述了条目的插入和删除的过程。
算法流程
体素块分配
这一部分因为没有看到代码,没有看明白论文表述的究竟是个什么过程。
体素块融合
在上一节中,我们将所有表面的截断区域中的体素块分配到了哈希表中。当接收深度图时,我们需要更新当前视锥中所有已分配的体素块。
体素块选择
我们并行访问哈希表中的所有哈希条目,并创建一个缓冲区,复制存储所有指向视锥中的体素块的哈希条目。
注意,不会复制体素块,仅复制存储其关联的哈希条目。
缓冲区建立示意图。
隐式表面更新
按照TSDF更新规则,并行处理哈希条目列表(上一步创建的缓冲区)。
其中,每个GPU kernel处理一个哈希条目,即一个体素块,每个线程处理一个体素。
(这种方法因为在单个GPU处理器上处理体素块,缓存命中率高,处理效果比较好)
深度的更新有赖于体素的深度权值,会根据实际深度值设置体素的深度权值,因为噪声与测量距离有关,表面距离摄像头越近,噪声越小,相应的深度权值越大。而颜色多采用平均值,但会将更多的权重分配给最近的颜色输入。
异常处理
如果遇到动态物体、或出现异常值时,会出现更新次数很少,但影响建模质量的体素块。我们在进行TSDF更新后,计算视锥内的每个体素块中的1最大权值,2最小TSDF值。删除最大权值为0或者最小TSDF大于阈值的体素块:1删除对应哈希条目,2释放体素块内存。
表面提取
这一步是通过raycast提取TSDF模型的隐式表面,获得指定视点的深度图,并通过颜色渲染获得rgb图。
对每个像素,我们沿着射线,由最小深度行进到最大深度,行进期间,如果当前位置处在被分配的体素块中,使用三线性插值评估当前位置的TSDF值。为了加快检索零交叉点位置,我们以截断值的一半为最小行径间隔。一旦检索到零交叉点,我们就使用迭代线性搜索来估计真实得表面位置。
相机跟踪
我们使用 上一步raycast得到的深度图和输入的深度图,采用point-plane ICP和投影匹配,进行位姿估计。其误差函数不仅包含几何误差、还包含颜色误差。
内存管理——双向数据传输(streaming)
我们使用双向的GPU-Host数据传输方案,以解决因体素块的存储带来的GPU内存占用和性能问题。
我们创建一个球形的活动区域(active region),该区域包含了当前帧的视锥范围。以Kinect相机为例,假定可探测深度为八米,我们将活动区域的中心定位在距离相机位置四米处,半径为八米。
在完成当前帧位姿估计后,即进行双向数据传输(streaming)
从GPU/活动区域中移出体素块
首先,在哈希表中标记出所有需要移出的体素块,然后,将这些体素块、哈希条目,借由中间缓冲区(intermediate buffer)送到host,删除/释放其原有的哈希条目/体素块。建立堆(heap),存储删去的体素块的位置,以便之后再次使用。
在host中,我们建立多个chunks,每个chunks对应现实世界的一个区域,使用linked list将体素块附加到这些chunks上。并为每个体素块创建一个描述符,存储哈希条目数据与体素数据。
体素块的传回
我们首先确定落入活动区域的chunks,直接将chunks中的所有体素块传输给GPU。考虑到HOST-GPU的高带宽,以及在GPU中进行体素块剔除的高效性,这样可以提高性能。
由于CPU性能有限,导致HOST-GPU的传输速度为1chunks/帧。因此,距离摄像机视锥中心最近的chunks优先传输。
为了保证体素块在被传回GPU之前,其相应位置不会被更新,我们需要在GPU上创建一个二进制占用网格,每个项对应一个特定的体素块,指示其是否在GPU中。如果显示目标体素块在Host中,应避免进一步操作。
该二进制网格在
256
m
3
256m^3
256m3的空间中仅有不到512KB的开销。