DeepSDF论文复现1---数据集生成2---原理解析
DeepSDF论文复现1—数据集生成2—原理解析
上一篇博客DeepSDF论文复现1—数据集生成1—官方代码运行 介绍了使用官方代码实现DeepSDF数据集生成的流程。这篇博客我们将深入代码内部,来看看其原理以及代码存在的一些问题。
数据长啥样
在正式分析代码之前,先看看数据集中的数据长什么样。首先定位到上一篇文章提到的DeepSDF/bin
这个位置,这里面有我们生成的两个可执行文件。其中,PreprocessMesh
用于将数据从obj格式转换为DeepSDF训练需要的npz格式。
为了看看转换前后的效果,可以去之前下载的ShapeNet数据集中随便找一个压缩包解压出来,然后再随便找到其中一个模型,将obj文件拷贝到DeepSDF/bin
中,比如本人拷贝了一个飞机模型长下面图1这样:
然后,通过如下命令调用PreprocessMesh
生成一个名字叫output.npz的文件:
./PreprocessMesh -m model_normalized.obj -o ./output.npz
再用如下代码给它可视化出来,即可得到图2所示的结果
import numpy as np
import open3d as o3ddef visualize_deepsdf_data(data):"""可视化DeepSDF数据(区分表面内外的点)- 表面外的点:红色(近)→ 蓝色(远)- 表面内的点:绿色(近)→ 蓝色(远)"""# 提取数据pos_data = data['pos'] # 表面内的点 (距离 < 0)neg_data = data['neg'] # 表面外的点 (距离 > 0)# 合并所有点all_xyz = np.vstack([pos_data[:, :3], neg_data[:, :3]])all_distances = np.hstack([pos_data[:, 3], neg_data[:, 3]])# 创建点云pcd = o3d.geometry.PointCloud()pcd.points = o3d.utility.Vector3dVector(all_xyz)# 初始化颜色数组colors = np.zeros((len(all_distances), 3))# 区分表面内外的点pos_mask = all_distances < 0 # 表面内的点neg_mask = all_distances >= 0 # 表面外的点# 对表面内的点:绿色(近)→ 蓝色(远)if np.any(pos_mask):pos_distances = np.abs(all_distances[pos_mask]) # 取绝对值pos_normalized = (pos_distances - pos_distances.min()) / (pos_distances.max() - pos_distances.min() + 1e-6)colors[pos_mask, 1] = 1.0 - pos_normalized # 绿色通道colors[pos_mask, 2] = pos_normalized # 蓝色通道# 对表面外的点:红色(近)→ 蓝色(远)if np.any(neg_mask):neg_distances = all_distances[neg_mask]neg_normalized = (neg_distances - neg_distances.min()) / (neg_distances.max() - neg_distances.min() + 1e-6)colors[neg_mask, 0] = 1.0 - neg_normalized # 红色通道colors[neg_mask, 2] = neg_normalized # 蓝色通道# 赋值颜色pcd.colors = o3d.utility.Vector3dVector(colors)# 可视化o3d.visualization.draw_geometries([pcd],window_name="DeepSDF数据可视化",width=800,height=600,point_show_normal=False)if __name__ == "__main__":data = np.load("output.npz")print("文件构成:", data.files)print("表面内点(距离 < 0):", data['pos'].shape)print(data['pos'])print("表面外点(距离 >= 0):", data['neg'].shape)print(data['neg'])visualize_deepsdf_data(data)
可以看到,模型外部的点为红色,内部的点为绿色,离模型表面越近则绿色或者红色越明显,训练DeepSDF模型要的数据就是这些点的组合。其中,点包括xyz三个空间坐标值,属于模型内部或外部的一个属性值以及与模型表面的距离这五个值。
另外,如果观察仔细点可以发现两个问题:
- 飞机的尾部细节不见了在转换过程中不见了
- 模型的内部点的个数明显多于外部点的个数(内部点311447个,外部160615个。可能从可视化看不太出来,但可以从打印出来的信息看到)。
这两个问题都和数据生成的方法密切相关,下面我们将深入介绍该方法。
从obj到npz的原理
obj文件是三维体的描述文件,存储着组成三维体的所有的点,以及怎么用这些点构建成一个个不同的三角形来描述三维体的信息。
DeepSDF需要得到空间中的点是在三维体的内部还是外部,以及点与三维体表面的距离这两个信息,操作比较麻烦。原因在于下面两点:
-
怎么确定空间中的点在三维体的内部还是外部?
-
怎么知道哪些三角形是三维体的表面?
比如下面图3这架飞机模型,其内部有些结构(图4),构成这些结构的三角形就不是这架飞机的表面。
DeepSDF官方解决这两个问题的方法非常直观,步骤如下:
-
获得三维体的表面三角形,并存储必要信息
a. 计算三维体的中心,将三维体偏移到中心上,随后进行归一化。这样三维体的全部点将位于半径为1的球体内。
b. 在半径为1.1的球面上架设100个摄像头,然后朝坐标原点拍照,获得对应的图像。
c. 遍历获得的图像,在这些图像中出现过的三角形就是三维体的表面三角形,没出现过的自然为内部三角形。
d. 获得表面三角形的法线,设置法线的方向朝向摄像机的为正。
说明:一个三角形的法线有前后两个方向,官方代码将朝向摄像机的方向标记为法线方向。值得注意的是,这样处理存在一个问题。对于一个封闭的物体,无论哪个方位的摄像机都只能看到三角形的一个面,因此只能得到三角形唯一一个法线方向。然而,如果构成三维体的某些部分并不封闭,则不同方位的摄像头会看到三角形的前后两个面,也就是说它们会得到两个法线方向。因为后面需要用法线的方向来区分空间中的点是在三维体的内外还是外部,因此这里将出现一个不可解决的bug。为了解决这个问题,官方代码是把这个面直接丢掉,认为它不是三维体的表面。这也就导致了图2看到的“飞机的尾部细节不见了”(飞机尾部的浆是用单层三角形面构成的,只有面积没有体积,不封闭)。
e. 将表面三角形的第三个顶点保存起来,并用KD-Tree进行组织,从而可以较快获得空间上接近的点。
-
采样空间点,标记其在三维体的外部还是内部,并计算其与三维体表面的距离
a. 计算构成三维体的所有三角形的面积
b. 在所有三角形上依据面积占比均匀采样n个点(官方代码默认的采样点数为500000个,在三角形周围采样的点为总点数的47/50—-不要问为什么是47/50,问就是感觉可以就可以的感觉。然后因为会在点的附近添加扰动来获得该点附近的两个随机的点,因此这个n=500000*47/50/2)。
注意,官方代码用的是全部三角形进行采样,而非表面三角形。对于内部三角形,在其上面采样点再进行扰动偏移,得到的绝大部分的点还是会处于三维体的内部。因此,从这里可以知道采样出来的内部点个数将明显比外部点个数多。也就是上面提到的问题2的由来。c. 对这每个点进行xyz方向的随机偏移(使用两种不同的偏移方法),获得2n个点。
d. 因为还差500000*3/50个点,因此在空间中随机采样补足点数。
e. 对于每个采样到的点,搜索与其距离最近的表面三角形的K个顶点(通过上面的KD Tree,K的值由用户设置,默认是11),这样可以通过索引获得与采样点最近的K个表面三角形。
f. 将与采样点最近的顶点对应的三角形视为与采样点最近的三角形。SDF值设置为采样点与顶点的距离值。如果这个距离值比较小,则SDF值设置为采样点与顶点对应的三角形面的距离值。
上述操作是官方代码的操作,个人觉得有点草率。首先,与采样点最近的顶点对应的三角形不见得就是与采样点最近的三角形。另外,为什么距离较远的就直接用采样点与顶点的距离定义SDF值也让人困惑,感觉官方就是感觉可以就可以的感觉随便写的。。。从后续的结果看好像也还行。。。
g. 计算采样点与最近的K个表面三角形的距离,如果全部为正则证明点在三维体的外部,全部为负则点在三维体的内部。如果有正有负则丢弃这个点,所以我们看到最终采样出来的点一般都不到500000个。
尚存的不足的和解决方法/思路
除了上面提到的问题外,还有如下问题虽然只看官方代码比较难发现,但自己稍一尝试便能发现。
1. 三维体表面三角形获取不全
本人将官方代码中关于三维体表面三角形获取的代码用glfw实现了一遍,在参数设置基本和官方代码一致的情况下将表面三角形存储出来,发现存在不少表面三角形缺失的问题,如图5所示:
原因:使用的摄像机数量太少,朝向太单一,分辨率太低(源码中默认摄像机数量为100个,全部朝向坐标原点,分辨率为400*400)
解决方法:增加摄像机数量、朝向和分辨率。本人将摄像机数量改为500个,朝向从坐标原点一个改为15个,分辨率改为1600*1600,基本就可以做到精细提取三维体的表面,如图6所示。不过计算时间也增加了快50倍。。。
2. 三维体内部依然存在部分三角形面片
朝三维体内部看,可以发现依然存在一些三角形的面片没有被扔掉(如图7的红框),一开始百思不得其解,最后将线框也可视化才破案。
原因:三维体在建模的时候是按照部件构建的。如图8所示,有些构成部件的三角形本身即存在于内部又存在于外部。因此当其外部的部分被摄像机看到的时候,内部的部分也同时被感染。
解决方法:可以通过对三角形进行三角细分来缓解这个问题。如果要彻底解决这个问题,则需要计算三角形与其他三角形的相交部分,然后顺着相交线来进行三角细分。这样方可保证三角形在外部的部分不会感染到内部。
3. 三维体内部出现莫名奇妙的面片
在找到问题2的原因之后,我本以为问题已经全部解决。然而,仔细一看又发现了一些悬浮着的三角面片,百思不得其解,如图9所示。
原因:经过了漫长的分析,最终在飞机的尾部发现问题。从图10可以看到,飞机尾部并不是完全闭合的,有几个小孔。在这种情况下,摄像机是可以顺着小孔看到模型内部三角形的,这些三角形会被当成外部三角形。。。
解决方法:可以通过对三维体网格的修补来解决这个问题,然而如何修补三维体表面网格本身就是一个比较困难的事情。。。
总结
由这篇博客的原理分析看,DeepSDF在做数据集生成的时候并不是特别精确,有些地方的处理甚至可以算是简单粗暴,精度不高。然而,对于学术研究而言,DeepSDF的这种处理方法也无可厚非,毕竟文章关注的点并不在此处。数据集生成部分的优化有可能给训练结果带来精度的提升,但不会有质的变化。
后续本人有空,将写一个更完善的三维体表面提取代码供大家参考,届时再续写DeepSDF数据集生成这一部分。