由于接下来的研究工作需要使用SMPL模型渲染人体不同姿态的图像,这里先对SMPL模型的代码进行复现。
环境配置
由于smpl的代码是针对python2实现的,这里在自己的Window上,使用Anaconda3+Pycharm进行复现(VS Code已不支持Python2,所以不采用)。
1. 配置虚拟环境
conda create -n smpl python=2.7
2. 安装相关库
conda install numpy scipy bottleneck pyopengl pillow
个别库的安装会出现问题,这里一一进行介绍:
- opencv-python:需要指定版本使用pip安装,否则安装不上。
pip install opencv-python==3.3.0.10
- chumpy:在创建好虚拟环境之后,会提示你使用的pip版本过低让你进行更新。不要进行更新,更新后的pip安装chumpy会报错。可以使用下面的指令将pip版本降低,之后可正常安装。
python -m pip install pip==9.0.3
pip install chumpy
- opendr:
这个包的安装是smpl代码复现最困难的地方,我原本计划在python3上进行复现,但这个包的安装失败,且出现的问题在两天的查阅后还是没有解决。在Python2.7如果出现安装失败的情况,如下图。
可以从mirrors / polmorenoc / opendr · GitCode下载源码,后切换到 opendr-master/opendr目录下进行安装。
后续发现在window上安装后也会出现各种问题,特别是安装后存在ColoredRenderer' object has no attribute 'vbo_verts_face'的问题,查阅资料后发现需要安装更新版本的opendr,上述安装的是0.73的版本。但是在安装opendr-0.78时,一个包(http://files.is.tue.mpg.de/mloper/opendr/osmesa/OSMesa.Windows.AMD64.zip)的url已经失效,且网上未找到可用的下载地址,故更改为在Ubuntu上部署环境,上面的步骤一致,到安装opendr时使用 pip install opendr即可成功安装。
安装完成之后就可以import一下opendr库看一下环境是否配置完毕。
代码复现及详解
hello_smpl.py
- 运行结果:输出hello_smpl.obj
- 代码详解:
1. 首先是读取标准模型(ready_arguments),(backwards_compatibility_replacements)先对老版本进行兼容,得到一个包含J_regressor_prior等字段的字典。
2. 加入必要的字段,计算shape parameter得到v_shaped,表示在标准模型上变化得到的新体形。计算关节点位置J,并计算pose的到新的体型 v_posed。
3. 使用lbs进行骨骼蒙皮计算,得到新的姿态及新的体型的模型,load_model返回chumpy.ch_ops.add对象。在该对象上直接修改pose及betas参数,将自动重新计算新的姿态及模型。可以通过下面对hello_smpl.py的line51代码前后的结果看处。
执行前:
执行后:
render_smpl.py
这部分代码主要涉及到opendr的渲染代码,网上缺少注释和文档只能自己看源码,这里边读源码边更新了。
chumpy
由于opendr是基于chumpy写的,需要先了解chumpy的一些源码。
首先看到在渲染时调用了ColorRenderer的r()函数。
cv2.imshow('render_SMPL', rn.r)
该函数实际上是chumpy包中Ch对象的函数:
@property
def r(self):
self._call_on_changed()
if self._cache['r'] is None:
self._cache['r'] = np.asarray(np.atleast_1d(self.compute_r()), dtype=np.float64, order='C')
self._cache['rview'] = self._cache['r'].view()
self._cache['rview'].flags.writeable = False
return self._cache['rview']
def _call_on_changed(self):
if hasattr(self, 'is_valid'):
validity, msg = self.is_valid()
assert validity, msg
if hasattr(self, '_status'):
self._status = 'new'
if len(self._dirty_vars) > 0:
self.on_changed(self._dirty_vars)
object.__setattr__(self, '_dirty_vars', set())
def on_changed(self, terms):
pass
可以看到调用r()时会更新对象中的数据,其中的_dirty_vars是每次在修改实例中的对象时,就会记录。on_changed函数是交由子类实现的。然后看到ColorRenderer类中,对该接口进行了实现。
renderer.py
def on_changed(self, which):
if 'frustum' in which:
w = int(self.frustum['width'])
h = int(self.frustum['height'])
self.glf = OsContext(w, h, typ=GL_FLOAT)
self.glf.Viewport(0, 0, w, h)
self.glb = OsContext(w, h, typ=GL_UNSIGNED_BYTE)
self.glb.Viewport(0, 0, w, h)
if 'frustum' in which or 'camera' in which:
setup_camera(self.glb, self.camera, self.frustum)
setup_camera(self.glf, self.camera, self.frustum)
if not hasattr(self, 'num_channels'):
self.num_channels = 3
if not hasattr(self, 'bgcolor'):
self.bgcolor = Ch(np.array([.5]*self.num_channels))
which.add('bgcolor')
if not hasattr(self, 'overdraw'):
self.overdraw = True
if 'bgcolor' in which or ('frustum' in which and hasattr(self, 'bgcolor')):
self.glf.ClearColor(self.bgcolor.r[0], self.bgcolor.r[1%self.num_channels], self.bgcolor.r[2%self.num_channels], 1.)
这里将场景上下文及相机进行了设置,同时设置好了背景的颜色。glf和glb看不到里面的源码,只能看到定义了许多接口。接下来看这个setup_camera接口,里面主要是对camera进行设置。
camera.py
def setup_camera(gl, camera, frustum):
_setup_camera(gl,
camera.c.r[0], camera.c.r[1],
camera.f.r[0], camera.f.r[1],
frustum['width'], frustum['height'],
frustum['near'], frustum['far'],
camera.view_matrix,
camera.k.r)
可以看到设置处,首先计算了camera的c和f的r,这里需要看render_smpl.py中的代码。
rn.camera = ProjectPoints(v=m, rt=np.zeros(3), t=np.array([0, 0, 2.]), f=np.array([w,w])/2., c=np.array([w,h])/2., k=np.zeros(5))
这里设置了c和f的值,c表示像素原点的偏移量、f表示焦距与缩放比例的乘积,属于相机内参。rt、t属于相机外参,分别对应选择矩阵及平移。查阅资料后发现,k可能为畸变参数。接下来看_setup_camera内部。
def _setup_camera(gl, cx, cy, fx, fy, w, h, near, far, view_matrix, k):
k = np.asarray(k)
gl.MatrixMode(GL_PROJECTION)
gl.LoadIdentity();
# 反向计算图像的上下左右在世界坐标系下的范围
f = 0.5 * (fx + fy)
right = (w-(cx+pixel_center_offset)) * (near/f)
left = -(cx+pixel_center_offset) * (near/f)
top = -(h-(cy+pixel_center_offset)) * (near/f)
bottom = (cy+pixel_center_offset) * (near/f)
gl.Frustum(left, right, bottom, top, near, far)
gl.MatrixMode(GL_MODELVIEW);
gl.LoadIdentity(); # I
gl.Rotatef(180, 1, 0, 0) # I * xR(pi)
view_mtx = np.asarray(np.vstack((view_matrix, np.array([0, 0, 0, 1]))), np.float32, order='F')
gl.MultMatrixf(view_mtx) # I * xR(pi) * V
gl.Enable(GL_DEPTH_TEST)
gl.PolygonMode(GL_FRONT_AND_BACK, GL_FILL)
gl.Disable(GL_LIGHTING)
gl.Disable(GL_CULL_FACE)
gl.PixelStorei(GL_PACK_ALIGNMENT,1)
gl.PixelStorei(GL_UNPACK_ALIGNMENT,1)
if np.any(k):
if not hasattr(gl, 'distortion_shader'):
program = gl.CreateProgram()
vs = gl.CreateShader(GL_VERTEX_SHADER)
gl.ShaderSource(vs, 1, vs_source, len(vs_source))
gl.AttachShader(program, vs)
# fs = gl.CreateShader(GL_FRAGMENT_SHADER)
# gl.ShaderSource(fs, 1, fs_source, len(fs_source))
# gl.AttachShader(program, fs)
gl.LinkProgram(program)
gl.UseProgram(program)
gl.distortion_shader = program
gl.UseProgram(gl.distortion_shader)
if len(k) != 8:
tmp = k
k = np.zeros(8)
k[:len(tmp)] = tmp
for idx, vname in enumerate(['k1', 'k2', 'p1', 'p2', 'k3', 'k4', 'k5', 'k6']):
loc = gl.GetUniformLocation(gl.distortion_shader, vname)
gl.Uniform1f(loc, k[idx])
else:
gl.UseProgram(0)
这里使用相机内外参设置场景,由于模型的k都为零,不考虑畸变的情况。执行完on_change后,会执行compute_r接口,在render的该接口中先调用了camera的r接口,然后调用camera的compute_r接口。
def compute_r(self):
return self.r_and_derivatives[0].squeeze()
@depends_on('v', 'rt', 't', 'f', 'c', 'k')
def r_and_derivatives(self):
v = self.v.r.reshape((-1,3)).copy()
return cv2.projectPoints(v, self.rt.r, self.t.r, self.camera_mtx, self.k.r)
可以看到这里就调用了v的r接口,即重新计算了新的pose和shape参数下的smpl模型,并获取了各顶点新的三维位置,之后调用opencv的接口,其中camera_mtx是相机的内参矩阵。
查看opencv的官方文档,可以看到该接口的声明,返回渲染后的顶点的图像坐标和雅可比矩阵。
cv.projectPoints( objectPoints, rvec, tvec, cameraMatrix, distCoeffs[, imagePoints[, jacobian[, aspectRatio]]] ) -> imagePoints, jacobian
对程序进行打断点可以看到,计算后camera.r的大小确实为[6890, 2]。但是只是传给了tmp中间变量,后面返回color_image,有点不理解,继续往后看。
color_image接口当中主要是对场景中的点进行着色。
draw_colored_verts(gl, self.v.r, self.f, self.vc.r)
def draw_colored_verts(gl, v, f, vc):
# TODO: copying is inefficient here
if vc.shape[1] != 3:
vc = np.vstack((vc[:,0], vc[:,1%vc.shape[1]], vc[:,2%vc.shape[1]])).T.copy()
assert(vc.shape[1]==3)
gl.EnableClientState(GL_VERTEX_ARRAY);
gl.EnableClientState(GL_COLOR_ARRAY);
gl.VertexPointer(np.ascontiguousarray(v).reshape((-1,3)));
gl.ColorPointerd(np.ascontiguousarray(vc).reshape((-1,3)));
gl.DrawElements(GL_TRIANGLES, np.asarray(f, np.uint32).ravel())
该接口主要是对场景中的smpl模型进行着色,可以看到最后是基于smpl的三角形面片进行着色的。之后通过gl的getImage获取渲染后的图像,具体实现看不到源码。
一些可视化
对camera.r的图像坐标进行可视化,发现将smpl的点云映射到图像平面后,实际上只有一些像素有点,是相对稀疏的。所以在绘制时需要基于三角形面片来进行着色。
统计后发现许多位置是对应了多个顶点的,最后的一个像素对应了20个顶点,如何确定其最终的颜色?如何获取该像素的标注信息?并不清楚其实现细节。