InfiniTAM是基于voxel hashing基础上做的,我们先对voxel hashing介绍。
参考文献:”Real-time 3D Reconstruction at Scale using Voxel Hashing”。
和 KinectFusion 一样也是利用 TSDF 模型对重建进行建模,只是在建模的时候,不是对整个空间都划分等大小的网格,只是在场景表面的周围划分网格。
system pipline
用无穷多个的网格将空间划分成 voxel blocks,每个voxel block是等大小的,每个voxel block包含 83 个voxels,每个voxel存储了 TSDF值,color和weight,voxel 的数据结构如下:
Voxel block只在待重建的表面分配显存,使用hash表来manage allocation和retrieval,哈希表存储了hash entry,每个hash entry存储了指向allocated voxel block的指针,根据3D点的坐标可以由如下方法计算hash value,即hash entry在哈希表中的索引:
H(x,y,z)=(x⋅p1⊕y⋅p2⊕z⋅p3) mod n
Hash entry的数据结构如下:
作者将hash table均匀地分成bucket,每个bucket对应一个hash value,每个bucket sequentially 存储了hash entries,当hash索引发生collision时,用bucket中的下一个entry存储voxle block的pointer。
通过选择合适的hash table和bucket size,bucket overflow一般不会发生,但是也有可能发生,当overflow时,在下一个bucket的free spot存储,在解决collision的时候,不会用bucket的最后一个entry去存储别的bucket的hash entry,最后一个entry用来解决当前bucket存储不足时,再别的bucket中存储的offset。
通过计算hash function在hash table中索引具有相同world space position的entry,如果可以找到,返回entry对应的block,如果找不到,则选择在第一个empty的entry处插入entry,如果bucket是满的,则在下一个bucket中插入(如上图2所示),在下一个bucket中插入时,不能在最末尾的entry插入(如上图3所示),最末尾的entry用来保存当前bucket不足时,向后插入的offset。为了防止GPU race condition,在一个bucket中插入entry时,将当前bucket锁住,并且对于在同一个bucket中进行操作的都放到下一帧才能重新开始。
根据world position计算hash value并且按照hash value在bucket中索引,如果有entry中的offset不是empty,还要按照offset再继续查找。因为对于entry的删除操作,会造成bucket中的entry有empty,当在bucket中索引出现empty时,也不会停止查找(如果找不到至少会搜索整个bucket)。
删除的时候分三种情况
1、如果删除项就在bucket中,则直接删除,如上图5所示
2、如果删除项是bucket中的最后一个entry如上图4所示,则按照offset拷贝下一个entry代替当前entry,并且修改offset的值
3、如果删除是在其它bucket中,删除并且修改指针位置。
在融合新的TSDF前,在相机视场内并且在truncation region内的hash entry和voxel block都必须先分配好。对于一幅新的深度图像,这里并行处理所有的像素点,为了补偿远距离测量时深度测量的大的不确定性,truncation 区域的大小而是按照深度图像的variance选取的。
对于深度图像的每个像素点,初始化一个到truncation region的interval bound,对于给定的预先定义好的resolution和block size,使用DDA计算和ray有交叠的voxel block,对于找到的有交叠的block,将block的entry插入到哈希表中(理想情况下,每个像素点初始化的ray应该用frustum而不是简单的用一个ray来做,但是用ray可以提高效率,并且即使在深度较大是算法性能上也没有多大的损伤)。这里是将全部的物理空间划分成等大小的网格,光线和truncation region计算相交的block时,truncation region根据深度值计算。
一旦在hash table中插入了一个hash entry,在GPU的显存堆预先分配好的区域中存储对应的block,所有的block都在GPU一块连续的存储空间中,用一个list来管理,list中存储了所有的还没有分配的block地址,新的block分配空间时,从list中的末尾元素获得地址,当一个block被删除时,删除的block放到list的末尾元素中。
融合的时候,并行地处理hash table中所有的entry,并且在一个数组中存储对应的block是否visible。用parallel prefix sum来扫描这个数组,将扫描后的visible的entry存储在另外一个buffer中。
生成的数组中存储的hash entry再用GPU并行处理更新block中的TSDF值,使用GPU的每个核心处理单个voxel block(而不是每个线程),一个单个的voxel block在一个单个GPU的核心中处理,这样可以最大化cache hits并且可以最小化code divergence,这样比用单个线程处理单个voxel block效率要高。
更新上述每个block中每个voxel存储的TSDF、weight和color更新TSDF时,对于近处的深度点权值设的相对较大,颜色更新时,最近测量的权值也设的相对较大。
上述更新对于所有的block操作,不管对应的block是否在truncation region内(即使surface被移动了,或者测量的有外点同样做更新),对于integration后还会再检查回收没有值的block(加权融合掉的)。
Garbage collection消除因为噪声外点或者moved surface产生的区域,对于每个block计算block中voxel TSDF值绝对值最小值和weight的最大值,如果weight的最大值为0或者最小的TSDF值大于一个阈值,将这个block标示为待删除的block,全部标识完后,再并行地删除前面标识的所有的block,当一个block被删除掉后,再将block对应的显存释放掉(将block的pointer挂到前面说道的heap的末尾)。
使用raycasting在TSDF模型中提取隐含的表面,首先通过光栅化所有的在当前视角下分配的block来计算每个ray的start和end points。通过two passes来完成,并且生成两个最小深度和最大深度的两个z-buffers。
对于每个像素点,从最小深度值到最大深度值初始化一个ray,在ray marching过程中,需要估计voxel周围点的TSDF值,在这个过程中,没有分配的voxel block被当成empty space看待,通过和领域内的8个相邻voxel使用tri-linear interpolation得到world position的坐标,对于处在voxel boundary上的voxel,通过索引hash table中的entry来得到相邻的voxel。
为了提取surface interface(zero-crossing),需要计算sign change的voxel。为了加快ray marching的速度,作者跳过一个predefined interval,在找到zero-crossing后再用iterative line search估计surface location。
几何point-to-plane ICP和photometric color估计相机运动。
我们创建sphere包含当前相机的frustum,如上图所示,在相机离相机光心4米的地方,以8米的半径创建一个sphere,作为当前的active region,在每次pipeline的开始和每次位姿估计的结束都会有bidirectional streaming。
我们并行的访问hash table并且标记移除active region的blocks,对于所有待移除的active region将其对应的hash entry删除,并且将删除的entry放到intermediate buffer里,并且对于这些entries对应的blocks复制到intermediate buffer中,原始的voxel blocks和entries重新放到heap的末尾,最后这些entry和block再拷贝到host。
在host中voxel data不在按照hash table的方式存储,而是将world space划分成chunks(each chunk 1 m3 ),voxel blocks用linked lists appended在这些chunks上。
和GPU-to-Host不同的是Host-to-GPU在进行Steaming的时候将整个chunk从Host移到GPU中,这样可以提升效率。由于CPU的计算能力有限,host-to-GPU steaming每一帧只传输一个chunk,选择和相机的frustum center最近的chunk移到GPU中,通过intermediate buffers作为CPU到GPU传输的媒介,复制到GPU后将block的描述子即entries插入到hash table中,类似于在allocation中做的操作。
在steaming过程中一个重要的事情是要确保voxel block不要在host和GPU中重复出现,造成potential memory leaks。当从host向GPU steam时,有种可能是在等待被steam的voxel blocks重新进入view frustum,必须确保在后续的操作中在这些voxel blocks不会做再新分配voxel blocks。在GPU中存储binary occupancy grid,当block对应的值为1时,表示chunk在GPU中,allocations可以在GPU中进行,当值为0时,表示值在host中存储,allocation应该被杜绝。 2563m3 的Binary grid大概消耗512KB的显存。