用InfoSwap做完身份交换,最容易被一眼识破的往往不是五官像不像,而是光影关系对不上:目标图是侧逆光,换上去的脸却是正面平光;背景整体偏暖,脸部肤色却发灰发冷。这类问题的本质是光照不匹配——源图像的光照条件被身份特征一起带进了生成结果。解决思路无非两条:要么在生成阶段就把光照从身份里解耦出去,要么在生成之后做一次光照估计与调整,把换脸区域的光照强行拉到与目标场景一致。本文把两条思路拆开讲透,并给出可以直接落地的实现代码。

光照不匹配的根源:信息瓶颈漏掉了什么
InfoSwap的核心设计是用信息瓶颈约束身份编码器:通过限制身份表征的信息量,让它只保留区分一个人所必需的最小特征,其余细节在压缩过程中被丢弃。这个思路对姿态、背景这类干扰很有效,但光照是个例外。光照与人脸的交互同时依赖几何和肤色:鼻梁的高度决定了鼻侧阴影的走向,反照率决定了高光区域的强度。编码器为了保住身份辨识度,往往会把这些与光照强相关的线索一并保留下来,这就是所谓的光照泄漏。
另一个原因出在生成器侧。InfoSwap的生成器以源图像的身份特征为主要条件,目标图像主要提供姿态和背景信息,生成器对目标场景光照的感知能力天然有限。两股力量叠加,最终换脸区域的光照由源图像主导,与目标场景产生系统性偏差,观感上就是一张脸像贴上去的贴纸。
实际排查时可以把偏差拆成三个维度:整体亮度水平对应曝光差异,色温对应冷暖偏差,方向性阴影对应主光方向与强度。前两类用颜色统计对齐就能压住,第三类必须依赖真正的光照模型,这也是下文重点展开的部分。
光照估计:用球谐模型从单张人脸反推光照参数
人脸皮肤近似朗伯表面,漫反射亮度可以写成反照率与入射光照的乘积,而环境光的低频成分可以用球谐函数的前9个基函数线性近似。这套建模把光照估计变成一个线性回归问题:把观测亮度除以反照率得到shading场,再对9个基函数做最小二乘拟合,得到9组系数,每组对应RGB三个通道,共27个数字。这27个数字完整描述了一盏低频环境光的强度、颜色和来向,足够刻画室内光、阴天自然光这类柔和光照。
手工实现这条链路需要两样东西:法线贴图和反照率贴图。法线可以从3DMM形状模型对顶点求微分得到,反照率可以借助纹理模型的先验。下面是核心代码:
import numpy as np
# 球谐基函数:3阶共9个,输入归一化法线贴图
def sh_basis(normal):
n_x, n_y, n_z = normal[..., 0], normal[..., 1], normal[..., 2]
basis = np.stack([
np.ones_like(n_x) * 0.5, # 常数项,决定整体亮度
n_y * 0.5, # 一阶项,捕捉上下方向来光
n_x * 0.5, # 一阶项,捕捉左右方向来光
n_z * 0.5, # 一阶项,捕捉前后方向来光
n_x * n_y * 0.5, # 二阶交叉项
n_y * n_z * 0.5,
(3 * n_z ** 2 - 1) * 0.5, # 二阶项,刻画顶光底光分布
n_x * n_z * 0.5,
(n_x ** 2 - n_y ** 2) * 0.5
], axis=-1)
return basis # 形状 (H, W, 9)
# 最小二乘估计球谐光照系数
def estimate_sh(image, normal, albedo):
basis = sh_basis(normal).reshape(-1, 9)
# 去掉反照率影响,得到纯shading观测值
shading = (image / np.maximum(albedo, 1e-3)).reshape(-1, 3)
coef, _, _, _ = np.linalg.lstsq(basis, shading, rcond=None)
return coef # 形状 (9, 3)
如果不想手工搭这条链路,可以直接用DECA。DECA是一个解耦的参数化头部模型,把光照作为独立隐变量与形状、表情、纹理一起编码,输出直接就是9乘3的SH系数,配合可微渲染器还能把系数反演回图像。它的好处是稳定:对普通人脸姿态,估计结果明显比手工最小二乘更鲁棒,因为网络在训练阶段见过大量光照与几何的组合先验,不容易被局部噪声带偏。
光照调整:SH系数迁移与重光照网络两条路线
拿到光照参数之后,调整有两条主流路线。第一条是纯参数化的SH系数迁移:分别估计源人脸与目标场景的SH系数,把源脸的shading用目标系数重新渲染一遍。整个过程只涉及一次矩阵乘法,计算量几乎可以忽略,而且完全可控——光照变了,反照率不动,肤色和身份不受影响。它的短板在于球谐只建模低频光照,镜面高光、鼻下硬阴影这类高频信息无能为力,遇到强定向光源就会露馅。
第二条路线是重光照网络。DPR以及后续的Total Relighting这类方法直接学习从图像到光照条件的端到端映射,输入一张目标光照描述(可以是环境贴图,也可以是SH系数),输出重光照后的人脸。Total Relighting额外预测了可见性图,把阴影计算显式建模,对硬阴影和复杂光位的还原能力明显更强,代价是需要法线、反照率等辅助输入,推理开销也更大。
选型上给一个简单判断:素材以柔和室内光、自然散射光为主,SH迁移基本够用;画面里有明确主光源、需要还原投影方向,就上重光照网络。下面是SH迁移的核心实现:
import numpy as np
def transfer_lighting(src_img, src_normal, src_albedo,
tgt_normal, tgt_img, tgt_albedo):
# 分别估计源图与目标图的光照系数
src_sh = estimate_sh(src_img, src_normal, src_albedo)
tgt_sh = estimate_sh(tgt_img, tgt_normal, tgt_albedo)
# 用目标光照系数在源脸法线上重新渲染shading
basis = sh_basis(src_normal).reshape(-1, 9)
shading = basis @ tgt_sh
shading = shading.reshape(src_img.shape)
# 反照率保持源脸不变,只替换光照成分
relit = src_albedo * shading
return np.clip(relit, 0, 1)
注意最后一步只把shading换掉,反照率原样保留。如果连反照率一起改,肤色会跟着漂移,身份信息也会被破坏,这是新手最容易踩的坑。
嵌入InfoSwap推理流程:融合与边界过渡的工程细节
把上面的模块串进InfoSwap的推理链路,完整流程是:InfoSwap生成换脸结果,对结果和目标原图各做一次光照估计,执行SH迁移或重光照得到光照一致的换脸脸,再做一次轻量的颜色统计微调,最后做边界融合输出。用DECA做估计器的伪代码如下:
import torch
def harmonize(swap_result, target_img, deca):
# 分别编码换脸结果与目标场景
coded_swap = deca.encode(swap_result)
coded_tgt = deca.encode(target_img)
# 关键一步:把目标场景的光照系数赋给换脸结果
coded_swap['light'] = coded_tgt['light']
# 解码得到光照对齐后的图像
relit = deca.decode(coded_swap)
return relit
边界融合是决定成败的细节。掩码范围要覆盖发际线和下颌线,过渡带用高斯模糊做alpha混合,硬边是穿帮的重灾区。泊松融合能进一步消除边界处的亮度跳变,但它会引入整体色偏,正确顺序是先做颜色对齐再进泊松。另外脖子区域经常被忽略:脸调好了,脖子还带着源图的光照,一样穿帮,掩码要么延伸到脖子,要么对脖子单独做一次颜色迁移。
import cv2
import numpy as np
def blend_face(relit_face, target_img, mask):
# 掩码高斯模糊生成柔和过渡带
mask_blur = cv2.GaussianBlur(mask, (21, 21), 0)
alpha = (mask_blur.astype(np.float32) / 255.0)[..., None]
# 先做alpha混合压住硬边
mixed = relit_face.astype(np.float32) * alpha + \
target_img.astype(np.float32) * (1 - alpha)
# 泊松融合消除残余的亮度跳变
center = (mask.shape[1] // 2, mask.shape[0] // 2)
fused = cv2.seamlessClone(
mixed.astype(np.uint8), target_img,
mask, center, cv2.NORMAL_CLONE)
return fused
如果换脸结果本身由InfoSwap的生成器输出,还可以更进一步:把光照调整做成可微模块挂在生成器后面,用目标图的光照系数作为监督信号微调几个epoch,让生成器学会在隐空间里直接对齐光照,推理时就不需要后处理了。这是工程上更优雅但成本更高的方案,适合对延迟有要求的在线服务场景。
效果评估与常见问题排查
评估光照一致性,定量可以计算换脸区域与周边区域在灰度图上的亮度梯度方向一致性,方向分布越接近,说明光影衔接越自然。更直接的土办法是把结果和目标图一起转灰度、缩到很小再看,整体明暗是否协调在小图上一目了然,人眼对低频差异远比高频敏感。
几个高频问题的对应解法:调完之后脸变平、立体感丢失,多半是反照率估计把shading烤了进去,检查albedo是否整体偏暗;边界出现色带,是过渡带太窄或泊松梯度场冲突,把高斯核加大一档再试;强光场景阴影方向始终不对,这是SH模型的能力上限,换Total Relighting这类重光照网络;大表情或大姿态场景估计不稳,DECA对此比较敏感,先做人脸对齐crop再编码。
总结一下,InfoSwap的光照不匹配是身份解耦不彻底的必然产物,治理思路是先估计后调整:球谐模型负责把光照参数化,SH迁移或重光照网络负责把参数落到像素上,最后用颜色对齐和泊松融合收尾。参数化路线胜在轻量可控,网络路线胜在高频细节,按素材的光照复杂度选型即可。