本文参考:Reference:Real-time 3D Reconstruction at Scale using Voxel Hashing
本文由CSDN用户CV_X.Wang撰写,部分参考内容来源于CSDN公开博客。
以下内容为本人的PPT汇报的汇总,可能会存在部分的前后语义逻辑存在问题等情况,如需原始PPT,请私聊联系本人!
一、哈希表
哈希表(Hash table),是根据键值(Key)而直接访问在内存存储位置的数据结构。即它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数也叫哈希函数,存放记录的数组称做哈希表。
哈希表本质上是数组+链表/二叉树的形式。不同的形式主要取决于解决哈希碰撞的方式。
哈希表为什么能快速查找呢,我们举个例子,一个通俗的例子是,为了查找电话簿中某人的号码,可以创建一个按照人名首字母顺序排列的表(即建立人名到首字母F(x)的一个函数关系),在首字母为W的表中查找“王”姓的电话号码,显然比直接査找就要快得多。这里使用人名作为关键字,“取首字母”是这个例子中散列函数的函数法则F(x),存放首字母的表对应散列表。关键字和函数法则理论上可以任意确定。
哈希表通常存储的是键值对,本质上就是一个数组。比如说,在哈希表中,由A映射到B,那么A叫做键值,B叫做哈希值。
二、体素块
从概念上讲,无限的均匀网格将世界细分为体素块。每个块都是一个小的规则体素网格,如下图所示。体素块由 8*8*8 体素组成。 每个体素存储一个 TSDF、颜色和权重。代码结构如下图所示。
三、SDF(Signed Distance Function)
SDF——假设相机光心到表面的距离为ds,到射线上体素voxel x的距离为dv ,则符号距离就是SDF(x)=dx,显然,当dx>0时,表示体素在平面前方,当dx<0时,表示体素在平面后方。
在空间中的一个有限区域上确定一个点到区域边界的距离并同时对距离的符号进行定义:点在区域边界内部为正,外部为负,位于边界上时为0。
四、TSDF(Truncated Signed Distance Function)
此处的x指的是图中的range,x>阈值,在表面的前面。X<阈值,在表面的后面。
TSDF——哈希表是稀疏的,通俗而言,什么是稠密,就是有个连续的函数,什么是稀疏,就是若干散点,为什么要用TSDF呢,因为TSDF可以通过设置阈值的形式,将若干散点在阈值范围内将其拟合成一个连续的函数。
体素的概率值是更新体素距离的方式。因为体素距离平面过远时,其数值对于平面影响过小。因此通常都会采用 TSDF 设定一个阈值 δ𝛿 来截断,类似上图。
当体素距离表面较近时,由于其对于表面的影响较大,当离着表面的距离超过了阈值,我们就给它设定为一个固定值。用以滤除。
五、哈希条目(Hash Entries)
为了利用哈希表的稀疏性,体素块仅分配在重建的表面几何形状周围。可以通过使用高效的GPU加速哈希表(Hash Table)来管理体素块的分配和检索。
哈希表中存储有哈希条目(Hash Entries),每个条目都包含一个指向已分配的体素块的指针。可以使用整数世界坐标 (x, y, z) 从哈希表中检索体素块。通过简单的乘法和舍入,找到世界空间中三维点的坐标。使用以下哈希函数可以从世界坐标 (x, y, z) 映射到哈希值 H(x, y, z):
注:⊕ 异或运算;mod取模运算;上式中p1,p2,p3为大素数: 73856093,19349669, 83492791;n为哈希表的大小。
除了存储指向体素块的指针之外,每个哈希条目还包含关联的世界位置和一个用于有效处理哈希碰撞的指针。
六、哈希函数
哈希函数——左边有若干个哈希条目,每个哈希条目都有他的Position,我们现在定义一个Key,假设Key=x+y+z。那么现在就存在一个情况,不同的Position存在对应同一个Key的情况,而Key经过哈希函数后,又与Value一一对应,这就说明了,为什么通过哈希函数后是多对一的关系。 右边就是哈希表。
七、哈希碰撞
从概念上讲,我们可以采用无限的统一网格划分整个世界。而后使用哈希函数,可以将世界坐标映射到哈希桶,哈希桶存储指向规则网格体素块的指针的小数组(哈希条目)。每个哈希桶都是由若干哈希条目组成的,无数的哈希桶,共同构成了一个哈希表。
解决哈希碰撞的方法由两种,一个是开放寻址法,一个是链表法。
一个hash value对应一个bucket,每个bucket有4个entry. 一个coord经过hash函数计算得到一个hash value,如果对应的bucket里面有值,那么可以放到此entry后面一个entry。当寻找这个coord,(hashtable.find(coord)),先找到bucket,然后在bucket中遍历entry, 比较coord.
哈希桶内的遍历过程就是线性遍历,就是挨个搜索遍历。
1.开放寻址法
这里所说的开放寻址法其实简单来说就是,既然位置被占了,那就另外再找个位置不就得了,怎么找其他的位置呢?这里其实也有很多的实现,我们说个最基本的就是既然当前位置被占用了,我们就看看在该哈希桶内的后一个位置是否可用,也就是1的位置被占用了,我们就看看2的位置,如果没有被占用,那就放到这里。当然,也有可能2的位置也被占用了,那咱就继续往下找,看看3的位置,一次类推,直到找到空位置。
也就是说,每个哈希桶中包含多个哈希条目,但对应同一个哈希值。
哈希表如何读取数据?
比如我们现在要通过学号102011来查找学生的姓名,怎么操作呢?我们首先通过学号利用哈希函数得出位置1,然后我们就去位置1拿数据,拿到这个Entry之后我们得看看这个Entry的key是不是我们的学号102011,一看是101011,这不是我们要的key啊,然后根据这个键值对的next知道下一给位置,在比较key,好成功找到李四。
2.哈希桶溢出
一般,如果哈希表和哈希桶的尺寸选择合理,那么哈希桶将会很少溢出。为了处理溢出情况,可以扩展哈希条目(Hash Entries) ,增添一个偏移量offset,用来指示下一个哈希条目的位置。
即当插入新的哈希条目a时,但对应的哈希桶A已经满时,我们顺着哈希表,寻找其他哈希桶中的空闲位置,如果找到,在该空闲位置插入该哈希条目,并在哈希桶A中的最后一个哈希条目的offset记录新插入的哈希条目的位置。如果该哈希桶A还有新的哈希条目,我们使用之前新插入的哈希条目a的offset记录哈希条目b的位置。这样,由溢出的哈希条目及其offset字段形成了链表,我们称为linked list。其中,注意,溢出的哈希条目不能占用哈希桶的最后一个条目位置,该位置需要存储该哈希桶的linked list的表头。
3.链表法
就像图中所示,现在张三和李四都要放在1找个位置上,但是张三先来的,已经占了这个位置,待在了这个位置上了,那李四呢?解决办法就是链表,这时候这个1的位置存放的不单单是之前的张三的键值对了,此时的键值对还额外的保存了一个next指针,这个指针指向数组外的另外一个位置,将李四安排在这里,然后张三那个键值对中的next指针就指向李四的这个位置,也就是保存的这个位置的内存地址,如果还有冲突,那就把又冲突的那个Entry放在一个新位置上,然后李四的Entry中的next指向它,这样就形成了一个链表。但是该方法有一个问题,如果冲突的很多,那么链表就要进一步的增长,转换频繁也会影响性能的。
八、哈希操作
插入:首先将Key通过哈希函数找到其对应的Value,进而确定哈希桶。而后迭代桶内所以的哈希条目,包括由于哈希桶溢出导致放到了其他位置的哈希条目,找到最后一个已经输入的哈希条目,在其后,将新的哈希条目插入。
删除:删除哈希条目与插入类似。 首先通过哈希函数计算Value,然后线性搜索找到相应的哈希桶。
(1)如果删除对象在该哈希桶中,且不在最后一个位置(该位置存储linked list表头),那么直接删除(如左图第四行)。
(2)如果删除对象在哈希桶中的最后一个条目位置,那么,将linked list中的倒数第二个哈希条目修改为相应的offset值(False),以保证linked list的正确性。
(3)如果删除对象在linked list(不包括哈希桶中的最后一个条目位置),那么直接删除对象,并修改相应的offset值。