Solutions to Common Problems in Pytorch3D Rendering

Pytorch3D Rendering 的一些疑难杂症

有哪些?

  1. 有了相机内参 K,而render又需要NDC坐标系,那要怎么定义相机?
  2. 图像的黄蓝色反了?
  3. render 完的图像锯齿很严重?怎么抗锯齿(Antialiasing)?
  4. 皮肤表面反光太强,光滑得像镜面一样,怎样更自然?
  5. 怎么物体只剩半截,更远的部分似乎被截掉了?
  6. 没解决的问题:PBR(physical based rendering)

1. 有了相机内参 K,而render又需要NDC坐标系,那要怎么定义相机?

这里的坑在于,camera本身支持任意坐标系,比如Freihand提供的是screen是224*224的相机坐标系。但是,render是默认NDC坐标系的!也就是normalized coordinate system,x和y是normalized到[-1,1]的。

一开始我直接把相机内参传给PerspectiveCameras,并且定义我的相机screen是224*224,像这样:

1
cameras = PerspectiveCameras(K=ks, image_size=((224,224),))

完全不报错,就是有问题:render 过后没东西在画面上。

解决:

我最后在官方文档找到不起眼的一句:

The PyTorch3D renderer for both meshes and point clouds assumes that the camera transformed points, meaning the points passed as input to the rasterizer, are in PyTorch3D's NDC space.

(世界坐标系 -> 相机坐标系 -> ndc坐标系 -> 图像坐标系)
(世界坐标系 -> 相机坐标系 -> ndc坐标系 -> 图像坐标系)

我一看,原来默认PerspectiveCameras是ndc坐标系的,in_ndc = False by default!

所以解决方法就是:

Screen space camera parameters are common and for that case the user needs to set in_ndc to False and also provide the image_size=(height, width) of the screen, aka the image.

那么加一个参数就好了,可是谁知道这问题困扰了我整整两三天:

1
cameras = PerspectiveCameras(K=ks, in_ndc=False, image_size=((224,224),))

另外,我还找到了如下这个等价方法,是先把内参转到NDC坐标系,再传给PerspectiveCameras。(至于为什么探索到这个方法,在后面问题 3 里可以找到原因…)

1
2
3
4
5
6
7
8
9
10
11
def get_ndc_fcl_prp(Ks):
ndc_fx = Ks[:, 0, 0] * 2 / 224.0
ndc_fy = Ks[:, 1, 1] * 2 / 224.0
ndc_px = - (Ks[:, 0, 2] - 112.0) * 2 / 224.0
ndc_py = - (Ks[:, 1, 2] - 112.0) * 2 / 224.0
focal_length = torch.stack([ndc_fx, ndc_fy], dim=-1)
principal_point = torch.stack([ndc_px, ndc_py], dim=-1)
return focal_length, principal_point

fcl, prp = get_ndc_fcl_prp(Ks)
cameras = PerspectiveCameras(focal_length=-fcl, principal_point=prp)

注意focal_length=-fcl,这个负号是为什么呢?这是另一个坑了哈哈哈哈。

答案是:pytorch3d坐标系的convention和我的相机不一样,它是+X指向左,+Y指向上,+Z指向图像平面外。这其中有个上下左右镜像的关系。

2. 图像的黄蓝色反了?

cv2的图像是BGR(老生常谈了),pytorch3d的是RGB。如果图像的黄蓝色相反了,基本就是这个问题,需要翻转一下,可以用torch的clip(dim=(2,))

3. render 完的图像锯齿很严重?怎么抗锯齿(Antialiasing)?

锯齿就是说像下图这样,物体的边缘很尖锐,像素点粒粒分明!

rand_4_skin_rendered_bad
rand_4_skin_rendered_bad

下面是我抗锯齿处理后的效果,可以看见边缘柔和了很多:

rand_4_skin_rendered
rand_4_skin_rendered

(我真的搞了一周这个问题……看看我的心路历程:

  1. 是不是 camera 没有用 NDC,而是直接用224x224的坐标系,导致投影过程有损失?所以我试了先转换成 NDC 坐标系的相机,再render。答案是,没有影响。
  2. 是不是 Shader 的参数设置得不对,比如 blur_radiusfaces_per_pixel 应该调大一些?这其实是一个很直观的想法了,甚至一个有经验的学长看了之后都告诉我应该是这个问题。可是当我疯狂调大这两个参数,发现并没有改变这个问题。blur_radius 只会让物体内部的材质更模糊,但是边缘的锯齿完全没改变。faces_per_pixel更是无益,几乎不影响效果。
  3. 是不是图像尺寸太小了(224x224),只能达到这么个效果?我首先测试了调大图像尺寸,到1024x1024,发现锯齿边缘的确是不明显了!可是我又看了相机拍摄的原始图像,虽然是有点模糊,但是不至于这么大的锯齿呀,肯定还有别的问题。)

解决:

终于,在这个issue里找到同样的问题:https://github.com/facebookresearch/pytorch3d/issues/399

解决方案是:

render at a higher resolution and then use average pooling to reduce back to the target resolution

居然这么暴力……不过issue里面有很详细的解释,也能理解,这就是render原理之外需要考虑的事情,甚至算不上什么bug。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
import torch.nn.functional as F

aa_factor = 3 # Anti-aliasing factor
raster_settings_soft = RasterizationSettings(
image_size=224 * aa_factor,
)
# ...
images = renderer(mesh)
images = images.permute(0, 3, 1, 2) # NHWC -> NCHW
images = F.avg_pool2d(images, kernel_size=aa_factor, stride=aa_factor)
images = images.permute(0, 2, 3, 1) # NCHW -> NHWC

4. 皮肤表面反光太强,光滑得像镜面一样,怎样更自然?

一开始,皮肤 render 出来像这样,跟陶瓷似的,像话吗:

rand_4_skin_rendered_bad2
rand_4_skin_rendered_bad2

改进后,效果这样,自然多了:

rand_4_skin_rendered_big
rand_4_skin_rendered_big

解决:

其实搞清楚材质相关的一些参数就好了。主要来说,这个反光是由这两个量决定的:

  1. specular_color: specular reflectivity of the material,指定镜面反射颜色,在表面有光泽和镜面般的地方看到的颜色。
  2. shininess:定义材质中镜面反射高光的焦点。 值通常介于 0 到 1000 之间,较高的值会产生紧密、集中的高光。

注意这里是改物体material的这些参数。虽然lighting也有这些参数定义,但这是关于光源的,和这个反光没有关系。

所以修改很简单:定义materials类,调整specular_color。默认是1,1,1,就是纯白色;调成0.2,0.2,0.2比较适合人的皮肤。

1
2
3
4
5
6
7
8
9
10
11
12
from pytorch3d.renderer import Materials

materials = Materials(
specular_color=((0.2, 0.2, 0.2),), # 默认是1,1,1,就是纯白色;测试发现调成0.2,0.2,0.2比较适合人的皮肤。
shininess=30, # 默认值是 64,看上去高光稍微有点聚集了,改成30的话略自然,差别不太明显
)
renderer_p3d = MeshRenderer(
rasterizer=MeshRasterizer(),
shader=HardPhongShader(
materials=materials,
),
)

5. 怎么物体只剩半截,更远的部分似乎被截掉了?

还是一只手的模型,render 出来居然只有半个手背,距离相机更远的部分像是被截断了:

rand_1_skin_rendered_half
rand_1_skin_rendered_half

改进后,正常的效果应该是这样才对:

rand_1_skin_rendered_full
rand_1_skin_rendered_full

所以问题出在哪呢?的确是“更远的部分被截掉了”。我找到了RasterizationSettings里有这么一个相关的参数:

  • z_clip_value: if not None, then triangles will be clipped (and possibly subdivided into smaller triangles) such that z >= z_clip_value. This avoids camera projections that go to infinity as z->0. Default is None as clipping affects rasterization speed and should only be turned on if explicitly needed. See clip.py for all the extra computation that is required.

可是问题不在这个参数上,因为它的默认值就是None,应该在后续都没有影响。

解决:

经过仔细看源码,我发现问题出在SoftPhongShader……具体来说,在shader.py 第138-139行,SoftPhongShaderforward函数里:

1
2
znear = kwargs.get("znear", getattr(cameras, "znear", 1.0))
zfar = kwargs.get("zfar", getattr(cameras, "zfar", 100.0))

居然有一个默认的z范围[1,100]……………………所以其实是我的mesh的scale太大了,再加上相机的dist比较大,整个深度就超过zfar了。所以有两种方法,要么缩小一下mesh的尺度;要么不想改变原数据的话,在render的时候,把znear zfar参数额外传入,如下:

1
images = renderer(mesh, ..., znear=-2.0, zfar=1000.0)

6. 没解决的问题:PBR(physical based rendering)

我的数据中3D mesh的材质用了PBR(physical based rendering)。它提供三张贴图图像:diffuse map,specular map和normal map。

但是pytorch3d目前并不支持PBR inspired shading(see issue)。

所以目前我只能把diffuse map作为一般意义上的texture map,而忽略了specular map和normal map这两张图。

我不确定能不能自己实现这部分功能,比如自定义 phong_shading函数(参考issue)。但这有点超出我的能力范围和精力范围,所以暂时搁置了。如果能实现的话,PyTorch3D 似乎是欢迎contribution的(issue