简介:一份面向计算机视觉与图形学研究者的学术论文,聚焦人脸运动重定向问题,提出可联合检测人脸边界框并回归3DMM参数的端到端网络。该网络由单面网络(SFN)与多面网络(MFN)构成,先将3DMM参数分解为身份、表情、姿态和比例四类,通过不同分辨率的多尺度特征强化参数组之间的解纠缠,并结合Fire模块与SE模块提升实时性;随后用SFN生成多脸图像中的弱标签,再训练MFN实现同步检测与重定向。实验表明其人脸检测精度较高、对极端表情和姿态鲁棒,且运行速度优于多数现有方法,在移动设备上也可达到较高帧率,可应用于人脸动画、面部表情识别等场景。资源包内含1个PDF,大小约2.47MB,已有97人学习,适合需要复现或深入理解该网络架构的研究生、工程师及人脸相关应用开发者参考。
1. 端到端人脸多任务网络:为什么我把运动重定向做在3DMM参数层
人脸多任务学习和运动重定向这个方向,最初吸引我的场景其实很朴素:一台人脸识别门禁机的联动屏上,想让检测到的人脸实时做出指定表情;或者做虚拟人驱动时,想把一段真人视频里的表情、姿态搬到另一个人的脸上。传统做法是检测、关键点对齐、3DMM拟合、重定向四步各跑各的,每一步的误差都会累积到下一步,最终表情偏移超过肉眼能忍的范围。而这个端到端网络把多人脸检测、3DMM参数回归、运动重定向焊进一条链路,输入普通RGB图,输出重定向后的人脸参数,不再有中间环节的修正机会,误差反而比级联方式小。适合谁?做虚拟人驱动、影视表情迁移、人脸数据集增强的从业者。这篇文章我按“参数结构 — 网络设计 — 训练策略 — 踩坑 — 验证”的顺序把它拆给你。
2. 从3DMM参数看运动重定向:为什么重定向要建立在参数空间而不是光流上
2.1 3DMM 把人脸拆成身份、表情、姿态三个独立坐标系
人脸运动重定向要的是“把A的表情系数搬到B的脸上,同时保留B的长相”。这件事在几何网格上做很麻烦,因为A和B的拓扑、顶点顺序、脸型都不同。3DMM(三维可形变模型)的思路是把人脸形状表示成一个线性组合:平均脸加上身份基向量、表情基向量和姿态项的加权和。BFM这类公开模型通常把身份形状压缩到199个主成分、表情压缩到几十个主成分,所有顶点坐标由这些系数唯一确定。
所以我一般把3DMM全量参数看成一个“高维向量”——这也是热词里常说的“人脸识别图像进入神经网络到输出高维度向量的过程”。检测网络输出的是框和置信度,3DMM回归头输出的是这个向量。向量头部是身份系数,中间是表情系数,再到纹理、姿态角度、平移量。重定向时只需要操作向量的某几段,跟人脸长什么样、视频分辨率是多少都无关。
2.2 参数空间重定向比光流重定向稳在哪
光流法做运动重定向的痛点是遮挡和大幅形变。张嘴、闭眼这类动作会带来非刚性形变,光流估计一旦出现空洞,迁移到目标脸上就是扭曲的。参数空间没有这个困扰,因为表情系数本身就是从真实人脸样本里统计出来的形变模式,天然保证拓扑一致性。
第二个原因是解耦。3DMM的身份子空间和表情子空间是分开的PCA基,重定向的数学操作退化成一次矩阵乘加:
def redirect_3dmm(id_coeff_target, exp_coeff_source, mean_shape, id_basis, exp_basis): # 目标脸身份 + 源脸表情 identity_part = mean_shape + id_coeff_target @ id_basis.T expression_part = exp_coeff_source @ exp_basis.T redirected = identity_part + expression_part return redirected这段代码的逻辑是:先由目标人脸的身份系数重建出它的中性形状,再把源人脸的表情系数叠加到这个中性形状上。id_basis和exp_basis是3DMM的正交基矩阵,形状分别是(顶点数, 身份维数)和(顶点数, 表情维数),它们从BFM模型文件里读取后不再参与训练。重定向的精度上限由表情基的表达能力决定,和网络本身无关。
2.3 3DMM 系数拆分:一个被很多人忽略的细节
无论是训练时算损失,还是推理时做重定向,第一步都是把回归头输出的系数向量拆开。常见做法是按约定好的维度顺序切片:
def split_bfm_coeffs(packed_coeffs, n_id=199, n_exp=29, n_tex=199): offset = 0 id_coeffs = packed_coeffs[offset: offset + n_id]; offset += n_id exp_coeffs = packed_coeffs[offset: offset + n_exp]; offset += n_exp tex_coeffs = packed_coeffs[offset: offset + n_tex]; offset += n_tex angles = packed_coeffs[offset: offset + 3] translation = packed_coeffs[offset + 3: offset + 6] return id_coeffs, exp_coeffs, tex_coeffs, angles, translation注意这里踩过的坑:不同训练集使用的3DMM版本不同,表情维数可能是29、46、52甚至79。如果你的预训练模型来自某个基准库,而重定向模块用了另一个库的维度切片,身份和表情会错位,重定向出来的人脸表情完全不对。我一般会在模型保存时把n_id/n_exp/n_tex三个值写进配置文件,加载时强校验,不匹配直接报错而不是悄悄跑。
3. 联合检测与重定向网络:主干、输出头与损失配比
3.1 一个网络三个头:检测、3DMM回归、重定向
整体结构我倾向于这样组织:共享主干采用类似ResNet的FPN结构,顶部拉出三个分支。第一个分支是检测头,输出多人脸的边框、置信度和用于NMS的额外特征;第二个分支是3DMM回归头,对每个人脸区域输出身份、表情、姿态系数;第三个分支是重定向头,它的输入是源人脸的完整3DMM系数、目标人脸的身份系数以及一个“驱动信号”。
关键的选型理由是:检测和3DMM回归共享底层特征,多人脸场景下特征图上的每个锚点都能同时响应“这里有人脸”和“人脸长什么样”;重定向头不直接吃图像,而是吃参数,所以这个分支很轻。实际实现里检测头用anchor-based还是anchor-free都行,但3DMM回归头要对齐检测框,我一般用RoIAlign从FPN的P2层抠特征,因为人脸框普遍偏小,P2层的感受野和高分辨率更适合抓五官细节。
3.2 回归头输出解析:把网络输出变成可重定向的参数
下面这段是3DMM回归头在推理时的解析逻辑,它在检测之后逐个人脸执行:
def decode_face_params(face_feat, delta_box): # face_feat: RoIAlign 后的特征,形状 (C, 14, 14) out = fc_layers(face_feat) # 得到全量3DMM系数 id_coeff, exp_coeff, tex_coeff, angles, translation = split_bfm_coeffs(out) # 部分实现里姿态角不是直接用,而是恢复旋转矩阵 import numpy as np rx, ry, rz = angles Rx = np.array([[1,0,0],[0,np.cos(rx),-np.sin(rx)],[0,np.sin(rx),np.cos(rx)]]) Ry = np.array([[np.cos(ry),0,np.sin(ry)],[0,1,0],[-np.sin(ry),0,np.cos(ry)]]) Rz = np.array([[np.cos(rz),-np.sin(rz),0],[np.sin(rz),np.cos(rz),0],[0,0,1]]) R = Rz @ Ry @ Rx return id_coeff, exp_coeff, R, translation, delta_box逻辑说明:fc_layers是全连接回归头,输出维度由n_id + n_exp + n_tex + 6决定。姿态角按Rz @ Ry @ Rx的欧拉角顺序转成旋转矩阵,这个顺序必须和训练时数据标注的约定一致,否则头部会绕错误轴旋转。重定向模块只取id_coeff和exp_coeff,姿态由驱动源决定,全网络只有这一处需要处理旋转矩阵。
3.3 多任务损失配比:权重不是拍脑袋定的
多任务学习最容易被忽略的是损失函数之间的尺度差异。检测头的Smooth L1损失通常在0.01量级,3DMM系数回归的L2损失可能在10以上,直接相加会让检测头完全失去梯度。我的做法是给每个任务设独立权重,同时对3DMM损失做按维度的归一化:
| 损失项 | 公式 | 初始权重 | 理由 |
|---|---|---|---|
| L_det | Smooth L1 (bbox + cls) | 1.0 | 检测是入口,错框全错 |
| L_3dmm | L2 (coeff_pred - coeff_gt) / dim | 2.0 | 按维度平均,避免高维主导 |
| L_redirect | L2 (redirected_coeff - target_gt) | 3.0 | 重定向是本网络最终目标 |
| L_id_consistency | 余弦距离 (id_coeff_pred, id_coeff_gt_target) | 1.5 | 保证重定向结果不丢身份 |
代码实现里,重定向损失不是直接盯着渲染图算的,而是监督重定向后的系数与真实目标表情系数之间的L2距离。身份一致性损失单独用余弦距离,因为身份系数是方向性的,欧式距离会惩罚整体缩放,而这种缩放对身份无关紧要。
def compute_multitask_loss(batch): det_loss = smooth_l1(batch["pred_boxes"], batch["gt_boxes"], beta=0.1) coeff_loss = torch.mean((batch["pred_coeff"] - batch["gt_coeff"])**2) / COEFF_DIM redirect_loss = torch.mean((batch["pred_redirect"] - batch["gt_redirect"])**2) / REDIRECT_DIM id_loss = 1.0 - torch.cosine_similarity( batch["pred_redirect"][:, :], batch["gt_identity"][:, :]).mean() total = 1.0 * det_loss + 2.0 * coeff_loss + 3.0 * redirect_loss + 1.5 * id_loss return total注意最后一步:pred_redirect的切片范围是完整的身份加表情段。这是有意为之——如果只拿身份段算,重定向头会学成“无视表情、只复制目标脸身份”,网络退化成身份编码器。
4. 训练数据与分阶段训练:联合网络翻车率最高的一环
4.1 数据怎么凑、怎么对齐
多任务联合训练的数据要求比单任务高得多,同一张图要同时有检测框标注、3DMM系数标注和重定向真值。公开数据里很难找到三者齐全的,常见做法是把几份数据混合起来用。检测标注来自WIDER FACE,3DMM系数来自带参数标注的人脸数据集,重定向真值来自不同身份、不同表情的配对数据。混合训练要注意采样比例,我一般按检测数据25%、3DMM数据50%、重定向配对数据25%的比例采样,每批数据只能激活对应的损失头。
对齐问题的典型翻车场景是:检测框来自WIDER FACE,人脸的裁剪方式和3DMM数据集的裁剪习惯不同,导致网络看到的人脸尺度不一致。解决方法是统一按检测框放大1.2倍裁剪,再缩放到固定尺寸,人脸中心偏移量作为检测头的额外回归目标。这一步看着小,但没对齐好,后面重定向头学到的身份系数会把背景纹理当成人脸特征。
4.2 分阶段训练:先把底层稳了再开全部头
多任务网络直接联合训练容易梯度互相拉扯。更稳的做法是分三阶段:
| 阶段 | 训练内容 | 学习率 | 轮数 | 备注 |
|---|---|---|---|---|
| 第一阶段 | 检测头 + 主干 | 1e-4 | 80 | 不更新3DMM和重定向头 |
| 第二阶段 | 检测头 + 3DMM回归头 | 5e-5 | 60 | 冻结重定向头,主干微调 |
| 第三阶段 | 全部头联合微调 | 1e-5 | 40 | 3DMM回归头学习率减半 |
第一阶段的目的是让FPN学到稳定的人脸特征,避免检测任务带着不成熟的3DMM梯度一起崩塌。第二阶段把3DMM回归头接到检测特征上,检测错框会导致回归错位,所以学习率调低。第三阶段才是完整的端到端训练,此时检测和3DMM回归已经基本稳定,重定向头的梯度不会像一开始那样剧烈震荡。
还有一个数据技巧:重定向配对样本里,同一张源脸表情要和多个身份配对。这样网络学到的是表情的泛化转移,而不是记住某个固定映射。每批里至少保证有一个身份是训练中没见过的,否则重定向头会倾向通过身份检索来作弊。
5. 避坑记录:这个网络我踩过的五个坑,按现象到解决写
5.1 多人脸场景小脸漏检
现象:两张及以上人脸时,较小那一张经常被漏掉,重定向根本没机会执行。
原因:FPN的P3到P5层下采样倍数太大,小脸在P5层特征里只剩2×2像素,检测头的anchor匹配不上。这个网络在单人脸或大脸数据上表现尚可,一上多人脸密集场景就翻车。
解决:把检测头锚定到P2层,并增加小尺度anchor。同时把RoiAlign的特征分辨率从7×7提到14×14,三个头共享。注意这会让显存增大,后面有专门讲显存的坑。
提示:如果用预训练权重,务必确认它的anchor尺度配置和你现网一致。之前直接复用基准模型,P2层特征没人用过,输出全是低置信度噪声框。
5.2 表情和身份耦合,重定向出来“像A又像B”
现象:目标身份的表情和源身份的表情同时出现在结果里,五官是B的,但表情幅度和纹理细节明显偏向A。
原因:3DMM回归头对身份和表情的系数分布学混了。身份基和表情基虽然正交,但网络输出层是全连接,没有显式约束两个子空间。PCA理论上正交,在参数空间解耦容易,在特征空间容易耦合。
解决:在3DMM回归头的输出层前加一个可学习线性变换矩阵,把特征投影到身份和表情两个正交子空间,并用正交性惩罚损失监督。简版做法是在损失里加id_coeff @ exp_coeff.T逼近零矩阵的项。这么做之后耦合现象明显减少,重定向表情幅度更干净。
5.3 重定向结果在连续帧里抖动
现象:单帧效果不错,视频序列里表情幅度忽大忽小,嘴的开合像低频振荡。
原因:重定向头逐帧独立回归,没有时序建模。3DMM坐标拟合本身对噪点敏感,检测框轻微抖动就会导致RoI区域变化,系数波动被放大。
解决:偏工程的做法是推理阶段给重定向系数加一阶指数平滑:
def smooth_coeff(new_coeff, prev_coeff, alpha=0.4): # alpha越大,跟随越快;越小越平滑 return alpha * new_coeff + (1 - alpha) * prev_coeffalpha取0.3到0.4比较平衡,取太低表情会拖尾、滞后明显。我一般还对人脸框做同样的平滑,让检测框抖动和系数抖动同时被压制。
5.4 显存不够:多人脸加三个头直接OOM
现象:8张1080Ti,batch size设为4,FPN加三个头直接OOM。这在多人脸场景尤其常见,每张图的人脸数量不定,RoI特征显存开销随人数线性增长。
原因:重定向头本身不吃显存,但3DMM回归头如果对每个RoI都过三四个全连接层,参数量不小。同时P2层的高分辨率特征把激活值显存推到很高。
解决:第一是主干换成可分离卷积版本,P2层通道从256降到128;第二是开启梯度检查点(gradient checkpointing),以少量计算换显存;第三是batch size降到2但不降图像分辨率,分辨率对多人脸检测影响大,batch size影响可以由累积梯度补偿。
5.5 第三个阶段loss不降反升
现象:分阶段训练前两步loss都正常,第三阶段联合微调loss震荡上升。
原因:要么学习率没按阶段衰减,要么梯度冲突集中在检测头和重定向头之间。重定向头的梯度期望主干特征更强调表情相关纹理,检测头的梯度期望更强调位置相关边缘,两者在FPN的高层特征上直接冲突。
解决:除了把3DMM回归头学习率减半,我对检测头做梯度裁剪,梯度范数阈值设为5.0。更稳妥的替代是更新检测头时让重定向头的梯度不反向传播到FPN,只更新它自己的几层,相当于软冻结共享特征。
6. 验证重定向效果:自监督一致性检查与一个收尾习惯
6.1 用合成数据做自监督验证
重定向的真值难以采集,但合成数据能闭环验证。做法是:用渲染引擎生成同一目标身份在10种表情下的图像,再生成一个源身份在同样10种表情下的图像。把源身份表情系数和目标身份中性脸输入重定向头,输出应该和目标身份对应的表情系数一致。
def verify_redirect(model, render_set, n_id=199, n_exp=29): errors = [] for identity_target, identity_source in render_set: for exp_id in range(10): src_exp = identity_source["exp"][exp_id] tgt_neutral = identity_target["exp"][0] pred = model.redirect(tgt_neutral + src_exp, identity_target["id"]) gt = identity_target["exp"][exp_id] errors.append(torch.cosine_similarity(pred, gt).item()) return sum(errors) / len(errors)这段代码的验证逻辑是:重定向后的表情系数应该和目标身份在该表情下的真实系数高度接近。余弦相似度平均在0.93以上算合格,低于0.90说明表情信息在重定向链路中丢失,优先怀疑分支输出头的特征融合方式。
6.2 一致性指标怎么结合渲染验证
系数相似度高不代表像素级正确,我会额外把重定向后的3DMM系数渲染成网格,和真实目标脸图像做轮廓IoU以及嘴部、眼部区域的光流误差。轮廓IoU反映形状贴合度,光流误差反映局部形变是否符合物理规律。两者都过合格线,才敢说“这个重定向网络真的能用”。
从那以后,我每次训练完这种联合网络,都强制走一遍“系数拆分 → 重定向 → 渲染 → 像素比对”的全流程,不只看loss曲线,哪怕系数损失已经收敛得很漂亮。因为系数空间差0.01在渲染后可能就是表情完全不同的两张脸。希望帮到你,少走我这些弯路。
本文还有配套的精品资源,点击获取