写在前面

  • 为了保证整个示例项目更加直观,方便理解,在展示一些函数的源码时会使用numpy版本进行展示,而在示例程序中并未使用numpy版本的库,在Cython版本与numpy版本出现差异的原码前会有标注,希望读者留意。
  • 3DMM实例程序的jupyter版本后续会更新,完全免费,欢迎大家下载

源码解析

在上一篇文章了解3DMM模型以及用随机的形状系数和表情系数生成面部网格进行3DMM模型的前向重建过程后进入例程的后半部分—— 由2D图像点和对应的3D顶点索引得到新的参数进而从二维图片进行三维人脸的重建。

理论部分

理论这部分借鉴了大佬的文章和一些论文。
从上篇文章我们了解了3DMM模型的公式:

通过一张单张正脸照片,首先利用人脸对齐算法计算得到目标二维人脸的68个特征点坐标xix_ixi​,在BFM模型中有对应的68个特征点XiX_iXi​,投影后忽略第三维,则特征点之间的对应关系如下:

根据这些信息求出α,β\alpha, \betaα,β系数,将平均脸模型与照片中的脸部进行拟合,即:

因此,三维人脸重建问题再次转化为求解系数(α,β\alpha,\betaα,β)以满足下列能量方程的问题:


人脸模型的三维点以及对应照片中的二维点存在映射关系,这个可以由一个3x4的仿射矩阵PPP进行表示。即:X=PA⋅X3dX=P_A\cdot X_{3d}X=PA​⋅X3d​。

黄金标准算法

要计算出仿射矩阵,代码中使用了黄金标准算法(Gold Standard algorithm)
算法如下:
目标为给定n>=4组3维(XiX_iXi​)到2维(xix_ixi​​)的图像点对应,确定仿射摄像机投影矩阵的最大似然估计。

  • 归一化,对于二维点(xix_ixi​​),计算一个相似变换TTT,使得 xˉ=Txi\bar{x} =Tx_ixˉ=Txi​​,同样的对于三维点,计算Xˉ=UXi\bar{X}=UX_iXˉ=UXi​​
  • 对于每组对应点 xix_ixi​~XiX_iXi​​,都有形如 Ax=bA x = bAx=b 的对应关系存在
  • 求出A的伪逆
  • 去掉归一化,得到仿射矩阵

在Face3d中的求解过程

过程可以概述如下:
(1)初始化α,β\alpha,\betaα,β为0;
(2)利用黄金标准算法得到一个仿射矩阵PAP_APA​,分解得到s,R,t2ds,R,t_{2d}s,R,t2d​;
(3)将(2)中求出的s,R,t2ds,R,t_{2d}s,R,t2d​带入能量方程,解得α\alphaα;
(4)将(2)和(3)中求出的α\alphaα代入能量方程,解得β\betaβ;
(5)更新α,β\alpha,\betaα,β的值,重复(2)-(4)进行迭代更新。

代码部分

下面将从Face3D的例程到源码一步步进行讲解:

例程部分

x = projected_vertices[bfm.kpt_ind, :2] # 2d keypoint, which can be detected from image
X_ind = bfm.kpt_ind # index of keypoints in 3DMM. fixed.# fit
fitted_sp, fitted_ep, fitted_s, fitted_angles, fitted_t = bfm.fit(x, X_ind, max_iter = 3)# verify fitted parameters
fitted_vertices = bfm.generate_vertices(fitted_sp, fitted_ep)
transformed_vertices = bfm.transform(fitted_vertices, fitted_s, fitted_angles, fitted_t)image_vertices = mesh.transform.to_image(transformed_vertices, h, w)
fitted_image = mesh.render.render_colors(image_vertices, bfm.triangles, colors, h, w)

x就是公式中的二维特征点XXX,例程里面给的是上篇文章生成二维图像时导出的二维数据。
X_ind是BFM模型三维特征点的索引,并非坐标。
然后执行了
fitted_sp, fitted_ep, fitted_s, fitted_angles, fitted_t = bfm.fit(x, X_ind, max_iter = 3
其中bfm.fit部分的源码如下:

def fit(self, x, X_ind, max_iter = 4, isShow = False):''' fit 3dmm & pose parametersArgs:x: (n, 2) image pointsX_ind: (n,) corresponding Model vertex indicesmax_iter: iterationisShow: whether to reserve middle results for showReturns:fitted_sp: (n_sp, 1). shape parametersfitted_ep: (n_ep, 1). exp parameterss, angles, t'''if isShow:fitted_sp, fitted_ep, s, R, t = fit.fit_points_for_show(x, X_ind, self.model, n_sp = self.n_shape_para, n_ep = self.n_exp_para, max_iter = max_iter)angles = np.zeros((R.shape[0], 3))for i in range(R.shape[0]):angles[i] = mesh.transform.matrix2angle(R[i])else:fitted_sp, fitted_ep, s, R, t = fit.fit_points(x, X_ind, self.model, n_sp = self.n_shape_para, n_ep = self.n_exp_para, max_iter = max_iter)angles = mesh.transform.matrix2angle(R)return fitted_sp, fitted_ep, s, angles, t

标签isShow给的是默认的False所以执行的else部分,里面执行了模型拟合部分代码:
fitted_sp, fitted_ep, s, R, t = fit.fit_points(x, X_ind, self.model, n_sp = self.n_shape_para, n_ep = self.n_exp_para, max_iter = max_iter)
以及生成旋转矩阵代码:
angles = mesh.transform.matrix2angle(R)
其中模型拟合部分的fit.fit_points部分的源码如下:

def fit_points(x, X_ind, model, n_sp, n_ep, max_iter = 4):'''Args:x: (n, 2) image pointsX_ind: (n,) corresponding Model vertex indicesmodel: 3DMMmax_iter: iterationReturns:sp: (n_sp, 1). shape parametersep: (n_ep, 1). exp parameterss, R, t'''x = x.copy().T#-- initsp = np.zeros((n_sp, 1), dtype = np.float32)ep = np.zeros((n_ep, 1), dtype = np.float32)#-------------------- estimateX_ind_all = np.tile(X_ind[np.newaxis, :], [3, 1])*3X_ind_all[1, :] += 1X_ind_all[2, :] += 2valid_ind = X_ind_all.flatten('F')shapeMU = model['shapeMU'][valid_ind, :]shapePC = model['shapePC'][valid_ind, :n_sp]expPC = model['expPC'][valid_ind, :n_ep]for i in range(max_iter):X = shapeMU + shapePC.dot(sp) + expPC.dot(ep)X = np.reshape(X, [int(len(X)/3), 3]).T#----- estimate poseP = mesh.transform.estimate_affine_matrix_3d22d(X.T, x.T)s, R, t = mesh.transform.P2sRt(P)rx, ry, rz = mesh.transform.matrix2angle(R)# print('Iter:{}; estimated pose: s {}, rx {}, ry {}, rz {}, t1 {}, t2 {}'.format(i, s, rx, ry, rz, t[0], t[1]))#----- estimate shape# expressionshape = shapePC.dot(sp)shape = np.reshape(shape, [int(len(shape)/3), 3]).Tep = estimate_expression(x, shapeMU, expPC, model['expEV'][:n_ep,:], shape, s, R, t[:2], lamb = 0.002)# shapeexpression = expPC.dot(ep)expression = np.reshape(expression, [int(len(expression)/3), 3]).Tsp = estimate_shape(x, shapeMU, shapePC, model['shapeEV'][:n_sp,:], expression, s, R, t[:2], lamb = 0.004)return sp, ep, s, R, t

fit.fit_points部分拆分讲解

(1)初始化α,β\alpha,\betaα,β为0

x = x.copy().T#-- initsp = np.zeros((n_sp, 1), dtype = np.float32)ep = np.zeros((n_ep, 1), dtype = np.float32)

x取转置,格式变为(2,68)
sp即α\alphaα,ep即β\betaβ。将它们赋值为格式(199,1)的零向量。

  1. X3dX_{3d}X3d​进行坐标转换
    由于BFM模型中的顶点坐标储存格式为{x1x_1x1​,y1y_1y1​,z1z_1z1​,x2x_2x2​,y2y_2y2​,z2z_2z2​,x3x_3x3​,y3y_3y3​,…}
    而在X_ind中只给出了三位特征点坐标的位置,所以应该根据X_ind获取X3dX_{3d}X3d​的XYZ坐标数据。
X_ind_all = np.tile(X_ind[np.newaxis, :], [3, 1])*3X_ind_all[1, :] += 1X_ind_all[2, :] += 2valid_ind = X_ind_all.flatten('F')

X_ind数据如下,是一个(68,1)的位置数据。

X_ind_all = np.tile(X_ind[np.newaxis, :], [3, 1])*3
X_ind_all拓展为(3,68)并乘3来定位到坐标位置:

X_ind_all[1, :] += 1
X_ind_all[2, :] += 2
再将第二行加一、第三行加二来对于Y坐标和Z坐标。

然后将它们合并

valid_ind = X_ind_all.flatten('F')

flatten是numpy.ndarray.flatten的一个函数,即返回一个折叠成一维的数组。但是该函数只能适用于numpy对象,即array或者mat,普通的list列表是不行的。
'F’表示以列优先展开。
合并后的结果valid_ind如下图:

通过合并后的valid_ind得到对应特征点的人脸形状、形状主成分、表情主成分这三种数据。
shapeMU = model['shapeMU'][valid_ind, :]
shapePC = model['shapePC'][valid_ind, :n_sp]
expPC = model['expPC'][valid_ind, :n_ep]

人脸形状shapeMU数据格式(68*3,1)

形状主成分shapePC数据格式(68*3,199)

表情主成分expPC数据格式(68*3,29)

for i in range(max_iter):X = shapeMU + shapePC.dot(sp) + expPC.dot(ep)X = np.reshape(X, [int(len(X)/3), 3]).T#----- estimate poseP = mesh.transform.estimate_affine_matrix_3d22d(X.T, x.T)s, R, t = mesh.transform.P2sRt(P)rx, ry, rz = mesh.transform.matrix2angle(R)# print('Iter:{}; estimated pose: s {}, rx {}, ry {}, rz {}, t1 {}, t2 {}'.format(i, s, rx, ry, rz, t[0], t[1]))#----- estimate shape# expressionshape = shapePC.dot(sp)shape = np.reshape(shape, [int(len(shape)/3), 3]).Tep = estimate_expression(x, shapeMU, expPC, model['expEV'][:n_ep,:], shape, s, R, t[:2], lamb = 0.002)# shapeexpression = expPC.dot(ep)expression = np.reshape(expression, [int(len(expression)/3), 3]).Tsp = estimate_shape(x, shapeMU, shapePC, model['shapeEV'][:n_sp,:], expression, s, R, t[:2], lamb = 0.004)return sp, ep, s, R, t

循环中的max_iter是自行定义的迭代次数,这里的输入为4。
X = shapeMU + shapePC.dot(sp) + expPC.dot(ep)
X = np.reshape(X, [int(len(X)/3), 3]).T
这里的XXX就是经过如下的运算的SnewmodelS_{newmodel}Snewmodel​,就是新的X3dX_{3d}X3d​。

真正重点的是mesh.transform.estimate_affine_matrix_3d22d(X.T, x.T),这是网格的拟合部分。
源码如下:

estimate_affine_matrix_3d22d(X, x):''' Using Golden Standard Algorithm for estimating an affine cameramatrix P from world to image correspondences.See Alg.7.2. in MVGCV Code Ref: https://github.com/patrikhuber/eos/blob/master/include/eos/fitting/affine_camera_estimation.hppx_homo = X_homo.dot(P_Affine)Args:X: [n, 3]. corresponding 3d points(fixed)x: [n, 2]. n>=4. 2d points(moving). x = PXReturns:P_Affine: [3, 4]. Affine camera matrix'''X = X.T; x = x.Tassert(x.shape[1] == X.shape[1])n = x.shape[1]assert(n >= 4)#--- 1. normalization# 2d pointsmean = np.mean(x, 1) # (2,)x = x - np.tile(mean[:, np.newaxis], [1, n])average_norm = np.mean(np.sqrt(np.sum(x**2, 0)))scale = np.sqrt(2) / average_normx = scale * xT = np.zeros((3,3), dtype = np.float32)T[0, 0] = T[1, 1] = scaleT[:2, 2] = -mean*scaleT[2, 2] = 1# 3d pointsX_homo = np.vstack((X, np.ones((1, n))))mean = np.mean(X, 1) # (3,)X = X - np.tile(mean[:, np.newaxis], [1, n])m = X_homo[:3,:] - Xaverage_norm = np.mean(np.sqrt(np.sum(X**2, 0)))scale = np.sqrt(3) / average_normX = scale * XU = np.zeros((4,4), dtype = np.float32)U[0, 0] = U[1, 1] = U[2, 2] = scaleU[:3, 3] = -mean*scaleU[3, 3] = 1# --- 2. equationsA = np.zeros((n*2, 8), dtype = np.float32);X_homo = np.vstack((X, np.ones((1, n)))).TA[:n, :4] = X_homoA[n:, 4:] = X_homob = np.reshape(x, [-1, 1])# --- 3. solutionp_8 = np.linalg.pinv(A).dot(b)P = np.zeros((3, 4), dtype = np.float32)P[0, :] = p_8[:4, 0]P[1, :] = p_8[4:, 0]P[-1, -1] = 1# --- 4. denormalizationP_Affine = np.linalg.inv(T).dot(P.dot(U))return P_Affinedef P2sRt(P):''' decompositing camera matrix PArgs: P: (3, 4). Affine Camera Matrix.Returns:s: scale factor.R: (3, 3). rotation matrix.t: (3,). translation. '''t = P[:, 3]R1 = P[0:1, :3]R2 = P[1:2, :3]s = (np.linalg.norm(R1) + np.linalg.norm(R2))/2.0r1 = R1/np.linalg.norm(R1)r2 = R2/np.linalg.norm(R2)r3 = np.cross(r1, r2)R = np.concatenate((r1, r2, r3), 0)return s, R, t

下面对这部分进行详细解读。

(2) 利用黄金标准算法得到一个仿射矩阵PAP_APA​,分解得到s,R,t2ds,R,t_{2d}s,R,t2d​;

estimate_affine_matrix_3d22d部分即黄金标准算法具体过程

a) 归一化

对于二维点XXX,计算一个相似变换TTT,使得Xˉ=TX\bar{X}=TXXˉ=TX,同样的对于三维点X3dX_{3d}X3d​,计算 Xˉ3d=UX3d\bar{X}_{3d}=UX_{3d}Xˉ3d​=UX3d​。
归一化部分的概念在Multiple View Geometry in Computer Vision一书中描述如下:
所以归一化可以概述为以下三步:

  1. 平移所有坐标点,使它们的质心位于原点。
  2. 然后对这些点进行缩放,使到原点的平均距离等于2\sqrt{2}2​。
  3. 将该变换应用于图像中的每一幅。

下面结合代码进行讲解:
输入检测,确保输入的二维和三维特征点的数目一致以及特征点数目大于4。

 X = X.T; x = x.Tassert(x.shape[1] == X.shape[1])n = x.shape[1]assert(n >= 4)

二维数据归一化:

    #--- 1. normalization# 2d pointsmean = np.mean(x, 1) # (2,)x = x - np.tile(mean[:, np.newaxis], [1, n])average_norm = np.mean(np.sqrt(np.sum(x**2, 0)))scale = np.sqrt(2) / average_normx = scale * xT = np.zeros((3,3), dtype = np.float32)T[0, 0] = T[1, 1] = scaleT[:2, 2] = -mean*scaleT[2, 2] = 1
  1. 平移所有坐标点,使它们的质心位于原点。
    经过x=x.T后x的格式变为(2,68)
    通过mean = np.mean(x, 1)获取x的X坐标和Y坐标平均值mean,格式为(2,)
    这一步x = x - np.tile(mean[:, np.newaxis], [1, n])
    x的所有XY坐标都减去刚刚算出的平均值,此时x中的坐标点被平移到了质心位于原点的位置。
  2. 然后对这些点进行缩放,使到原点的平均距离等于2\sqrt{2}2​。
    average_norm = np.mean(np.sqrt(np.sum(x**2, 0)))
    算出所有此时所有二维点到原点的平均距离average_norm,这是一个数值。
    scale = np.sqrt(2) / average_norm
    x = scale * x
    算出scale再用scale去乘x坐标,相当与x所有的坐标除以当前的平均距离之后乘以2\sqrt{2}2​。
    这样算出来的所有点到原点的平均距离就被缩放到了2\sqrt{2}2​。
  3. 同时通过计算出的scale和mean可以算出相似变换T
    T = np.zeros((3,3), dtype = np.float32)
    T[0, 0] = T[1, 1] = scale
    T[:2, 2] = -mean*scale
    T[2, 2] = 1
# 3d pointsX_homo = np.vstack((X, np.ones((1, n))))mean = np.mean(X, 1) # (3,)X = X - np.tile(mean[:, np.newaxis], [1, n])m = X_homo[:3,:] - Xaverage_norm = np.mean(np.sqrt(np.sum(X**2, 0)))scale = np.sqrt(3) / average_normX = scale * XU = np.zeros((4,4), dtype = np.float32)U[0, 0] = U[1, 1] = U[2, 2] = scaleU[:3, 3] = -mean*scaleU[3, 3] = 1

三位归一化的原理与二维相似,区别就是所有点到原点的平均距离要被缩放到3\sqrt{3}3​,以及生成的相似变换矩阵UUU格式为(4,4)。这里不赘述了。

b) 对于每组对应点 xix_ixi​~XiX_iXi​​,都有形如 Ax=bA x = bAx=b 的对应关系存在
# --- 2. equationsA = np.zeros((n*2, 8), dtype = np.float32);X_homo = np.vstack((X, np.ones((1, n)))).TA[:n, :4] = X_homoA[n:, 4:] = X_homob = np.reshape(x, [-1, 1])

这里结合下面的公式来看:
A对应其中的[XˉiT0T0TXiT]\left [\begin{array}{l} \bar{X}_i^T & 0^T\\0^T & {X}_i^T\end{array}\right ][XˉiT​0T​0TXiT​​]
b是展开为(68*2,1)格式的x。

c) 求出A的伪逆
 # --- 3. solutionp_8 = np.linalg.pinv(A).dot(b)P = np.zeros((3, 4), dtype = np.float32)P[0, :] = p_8[:4, 0]P[1, :] = p_8[4:, 0]P[-1, -1] = 1

关于A的伪逆的概念和求取方法可以参照Multiple View Geometry in Computer Vision书中的P590以后的内容。这里A的伪逆是利用numpy里面的函数np.linalg.pinv直接计算出来的,非常方便。

d)去掉归一化,得到仿射矩阵
 # --- 4. denormalizationP_Affine = np.linalg.inv(T).dot(P.dot(U))return P_Affine

这部分的代码参照公式:

以上四步就是黄金标准算法的完整过程
得到的PAffineP_{Affine}PAffine​就是式中的PAP_APA​,到这里,我们通过黄金标准算法得到了X=PA⋅X3dX=P_A\cdot X_{3d}X=PA​⋅X3d​中的PAP_APA​​。

将仿射矩阵RAR_ARA​分解得到s,R,t2ds,R,t_{2d}s,R,t2d​
s, R, t = mesh.transform.P2sRt(P)
rx, ry, rz = mesh.transform.matrix2angle(R)

其中mesh.transform.P2sRt部分的源码如下:

def P2sRt(P):''' decompositing camera matrix PArgs: P: (3, 4). Affine Camera Matrix.Returns:s: scale factor.R: (3, 3). rotation matrix.t: (3,). translation. '''t = P[:, 3]R1 = P[0:1, :3]R2 = P[1:2, :3]s = (np.linalg.norm(R1) + np.linalg.norm(R2))/2.0r1 = R1/np.linalg.norm(R1)r2 = R2/np.linalg.norm(R2)r3 = np.cross(r1, r2)R = np.concatenate((r1, r2, r3), 0)return s, R, t

这部分就是将仿射矩阵RA{R_A}RA​分解为下图的缩放比例s、旋转矩阵R以及平移矩阵t。

这部分代码比较简单,读者可以自行理解。
篇幅原因,这边只给出(1)(2)的源码解析部分,求解α,β\alpha,\betaα,β的过程将在下篇文章讲解。

Face3D学习笔记(5)3DMM示例源码解析【中下】从二维图片的特征点重建三维模型——黄金标准算法相关推荐

  1. Laravel 学习笔记之 Query Builder 源码解析(下)

    说明:本文主要学习下Query Builder编译Fluent Api为SQL的细节和执行SQL的过程.实际上,上一篇聊到了\Illuminate\Database\Query\Builder这个非常 ...

  2. Shiro学习笔记(三)源码解析

    Shiro作为轻量级的权限框架,Shiro的认证流程是怎样的一个过程. 如果没有对Shiro进行了解的话,建议先对Shiro学习笔记(一)学习一下Shiro基本的组 成. 1,几大重要组件解析 1.1 ...

  3. Kubernetes学习笔记之Calico Startup源码解析

    女主宣言 我们目前生产k8s和calico使用ansible二进制部署在私有机房,没有使用官方的calico/node容器部署,并且因为没有使用network policy只部署了confd/bird ...

  4. Android学习笔记-常用的一些源码,防止忘记了

    Android学习笔记-常用的一些源码,防止忘记了... 设置拨打电话 StringdialUri="tell:"+m_currentTelNumble; IntentcallIn ...

  5. 精仿交易猫手游1:1源码可运营 支持二维码收款

    介绍: 精仿交易猫手游1:1源码可运营 支持二维码收款 源码安装需要asp主机 后台地址/admin 帐号admin密码admin 网盘下载地址: https://zijiewangpan.com/w ...

  6. 虚拟商品帐号交易平台源码_支持个人二维码收款

    精仿淘手游马上有号账号交易平台源码支持个人二维码收款,安装非常简单,支持个人二维码收款,可以运营精仿马上有号账号交易平台源码 支持个人二维码收款 安装教程: PHP版本一定要选择5.2 1.先修改配置 ...

  7. Nginx学习笔记(五) 源码分析内存模块内存对齐

    Nginx源码分析&内存模块 今天总结了下C语言的内存分配问题,那么就看看Nginx的内存分配相关模型的具体实现.还有内存对齐的内容~~不懂的可以看看~~ src/os/unix/Ngx_al ...

  8. Simple Dynamic Strings(SDS)源码解析和使用说明二

    在<Simple Dynamic Strings(SDS)源码解析和使用说明一>文中,我们分析了SDS库中数据的基本结构和创建.释放等方法.本文将介绍其一些其他方法及实现.(转载请指明出于 ...

  9. 【Android 控件使用及源码解析】 GridView规则显示图片仿微信朋友圈发图片

    今天闲下来想用心写一点东西,发现没什么可写的,就写一下最近项目上用到的一些东西吧.最近项目要求上传多图并且多图显示,而且要规则的显示,就像微信朋友圈的图片显示一样. 想了一下用GridView再适合不 ...

  10. Netty学习笔记(一)Netty客户端源码分析

    最近在学些BIO,NIO相关的知识,也学习了下Netty和它的源码,做个记录,方便以后继续学习,如果有错误的地方欢迎指正 如果不了解BIO,NIO这些基础知识,可以看下我的如下博客 IO中的阻塞.非阻 ...

最新文章

  1. 计算机类英语怎么学,计算机专业英语教程视频
  2. 简单的3个SQL视图搞定所有SqlServer数据库字典
  3. [Z]为Web程序员解毒:9个IE常见Bug的解决方案
  4. 转:亿级Web系统的高容错性实践(好博文)
  5. NSLog (Log信息的输出)
  6. Apache Camel 3.1 – XML路由的快速加载
  7. python data frame_Python dataframer包_程序模块 - PyPI - Python中文网
  8. [js] innerHTML与outerHTML有什么区别?
  9. 基于JAVA+SpringBoot+Mybatis+MYSQL的在线论坛管理系统
  10. ios uiswitch 开关_学习iOS开关按钮UISwitch控件的方法
  11. java贪吃蛇项目总结_贪吃蛇总结
  12. VS2012 Npcap使用
  13. Zune账号注册教程
  14. Gflops是什么?
  15. 微信浏览器中唤醒App
  16. 一切的闹闹哄哄,只是他在水帘洞躲避风沙那晚做的一个梦
  17. HMM隐马尔科夫模型(附维特比代码)
  18. 如何写一份详细的创业计划书?
  19. 在安装了zonealarm的机器上实现共享上网
  20. 宜昌方言 RAP 《在宜昌2-过去现在和将来》

热门文章

  1. WPF扫雷游戏(简略版)
  2. Delphi源码网站
  3. 《算法导论》+第四版++答案
  4. 史上最牛最全android开发知识汇总
  5. 赛程安排 单循环赛 matlab程序
  6. 多媒体计算机中的扬声器指的是音箱和,怎样算合格? 音箱主要指标参数全解析...
  7. 参考文献的引用的格式
  8. 英文参考文献的正确引用格式详解
  9. WebService入门
  10. 计算机等级考试四级信息安全工程师