做三维人脸这行的人,一听到"人面对齐"大概率先想到68个关键点、99个关键点这类稀疏检测。但真到了重建、换脸、AR贴妆这类应用里,稀疏关键点根本不够用——眼睛鼻尖轮廓能对上,额头、脸颊、下颌缘这些大片平滑区域全靠猜。所以当Position Map Regression Network(以下简称PRN)这篇工作出来的时候,很多人的第一反应是:终于有人把"稠密对齐"和"三维重建"放进同一个网络里一次性解决了。PRN的完整思路是Joint 3D Face Reconstruction and Dense Alignment,输入一张2D人脸图,输出一个叫UV位置图的东西,然后从位置图解码出完整的3D人脸Mesh,同时天然携带逐像素的稠密对应关系。这篇文章我不打算复述论文公式,而是从"我实际读代码、跑通、再拿它落地项目"的角度,把它从头到尾拆一遍,包括它的思路为什么成立、训练数据怎么造、Loss为什么要这么设计、部署时有哪些坑。
1. 稠密三维人脸对齐的老问题,以及PRN换了个什么解题姿势
1.1 稀疏关键点对齐的天花板
传统上做人脸对齐,大家熟悉的是级联回归、热图回归那一套,输出的是稀疏的语义关键点。好处是标注相对容易,人脸数据集里人工标68点、106点是成熟流程。但稠密对齐要的是每个像素、或者每个三维顶点级别的对应关系,这就不是标注成本的问题,而是表示方式的问题。
想象一下你要把一个三维脸的Mesh一一对应到二维图像的每个脸部像素上。稀疏关键点给出的对应只能覆盖面部一小块区域,额头、脸颊这些位置没有关键点,软件只能用插值甚至最近邻瞎补。换到3D重建场景,稀疏关键点只能勉强约束五官位置,面部曲率、轮廓起伏全部缺失,出来的模型像一张"贴了五官的平面",完全没法用于需要真实几何的任务。
1.2 3DMM拟合的老路子:优化难、初始化敏感
在那之前,想从单张图得到三维人脸,主流方案是3DMM(三维形变模型)拟合。思路是先学一个形状空间和表情空间,把3D人脸表示成基底系数,然后通过优化相机参数、形状系数、表情系数,让渲染出来的人脸在关键点位置上逼近输入图。
这条路子的问题也很经典。第一,优化本身是非凸的,对初始值极其敏感,同一个模型换个姿态、换个光照,可能收敛到完全不同的形状;第二,用关键点做能量项,本质上还是稀疏约束,稠密细节依旧缺失;第三,每张图要单独迭代优化,虽然最终精度可以很高,但速度根本谈不上实时,一帧几百毫秒到几秒都有,工程上很难受。
其实说到底,这些人脸重建方案的共同弱点是:它们都在"参数空间"或者"稀疏点空间"里做优化,中间都没有一个让卷积网络直接输出稠密三维信息的媒介。PRN换了一个很直接的思路——我不优化系数,我直接让网络学会输出一个稠密的东西。
1.3 "回归位置图"这个姿势妙在哪
PRN核心创新在于定义一个叫Position Map的二维表示。简单说,把人脸Mesh的每个顶点映射到UV坐标系的二维平面上,每个UV像素存储该顶点在三维空间中的XYZ坐标。这样一来,原本需要解非线性优化问题才能得到的稠密三维信息,被转换成了一个标准的"图像到图像"的回归问题。
姿势一换,整个流程就走进了卷积网络最舒服的领域。CNN天然擅长从2D图像回归到2D结构,而且2D卷积的局部性和参数共享,对空间相关性的建模能力远超全连接层。更重要的是,因为Position Map的每个像素位置是固定的UV坐标,所以它天然携带了顶点之间的拓扑连接关系,输出一个Position Map就等于同时输出了一个稠密Mesh和一组稠密对应关系。
我第一次看到这个设计的时候,心里想的是:"原来可以这样偷懒。"但后来仔细想,这其实是非常聪明的表示设计,它把一个三维重建问题,巧妙地变成了一个数据充分、监督明确、网络容易学的二维回归任务。
2. UV位置图详解:三维人脸是怎么被"摊平"成一张图的
2.1 UV空间说白了就是给模型贴图用的坐标系
很多做图像的人听到UV坐标会有点懵,但做过三维建模的人都知道,UV展开就是把三维物体的表面摊平成一张二维贴图。地球仪展开成世界地图就是这个道理,每个三维点都能找到它在二维地图上的唯一位置,反过来,地图上每个像素也能对应回三维表面上的一个点。
PRN里的人脸模型有一个固定的UV模板,模型的几千上万个顶点,每个点都有一个固定的UV坐标。这个UV坐标不随人脸变化,不管这个人胖了瘦了、笑了哭了,额头中心在UV图上的位置永远不变。所以UV图本质上是一张带编码规则的"脸的地图",它记录的不是颜色,而是位置。
2.2 Position Map里存的是XYZ,不是RGB
这是理解PRN最关键的一点。普通贴图里,UV图存的是颜色信息,红绿蓝三个通道是反射率。而PRN的Position Map,每个像素的三个通道直接存储该位置对应三维顶点的X、Y、Z坐标。训练的时候,我们用已知的3D模型渲染出标准的Position Map当作监督信号;推理的时候,网络输出一张Position Map,你把它读出来,就得到了一堆三维顶点坐标。
这里有个容易踩坑的细节:坐标值范围很大,一个顶点的X坐标可能在正负100毫米这个量级,不能直接塞进网络输出层。PRN的处理是把坐标值归一化到0到1之间,也就是用255来除一下坐标范围,让输出通道落在适合网络学习的数值区间。推理结束后再乘回去,还原成毫米量级的真实坐标。
import numpy as np def decode_position_map(pos_map): # pos_map: HxWx3,网络输出,数值范围约 0~1 vertices = pos_map * 255.0 return vertices # 还原为原始三维坐标范围2.3 相比直接回归3D点云,Position Map最大的优势是拓扑不乱
也有人问过:为什么不直接让网络回归一个N乘3的顶点列表?理论上可以,但实际上有两个致命问题。第一,顶点数量太多,如果网络最后一层直接输出几万个坐标,全连接层的参数量大到根本训不动;第二,更关键的是,如果回归的是无序点云,你怎么保证网络输出的第100个点永远对应鼻尖、第200个点永远对应颧骨?一旦顶点顺序错乱,整个重建也就废了。
Position Map用固定UV坐标把每个三维点"钉死"在二维网格上,从表示层面杜绝了这个混乱问题。网络学到了"在哪个UV位置上该出现哪个三维点",换句话说,它学到的不仅是三维形状,还学到了一个隐式的顶点索引和拓扑结构。这个特性在后续做稠密对齐、贴图映射、网格变形时都极其有价值,是我认为PRN最值得借鉴的地方。
3. 网络结构、训练数据与Loss设计:这是一套系统工程
3.1 ResNet编码器加解码器,对称而且简单
PRN的网络结构放在现在看并不复杂,但非常务实。输入是一张256乘256的RGB人脸图像,主干特征提取用的是ResNet-152去掉了最后的全连接层,相当于一个强大的编码器;通过一系列残差块和上采样操作,把特征图逐步恢复到256乘256分辨率,输出3通道的Position Map。
整体上这就是一个标准的编码器-解码器结构,中间没有花哨的注意力机制、没有图卷积,就是朴素的卷积和残差连接。但效果就是稳。我跑下来觉得它稳的关键在于:任务本身被Position Map定义得足够干净,网络有多少能力都能全部用来学"图像到坐标"的映射,不需要多余的建模能力去和耦合的参数纠缠。
3.2 训练数据靠3DMM"造",真实图像用了拟合结果做微调
稍微懂点深度学习的人都会问:稠密的三维坐标监督信号从哪里来?总不可能人工标注人脸的三维CT吧?PRN的做法是,用3DMM随机采样大量形状参数、表情参数,再随机设置姿态和光照,用渲染管线自动生成"二维图像 + 对应Position Map"的数据对。
这个造数据的过程我特别喜欢,因为它干净且成本低。传统人工标注只能标稀疏关键点,3DMM合成数据却能轻松产生像素级稠密监督。当然合成数据也有问题——渲染出来的人脸太"CG感",缺少真实纹理和光照复杂度,所以PRN还做了一步:用当时已有人脸对齐方法拟合一批真实照片,把拟合出的Mesh当作弱监督真值,用真实照片微调网络。这样既保证了数据多样性,又让模型见识了真实世界的各种干扰。
3.3 加权L1 Loss、可见性Mask,看似细节实则是成败关键
Loss设计是PRN里容易被忽略但影响很大的部分。表面看就是一个L1回归Loss,实际上里面加了三层心思。
第一,不同区域的权重不一样。眼睛、嘴巴、鼻子这些五官区域,形状复杂而且对下游应用价值高,权重给得大;脸颊、额头这些相对平坦区域,权重给得小。这样网络会把有限的学习能力优先用在最关键的部位。第二,UV图并不完全对应可见区域,比如侧脸时候,鼻子会挡住一部分脸,被遮挡区域在输入图像里根本没有有效信息。PRN的做法是给这些不可见区域增加权重抑制,不让网络去瞎猜被遮挡区域,减少无关干扰。第三,Loss用L1而不是L2,因为L1对大误差的惩罚相对温和,对离群点没那么敏感,训练早期不容易被少数难样本带偏。
这些设计单拎出来都不难理解,但能把每一项都想清楚并且落实在训练里,才是这篇文章工程功底扎实的体现。后来我调自己的类似任务时,深有体会:很多模型训崩不是网络结构的问题,而是Loss没有把"该学什么、不该学什么"表达清楚。
表格对比一下设计选择:
| 设计点 | 选择 | 出发点 |
|---|---|---|
| 输出表示 | 256x256x3 Position Map | 稠密且拓扑稳定 |
| 主干网络 | ResNet-152 | 深度足够,提取全局特征 |
| 训练数据 | 3DMM合成 + 真实图拟合 | 兼顾多样性和真实性 |
| Loss类型 | 区域加权L1 | 关键五官优先,抑制离群点 |
| 遮挡处理 | 可见性Mask | 避免对无信息区域做无意义回归 |
4. 推理阶段:从一张位置图解码出完整三维人脸
4.1 一张图从输入到输出的完整流程
PRN的推理流程非常短,这也是它能做到实时的重要原因。前置是人脸检测加对齐裁剪,把检测到的人脸区域裁剪成256乘256的正方形,喂给网络。网络前向一次,得到一张256乘256乘3的Position Map。接下来就是纯后处理,把Position Map按之前归一化的逆操作还原成三维坐标,然后按固定拓扑连接成Mesh。
整个网络推理耗时,官方在CPU上能做到每帧约10毫秒的量级,GPU更不用说了。相比传统3DMM拟合一次要跑好几百次迭代优化,这个速度完全是代际差异。我实际用PyTorch复现的时候,在普通显卡上单张图前向加后处理也就几毫秒,这在工程上意味着你可以把它放在实时视频流里做人脸跟踪,放在移动端做滤镜甚至虚拟试戴。
4.2 稠密对齐是怎么和三维重建"同时"完成的
Joint这个关键词是PRN题目的核心。一次前向为什么能同时完成三维重建和稠密对齐?逻辑很简单:Position Map的每个像素位置是固定的UV坐标,它既是三维顶点的索引,也是和二维图像像素的对应关系。网络输出了一张Position Map,你取某个UV位置的数值,就拿到了对应三维顶点的坐标;同时因为这个UV位置和输入图像的某个像素位置是对应的,你就知道了"这个三维点对应图像上哪个像素"。
这就是稠密对齐的本质:三维点与二维像素的逐点对应。相比稀疏关键点需要在Mesh上反复找最近点,PRN在表示层面就把对应关系直接输出了,连后处理查表都省了。我在做换脸渲染的时候深有体会,先前稀疏对齐做完还得在网格上插值对应关系,PRN这种方案拿来就能用,而且因为是逐顶点对应,换脸贴图的细节保持得特别完整,嘴角眼角这种微妙区域都不容易穿帮。
4.3 我自己梳理的最小可跑推理链路
如果你想快速验证PRN的效果,核心链路其实很短。我整理了一份伪代码,方便理解整个流程:
- 用现成的人脸检测器获取边界框
- 按边界框裁剪区域,缩放到256乘256,做归一化
- 网络前向,输出256乘256乘3的Position Map
- 反归一化得到三维坐标,组织成顶点数组
- 按预定义的三角形索引生成Mesh,渲染出来
真正会影响效果好坏的是第2步。裁剪时如果人脸没对齐居中,或者边界框留白过多,网络输出的Position Map坐标质量会明显下降。我的经验是,先做一次粗的人脸关键点检测,用两眼连线把人脸旋转到水平,然后再裁切,出来的重建稳定性会好很多。这种预处理细节,论文里不会细说,但属于实操中人人都要趟一遍的环节。
5. 工程落地实录:复现PRN最容易踩的坑和我的解决办法
5.1 跑官方源码之前,先确认环境依赖的兼容性
PRN官方实现基于Caffe框架,这个框架本身已经在很多新环境上很难编译了,尤其是换到新版本Python或者新版本依赖库之后,各种链接错误和版本冲突能把人磨到没脾气。我当时的做法是放弃直接跑老环境,直接在PyTorch里按结构重写了一遍,效果完全可以复现,训练和推理反而更方便。
如果你也需要复现,我的建议是除非你有特殊理由非要跑老代码,否则直接用当前主流的深度学习框架实现。PRN的核心结构并不复杂,编码器换成ResNet-152,解码器按原论文的残差块和上采样层垒上去,Loss按加权L1实现,训练数据用3DMM渲染生成,基本就能获得接近论文报告的效果。省下来的时间足够你把后处理、评估、可视化全都过一遍。
5.2 预处理决定成败:裁剪对齐比网络结构更影响结果
复现过程中我做过一个对照实验:同一张图,分别做两种预处理,一种是直接按检测框裁剪缩放,另一种是先检测关键点做刚性对齐再裁剪缩放。结果后者的稠密重建效果明显更好,特别是侧脸和低头角度,重建出来的脸不会出现五官扭曲、左右脸不对称这类问题。
原因是网络训练时输入的图像基本都是正脸居中的标准裁剪,如果推理时输入的图像在几何上偏离了这个分布,网络就会倾向于输出一个"标准脸"而不是"这张人脸的形状"。所以复现阶段别急着调网络,先把预处理流程拉齐。另外输入像素的归一化方式也要和训练保持一致,有的实现是减均值除方差,有的是映射到0到1,混用会让输出坐标整体偏移。
5.3 光照、肤色、遮挡的实测观察
我在测试集上跑了很多自然场景照片,总结下来:对于正常光照下的正脸,PRN重建的轮廓和五官位置相当准,稠密对齐结果可以直接用于贴图和换脸;对偏暗光、强逆光场景,整体形状会往均值脸方向收缩,五官立体感变弱;戴眼镜、口罩这类遮挡,遮挡区域的Position Map预测会明显退化,但没遮住的区域受影响不大。
肤色方面,我的观察是PRN并没有明显偏科,毕竟训练数据里3DMM覆盖了不同肤色的纹理。真正影响大的是姿态,极端大角度比如侧面超过90度时,自遮挡面积过高,可见性Mask能帮到的有限,重建会明显失真。这是PRN这类单眼重建方案的原理性局限,不要指望靠调参能解决。
我当时还专门做了一组针对真实场景的增强策略:训练阶段增加随机亮度扰动、随机模糊、随机遮挡块(比如在眼睛或嘴巴区域盖一个黑块),配合原有可见性Mask,能有效提升遮挡场景下的重建质量。小改动,收益明显,算是亲测有效的技巧。
5.4 移动端和实时部署的量化问题
PRN模型大小在百兆级别,直接部署到手机端偏重。我自己试过做8位量化,直接用现成工具量化后,Position Map的输出精度损失明显,重建的人脸出现可见的接缝和抖动。原因是Position Map的数值本身就是连续坐标,量化误差会直接放大成三维坐标误差,这跟分类网络对量化不敏感很不一样。
我的建议是,如果非要在移动端做,先做模型剪枝或者知识蒸馏把模型瘦身,再考虑量化,最后加一个轻量级的关键点稳定后处理。另外实时视频流里,可以考虑用光流或上一帧的网格做时序平滑,这样视觉上的抖动会小很多,这个属于通用后处理方案,PRN本身没有涉及。
6. PRN的局限和它给后来工作留下的"位置图遗产"
6.1 不能回避的短板:无纹理区、极端表情、大姿态
PRN在2018年算惊艳,拿到今天看,短板也一目了然。最大问题是几何精度有限,UV位置图分辨率只有256乘256,相当于你的人脸Mesh顶点数受这个分辨率限制,细节上没办法表达皱纹、毛孔这种高频几何信息。你要做精细的影视级重建,PRN这类方法只能打底,细节还得靠别的方案补。
第二个短板是极端表情跟踪困难。训练数据里3DMM的表情空间虽然是连续的,但夸张表情(比如大喊、做鬼脸)在训练分布里占比少,网络往往把这类输入拉回均值表情。我实测张嘴大笑时,重建出的嘴部形状会明显收敛,不像原图那么夸张。
第三个短板是忽略了光照、反照率和纹理的分离。PRN重建出的Mesh是裸几何,没有材质属性,也没办法做重光照。所以它适合AR、换脸、人脸识别等不需要照片级真实渲染的场景,但如果你想把一张人脸放进一个新光照环境里,PRN做不到,还得借助光照估计和纹理生成模型。
6.2 "回归位置图"这个思路,其实影响了很多后续方向
虽然PRN本身有局限,但它选择的"位置图回归"这个中间表示影响深远。后续的人脸重演、口型同步、表情迁移等方向,很多都借鉴了这个思想:先用某种稠密2D表示编码空间信息,再让网络直接回归这个表示,绕开复杂度高的参数优化。
后来的人脸重建工作也延续了PRN的轨迹:有的做轻量化,把主干结构换成MobileNet系列,让模型可以实时跑在移动端;有的引入GAN分支生成纹理,补上PRN没有的纹理能力;有的把几何细节分离成基础形状加细节残差,弥补PRN高频信息不足的问题。但底层那个"用固定UV映射存储XYZ"的表示,还是在很多系统里不断出现。我后来做数字人驱动、面部动作捕捉优化的时候,也经常回头翻PRN的代码,参考它的数据构造和Loss设计,很多套路依然可以直接套用。
最后的实际操作体会
如果这篇文章只能记住一句话,我想说:PRN最大的价值不是那个具体网络,而是把"在表示层面解决对应关系混乱问题"这个思路演示了一遍,而且演示得很彻底。做三维视觉的人都知道,方向性的创新比刷点重要得多。我在后续项目里尝试过不少类似问题——比如人体姿态重建、手部重建,都试着找一个类似Position Map的稠密二维表示,效果确实比直接回归三维坐标稳定得多。
另外还有一点个人分享:复现PRN时不要把太多精力放在调网络结构上,真正花时间的应该是预处理一致性和训练数据分布。我一开始复现效果偏模糊,一直以为是解码器不够深,后来查了代码发现是数据归一化跟原版实现不一致,改完之后效果立刻上一个台阶。这类"表示型方法",数据和预处理的影响往往被低估,建议每一个做类似工作的人,先把数据管线检查三遍,再动结构。