人脸仿射变换与换脸术实战:从getAffineTransform到FaceSwap
应用方向说明人脸对齐用仿射变换将不同角度的人脸对齐到标准姿势,是人脸识别的预处理必备步骤表情迁移将 A 的表情关键点映射到 B,实现换表情效果DeepFake现代换脸基于深度学习 GAN,精度更高,但计算量更大AR 贴纸口罩、眼镜等 AR 效果本质上是局部掩膜 + 仿射变换① 仿射变换 = getAffineTransform(3对点) + warpAffine(矩阵M)② 换脸术 = dlib6
在 OpenCV 与 dlib 的人脸处理体系中,仿射变换是很多高级功能的地基——人脸对齐、换脸术、表情迁移,都离不开它。本文从代码出发,先讲透仿射变换本身,再深入换脸术的完整实现链路。
一、仿射变换:三点定乾坤
1.1 原理
仿射变换(Affine Transform)是一种线性变换 + 平移,可表示为:
x' = a·x + b·y + e
y' = c·x + d·y + f
用矩阵写就是 2×3 的变换矩阵 M:
M = | a b e |
| c d f |
只要知道 3 个对应点对,就能唯一确定 M。这是仿射变换最核心的特性:三点定乾坤。
原图三点 目标图三点
P1(0,0) ──────► P1'(0,0)
P2(0,h-1) ──────► P2'(50,h-50)
P3(w-1,0) ──────► P3'(w-50,50)
仿射变换保平行线(平行线变换后仍平行),但不保长度和角度。适用于:平移、旋转、缩放、倾斜。
1.2 代码实现
import cv2
import numpy as np
img = cv2.imread("handou2.jpg")
h, w = img.shape[:2]
# 选取三个控制点
mat_src = np.float32([[0, 0], [0, h-1], [w-1, 0]])
mat_dst = np.float32([[0, 0], [50, h-50], [w-50, 50]])
# 获取 2x3 仿射变换矩阵
M = cv2.getAffineTransform(mat_src, mat_dst)
# 应用仿射变换
dst = cv2.warpAffine(img, M, (w, h))
# 拼接原图和结果图对比
imas = np.hstack([img, dst])
cv2.imshow('imgs', imas)
cv2.waitKey(0)
流程图解:

[原图] --选取3点--> mat_src, mat_dst
|
v
getAffineTransform()
|
v
矩阵 M [2x3]
|
v
warpAffine(M)
|
v
[变换结果]
二、换脸术:技术链路全解析
2.1 整体流程
换脸(Face Swap)的本质是把 B 脸"贴"到 A 脸上,但不是简单叠加,而是经过以下 7 步精细处理:

① 读取图像A、B
↓
② dlib检测人脸 → 提取68个关键点
↓
③ 对68点求凸包 → 构建脸部掩膜
↓
④ SVD相似变换 → 求A→B的变换矩阵M
↓
⑤ warpAffine → 把B的掩膜变形对齐到A
↓
⑥ 高斯颜色迁移 → 让B的颜色与A一致
↓
⑦ 掩膜加权融合 → 最终换脸结果
2.2 核心技术一:凸包掩膜构建
POINTS = (LEFT_BROW + RIGHT_EYE + LEFT_EYE +
RIGHT_BROW + NOSE + MOUTH) # 眉毛+眼睛区域
def getFaceMask(im, keyPoints):
im_mask = np.zeros(im.shape[:2], dtype=np.float64)
for p in POINTS:
hull = cv2.convexHull(keyPoints[p]) # 求凸包
cv2.fillConvexPoly(im_mask, hull, color=1) # 填充凸包(值在0~1)
# 单通道 -> 三通道,适应OpenCV
im_mask = np.array([im_mask]*3).transpose((1, 2, 0))
im_mask = cv2.GaussianBlur(im_mask, (29, 29), 0) # 边缘羽化
return im_mask
关键点: 凸包填充是为了得到精确的脸部区域;高斯模糊((29,29) 大核)的目的是羽化边缘,让融合更自然,不留接缝。

2.3 核心技术二:SVD相似变换
这是换脸的核心数学。其目标是:找到 A 的 68 点到 B 的 68 点之间的相似变换矩阵 M(缩放 + 旋转 + 平移)。
def getM(points1, points2):
points1 = points1.astype(np.float64)
points2 = points2.astype(np.float64)
c1 = np.mean(points1, axis=0) # 质心
c2 = np.mean(points2, axis=0)
points1 -= c1; points2 -= c2 # 去均值(归一化)
s1 = np.std(points1)
s2 = np.std(points2)
points1 /= s1; points2 /= s2 # 标准化
U, S, Vt = np.linalg.svd(points1.T * points2) # SVD分解
R = (U * Vt).T # 旋转矩阵
return np.hstack(((s2/s1)*R, c2.T - (s2/s1)*R*c1.T)) # M = [sR | t]
四步推导:
| 步骤 | 操作 | 目的 |
|---|---|---|
| ① 归一化 | X -= mean; X /= std |
消除尺度和位置差异 |
| ② SVD分解 | U,S,Vt = svd(X·Yᵀ) |
找到最优旋转矩阵 |
| ③ 求旋转R | R = U·Vtᵀ |
旋转矩阵(正交矩阵) |
| ④ 构建M | M = [(s₂/s₁)R | c₂-(s₂/s₁)Rc₁] |
尺度 + 旋转 + 平移 |
几何含义:
B脸关键点群
│
│ 减去质心 c2,除标准差 s2
▼
▼ SVD分解找最优旋转 R
▼
▼ 缩放 s2/s1,旋转 R
▼
▼ 加上 A 的质心 c1
▼
A脸关键点群位置(对齐!)

2.4 核心技术三:仿射变形 + 颜色迁移
# 用 M 将 B 的掩膜变形对齐到 A
bMaskWarp = cv2.warpAffine(bMask, M, dsize,
borderMode=cv2.BORDER_TRANSPARENT,
flags=cv2.WARP_INVERSE_MAP)
# 高斯颜色迁移:调整B的颜色分布匹配A
def normalColor(a, b):
aGauss = cv2.GaussianBlur(a, (111, 111), 0)
bGauss = cv2.GaussianBlur(b, (111, 111), 0)
weight = aGauss / bGauss
weight[np.isinf(weight)] = 0
return b * weight
bcolor = normalColor(a, bWrap)
颜色迁移原理: 用超大核 (111,111) 高斯模糊获取两张脸的整体颜色分布,用比值 A_blur / B_blur 调整 B 的颜色。isinf 处理是防止 bGauss 中出现 0 导致除零。
2.5 核心技术四:掩膜加权融合
# 取两张掩膜的最大值(处理重叠区域)
mask = np.maximum(aMask, bMaskWarp)
# 掩膜加权融合
out = a * (1.0 - mask) + bcolor * mask
融合公式:
换脸结果 = A图 × (1 - mask) + B颜色图 × mask
mask = 0 → 显示A(原图)
mask = 1 → 显示B(换上的脸)
mask = 0.5 → 两图各50%混合(边缘过渡)
三、两技术对比

| 维度 | 仿射变换 | 换脸术 |
|---|---|---|
| 定位 | 基础工具(底层) | 上层应用 |
| 核心原理 | 三点线性变换 | 68点SVD + 仿射 + 颜色迁移 |
| 变换矩阵 | cv2.getAffineTransform() |
自定义 getM() (SVD) |
| 适用范围 | 人脸对齐、文档校正、UI变换 | 娱乐换脸、人脸替换 |
| 代码复杂度 | 极简(~5行) | 中等(~50行) |
| 关键参数 | 三点坐标、warpAffine |
68点、GaussianBlur核大小 |
四、完整代码
import cv2
import dlib
import numpy as np
POINTS = (LEFT_BROW + RIGHT_EYE + LEFT_EYE +
RIGHT_BROW + NOSE + MOUTH)
POINTStuple = tuple(POINTS)
def getFaceMask(im, keyPoints):
im_mask = np.zeros(im.shape[:2], dtype=np.float64)
for p in POINTS:
hull = cv2.convexHull(keyPoints[p])
cv2.fillConvexPoly(im_mask, hull, color=1)
im_mask = np.array([im_mask]*3).transpose((1, 2, 0))
return cv2.GaussianBlur(im_mask, (29, 29), 0)
def getM(points1, points2):
points1 = points1.astype(np.float64)
points2 = points2.astype(np.float64)
c1 = np.mean(points1, axis=0); c2 = np.mean(points2, axis=0)
points1 -= c1; points2 -= c2
s1 = np.std(points1); s2 = np.std(points2)
points1 /= s1; points2 /= s2
U, S, Vt = np.linalg.svd(points1.T * points2)
R = (U * Vt).T
return np.hstack(((s2/s1)*R, c2.T-(s2/s1)*R*c1.T))
def getKeyPoints(im):
rects = detector(im, 1)
shape = predictor(im, rects[0])
return np.matrix([[p.x, p.y] for p in shape.parts()])
def normalColor(a, b):
weight = cv2.GaussianBlur(a, (111,111), 0) / cv2.GaussianBlur(b, (111,111), 0)
weight[np.isinf(weight)] = 0
return b * weight
a = cv2.imread("hg.jpg")
b = cv2.imread("pyy.jpg")
detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")
aMask = getFaceMask(a, getKeyPoints(a))
bMask = getFaceMask(b, getKeyPoints(b))
M = getM(getKeyPoints(a)[POINTStuple], getKeyPoints(b)[POINTStuple])
dsize = a.shape[:2][::-1]
bMaskWarp = cv2.warpAffine(bMask, M, dsize, borderMode=cv2.BORDER_TRANSPARENT, flags=cv2.WARP_INVERSE_MAP)
mask = np.maximum(aMask, bMaskWarp)
bWrap = cv2.warpAffine(b, M, dsize, borderMode=cv2.BORDER_TRANSPARENT, flags=cv2.WARP_INVERSE_MAP)
out = a * (1.0 - mask) + normalColor(a, bWrap) * mask
cv2.imshow('out', out / 255)
cv2.waitKey(0)
五、总结与扩展
| 应用方向 | 说明 |
|---|---|
| 人脸对齐 | 用仿射变换将不同角度的人脸对齐到标准姿势,是人脸识别的预处理必备步骤 |
| 表情迁移 | 将 A 的表情关键点映射到 B,实现换表情效果 |
| DeepFake | 现代换脸基于深度学习 GAN,精度更高,但计算量更大 |
| AR 贴纸 | 口罩、眼镜等 AR 效果本质上是局部掩膜 + 仿射变换 |
本文两段代码的核心知识点:
① 仿射变换 = getAffineTransform(3对点) + warpAffine(矩阵M)
② 换脸术 = dlib68点 → 凸包掩膜 → SVD相似变换
→ 高斯颜色迁移 → 掩膜加权融合
更多推荐
所有评论(0)