SMPL模型代码复现

由于接下来的研究工作需要使用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个顶点,如何确定其最终的颜色?如何获取该像素的标注信息?并不清楚其实现细节。

  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值