Voxel Hashing阅读笔记

前言

最近在学习infiniTAM,里面涉及到了许多Voxel Hashing的内容,不是很明白,所以打算阅读一下Voxel Hashing这篇文章。

Voxel Hashing的特点

  • 能够有效的压缩T-SDF的体积,在无需分层空间的数据结构的同时,保证表面的分辨率
  • 通过插入和更新操作,能够有效的把新的T-SDF数据融合到哈希表中,同时最小化哈希冲突
  • 在清理无效的体素块时,不需要重组数据结构,避免了巨大的开销
  • 在主机和GPU之间构建了轻量级的双向流,支持无边界重建
  • 通过标准光线投射和多边形操作,从数据结构中提取出等值面,以进行渲染和相机位姿估计

系统管道
体素块(Voxel Blocks):无限均匀的网格将世界细分为体素块(voxel block),每个体素块由888个体素构成,每个体素存储一个T-SDF、颜色和权重。哈希表中的每一项都对应着一个体素块。
Voxel Hashing阅读笔记_第1张图片
1.在输入一张新的深度图片时,首先执行融合操作。根据输入的深度图分配体素块,并将块描述符插入哈希表中。这里要注意,只是分配了体素块,但是体素块内并没有存储相应的数据。接下来对依据输入的深度图和颜色数据对每个分配的体素块进行扫描,从中获得对应的SDF、颜色和权重。此外,对距离等值面太远的体素块进行移除,并释放相应的内存。
2.在融合操作结束之后,依据估计的当前相机位姿进行光线投射从而获得等值面。获得的深度信息和颜色信息可以用在下一步的相机位姿估计当中。最后,在主机和GPU中间建立双向流,将即将使用的哈希表条目及其对应的体素块传输到主机上。之前流出的体素块也可在重新访问时流回GPU数据结构。

数据结构

无限均匀的网格将世界细分为体素块(voxel block),每个体素块由888个体素构成,每个体素存储一个T-SDF、颜色和权重。
体素的数据结构如下
Voxel Hashing阅读笔记_第2张图片
为了利用稀疏性,同时减少计算资源的消耗,仅仅在重建的曲面几何体周围分配。作者使用一个高效的GPU加速哈希表来管理体素块的分配和检索。哈希表存储哈希条目,每个条目都包含指向已分配的体素块的指针。可以使用整数世界坐标(x,y,z)从哈希表中检索对应的体素块。通过简单的乘法和舍入也可以找到3D坐标。通过世界坐标(x,y,z)计算哈希值的函数如下:
哈希函数
p1,p2,p3是三个大素数(常量),n是哈希表的大小。除了指向voxel block的指针外,哈希表中的每个条目还包含了世界坐标,和处理冲突时要用到的偏移指针(后续会提到)。
哈希表项的数据结构如下:
Voxel Hashing阅读笔记_第3张图片
voxel Hashing数据结构
Voxel Hashing阅读笔记_第4张图片
无限统一的网格划分了世界。使用山列函数可以从世界坐标映射到散列桶,散列桶中的条目对应着一个个的体素块,体素块中存储着8^3个voxel。当添加红色的块时,因为其映射到的hash table中不是空闲的,所以发生了冲突,这个冲突可以通过哈希桶中的下一个可用条目解决。
冲突解决
当多个分配的块被映射到同一哈希值,则会出现冲突。作者通过将哈希表统一组织成bucket来解决冲突,每个bucket对应着唯一的哈希值,每个bucket中顺序存储着少量的哈希条目。当发生冲突时,将voxel block的指针存储在bucket下一个可用的顺序条目中。为了找到某个世界坐标对应的voxle Block,首先对这个坐标计算哈希值,通过哈希值映射到对应的bucket。在对应的bucket中进行遍历,上文已经说了,每个哈希条目中还存储了对应的世界坐标,我们将条目中存储的坐标和待查询的坐标进行比对,从而找到对应的voxel block。
通过选择合理的哈希表和bucket的大小,很少会出现bucket溢出的情况。但是如果发生了这种情况,那么就从该bucket的最后一个条目的offset得到一个空闲的slot用来存储信息。每个bucket保证自己最后一个slot一定是用来存储哈希值本就属于这个bucket的表项。
Voxel Hashing阅读笔记_第5张图片
哈希操作
(1)插入
当插入新的哈希表项时,首先计算哈希值,然后确定目标bucket。然后遍历bucket中的所有元素,包括通过最后一项连接的其他表项。如果找到了一个具有相同世界坐标的表项,那么立即返回对应的voxel block,如果没有找到,则从bucket中寻找第一个空位置。如果bucket中的某个位置可以用,则直接插入,若bucket满了,则将一个元素附加到他的链表元素。
为了避免并行时会产生竞争错误,因此对于要插入的bucket会增加一个锁,一旦某个bucket被锁定用于写入,那么同一个bucket的其他分配将会交错进行,这可能会产生一些延迟,但是很好的保证了重建的质量。
(2)检索
先对待检索的世界坐标计算哈希值,通过计算的哈希值对应到bucket,在目标bucket进行线性搜索,如果bucket的最后一个表项设置了offset那么还要遍历这个列表,将query position和表项中存储的position进行对比,找到一样的为止。
(3)删除
删除操作的前半部分和检索操作一致,这里就不再赘述,当找到目标表项之后要根据目标表性所处的位置进行判断,不同的位置有不同的删除策略。如果待删除的表项不在补充的列表内,则直接删除即可。如果待删除的表项是bucket的最后一个元素,也就是说待删除表项是列表头,则将表项offset指向的散列条目复制到bucket的最后一项,并将原来的删除。如果待删除表项是链表中的非头元素,则删除它还需要相应的更正列表。具体的步骤如上图所示。直接在bucket内删除不需要同步,但是如果需要修改链表,那么必须原子锁定bucket,并错开此bucket的列表操作,直到下一帧为止。

Voxel Block Allocation(体素块分配)

在tracking得到了相机的位姿之后,要将新输入的深度图像融合到模型当中。作者采用了DDA算法,得到一系列的与ray相交的blocks。
Voxel Hashing阅读笔记_第6张图片
如果这些blocks中有尚未分配的,就在哈希表中插入对应的表项,并在GPU上分配相应的内存用来存储voxel block的数据。GPU中的存储空间是预留的,以链表的形式进行连接。当有一个voxel block需要分配内存,则从列表的尾部取相应的内存进行存储,一旦有voxel block释放内存,也相应的将内存挂到列表的尾部。

Voxel Block Integration

在上一步中,作者将曲面可见截断区域内的所有voxel block进行了分配,这一步作者对位于相机视锥范围内的所有已分配voxel block进行更新。
Voxel Block Selection
首先并行的访问所有的哈希表项,并声明一个数组用来标识对应的voxel block。若某个表项对应的voxel block被占用且在相机的视锥范围内,那么就在数组中对应的位置置1,否则置0。然后通过parallel prefix sum和三级上下扫描技术对数组进行check。将那些即被占用又可视的voxel block的哈希表项复制到缓冲区,便于后续连续访问。

Voxel Hashing阅读笔记_第7张图片
Implicit Surface Update
对Selection出来的哈希表条目进行t-sdf更新处理。为每个voxel block分配一个GPU内核,为block中每一个voxel分配一个单独的线程,这样能够极大地提高缓存命中率,最小化代码差异。
更新voxel block需要更新其中的tsdf、颜色、和权重信息。利用running average算法进行积分,并对不同的深度值设置不同的积分权重来整合传感器的噪声特性,即深度较小的数据会获得更大的权重。作者对所有的落入相机可视范围内的voxel block都进行了更新,但是有部分voxel block并不在sdf的截断区域内。所以作者在集成完毕后还会对所有的voxel block进行评估,识别其中不具备价值的块,将其从哈希表中删除并释放内存。
Garbage Collection
这一步作者对selection中得到的压缩哈希表进行操作,对表中的每个关联的voxel blcok计算最小绝对T-SDF和最大权重。如果某个voxel block的最大权重为0,或者最小绝对T-SDF小于阈值,就将其标记为删除。在第二遍的遍历过程中,将标记为删除的哈希表项删除,并释放其在GPU中的内存。

Surface Extraction

作者使用raycasting以提取当前视角下的重建表面。在做 raycasting 前,需要计算每个 ray 的出发点和结束点,也就是需要找到重建好的 block 的 bounding box。方法是,对于每个 block,计算可以囊括 block 的三角面片,然后对于所有的 block 采用 shader 计算当前视角下的最小 z buffer 值和最大 z buffer 值,最小 z buffer 对应 raycasting 时 ray 的起点,最大 z buffer 对应 raycasting 时 ray 的终点。在从起点到终点进行匹配时,需要使用tri-linear方法,该方法需要相邻的8个voxel的sdf值。为了解决跨block取值的问题,在获得相邻voxel的sdf值的时候,作者采用从哈希表进行查找的方式。
为了提高效率,在搜索匹配的时候,作者先跳过了一个预先定义的值(最小截断的一半),这样能够在保证不错过等值面的前提下较快的逼近结果,一旦检测到了等值面,就采用iterative line search来进行搜索。
Tracking
利用当前帧和下一帧的深度信息进行ICP求解,从而得到相机pose。

Streaming

为了提高GPU内存的利用率,充分使用GPU的性能,作者采用了GPU-host流传输的方法,保证GPU内存中保存的都是相机视锥内的voxel block。Voxel Hashing阅读笔记_第8张图片
图中圈是以相机光心前4m,半径8米画的圆。
GPU-to-host
并行访问 hash 表并且标记移除从 active region 移出去的 hash entry 和 hash block,对于所有待移出去的 hash entry,先将其拷贝到 intermediate buffer,然后再将 hash entry 对应的 block 拷贝到另外的 intermediate buffer,删除掉原始的 hash entry 和 block,并且将 intermediate buffer 中的 hash entry 和 block 拷贝到host。在host中,voxel 不按照 hash 表存储,而是将空间划分成等大小 chunks(1 m^3),每个 chunk 对应的 voxel block 用链表的形式链接起来,block 对应的 hash entry 也存起来。
Host-to-GPU Streaming
对于 Host 向 GPU 的移动,找到完全在相机视场内的 chunk,GPU-to-CPU 是以 block 的单位移动的,CPU-to-GPU 以 chunk 为单位移动。移动时,每次只移动和相机视角最近的单个 chunk。移回到 GPU 的 block,重新插入哈希表并分配内存。
Stream and Allocation Synchronization
HOST和GPU中存储的block必须进行同步,不然会造成内存泄漏。为了解决这个问题,作者提出了在GPU上建立一个binary occupancy grid,binary occupancy grid的每一个表项对应着一个特殊的chunk,如果这个chunk在GPU中且可以进行allocation,否则的话就认为这个chunk在HOST上,不能再GPU上allocation。

以上是对Voxel Hashing论文的学习,有不对的地方希望各位能及时指正,谢谢。

你可能感兴趣的:(Voxel Hashing阅读笔记)