文章目录
- 引言
- 一、技术概述
- 二、环境准备
- 三、关键代码解析
- 1. 人脸关键点定义
- 2. 获取人脸掩模
- 3. 计算仿射变换矩阵
- 4. 检测并提取人脸关键点
- 5. 颜色校正
- 四、完整流程
- 五、效果展示
- 六、总结
引言
本文将介绍如何使用Python、OpenCV和dlib库实现人脸融合技术,将一张人脸的特征无缝融合到另一张人脸上。
一、技术概述
人脸融合是一种将两张人脸的特征进行智能融合的技术,主要包含以下几个关键步骤:
- 人脸关键点检测
- 人脸对齐和仿射变换
- 人脸区域分割和融合
- 颜色校正
二、环境准备
import cv2
import numpy as np
import dlib
需要安装以下库:
- OpenCV (
pip install opencv-python
) - NumPy (
pip install numpy
) - dlib (
pip install dlib
)
还需要下载dlib的预训练模型shape_predictor_68_face_landmarks.dat
链接在下方:
shape_predictor_68_face_landmarks.dat
三、关键代码解析
1. 人脸关键点定义
JAW_POINTS = list(range(0,17)) # 下巴轮廓点
RIGHT_BROW_POINTS = list(range(17,22)) # 右眉毛
LEFT_BROW_POINTS = list(range(22,27)) # 左眉毛
NOSE_POINTS = list(range(27,35)) # 鼻子
RIGHT_EYE_POINTS = list(range(36,42)) # 右眼
LEFT_EYE_POINTS = list(range(42,48)) # 左眼
MOUTH_POINTS = list(range(48,61)) # 嘴巴
FACE_POINTS = list(range(17,68)) # 全脸# 合并关键点集
POINTS = [LEFT_BROW_POINTS + RIGHT_EYE_POINTS + LEFT_EYE_POINTS + RIGHT_BROW_POINTS + NOSE_POINTS + MOUTH_POINTS]
POINTStupLe = tuple(POINTS)
2. 获取人脸掩模
def getFacemask(im, keyPoints):# 创建与图像同尺寸的全零矩阵im = np.zeros(im.shape[:2], dtype=np.float64)# 为每个面部区域创建凸包并填充for p in POINTS:points = cv2.convexHull(keyPoints[p])cv2.fillConvexPoly(im, points, color=1)# 转换为3通道并应用高斯模糊im = np.array([im, im, im]).transpose(1, 2, 0)im = cv2.GaussianBlur(im, (25, 25), 0)return im
这段代码的作用是根据给定的关键点(keyPoints)在图像上生成一个面部遮罩(face mask),并对其进行高斯模糊处理。下面是逐步解释:
(1)初始化空白图像
im = np.zeros(im.shape[:2], dtype=np.float64)
- 创建一个与输入图像
im
大小相同的全黑(值为0)的单通道浮点型图像。
(2)绘制凸包填充区域
for p in POINTS:points = cv2.convexHull(keyPoints[p])cv2.fillConvexPoly(im, points, color=1)
- 遍历
POINTS
(可能是面部关键点的集合,比如眼睛、嘴巴等区域)。 - 对每组关键点
keyPoints[p]
计算凸包(cv2.convexHull
),得到一个凸多边形。 - 用
cv2.fillConvexPoly
在单通道图像im
上填充这些凸多边形,填充值为1
(白色)。
(3) 扩展为三通道并模糊
im = np.array([im, im, im]).transpose(1, 2, 0)
- 将单通道图像复制为三通道(RGB),通过
transpose
调整维度顺序为(height, width, 3)
。
im = cv2.GaussianBlur(im, (25, 25), 0)
- 对三通道图像应用高斯模糊(核大小为
25x25
,标准差为0
表示自动计算)。
(4)返回值
- 返回模糊后的三通道遮罩图像,值范围是
[0, 1]
(原填充区域为1
,其余为0
,模糊后边缘过渡平滑)。
3. 计算仿射变换矩阵
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 -= c1points2 -= c2s1 = np.std(points1)s2 = np.std(points2)points1 /= s1points2 /= s2# 奇异值分解计算旋转矩阵U, S, Vt = np.linalg.svd(points1.T * points2)R = (U * Vt).Treturn np.hstack(((s2/s1)*R, c2.T-(s2/s1)*R*c1.T))
这段代码的作用是计算两组点集(points1
和 points2
)之间的相似变换矩阵(Similarity Transformation Matrix),用于将 points1
映射到 points2
的坐标系中。相似变换包括缩放(scale)、旋转(rotation)和平移(translation),但不包括倾斜或剪切变换。
(1)数据预处理(归一化)
points1 = points1.astype(np.float64)
points2 = points2.astype(np.float64)
- 将输入的点集转换为
float64
类型,避免整数运算带来的精度问题。
c1 = np.mean(points1, axis=0)
c2 = np.mean(points2, axis=0)
points1 -= c1
points2 -= c2
- 计算两组点的均值
c1
和c2
(相当于中心点)。 - 将点集中心化(减去均值),使得它们的中心都位于
(0, 0)
,方便后续计算旋转和缩放。
s1 = np.std(points1)
s2 = np.std(points2)
points1 /= s1
points2 /= s2
- 计算两组点的标准差
s1
和s2
(衡量点的分布范围)。 - 对点集进行缩放归一化(除以标准差),使它们的尺度一致(类似于 Z-score 标准化)。
(2)计算旋转矩阵(SVD 分解)
U, S, Vt = np.linalg.svd(points1.T @ points2)
R = (U @ Vt).T
- 对
points1.T @ points2
进行奇异值分解(SVD),得到U
、S
、Vt
。 - 旋转矩阵
R
的计算方式为R = U @ Vt
,并转置(.T
)使其适用于列向量变换。 - 这一步的目的是找到最优旋转,使得
points1
和points2
对齐。
(3)计算完整的相似变换矩阵
return np.hstack(((s2/s1)*R, c2.T-(s2/s1)*R*c1.T))
(s2/s1)*R
:缩放因子s2/s1
乘以旋转矩阵R
,表示points1
需要缩放s2/s1
倍并旋转R
才能匹配points2
的尺度。c2.T - (s2/s1)*R @ c1.T
:计算平移向量,使得变换后的points1
中心c1
能对齐points2
的中心c2
。np.hstack
:将缩放旋转部分和平移部分水平拼接,形成完整的 3×3 相似变换矩阵(如果是 2D 点,则是 2×3 矩阵,最后一行为[0, 0, 1]
的齐次坐标形式)。
(4)数学表示
最终的变换矩阵 M
可以表示为:
其中:
- ( s = \frac{s2}{s1} )(缩放因子)
- ( R )(旋转矩阵)
- ( t = c2 - s \cdot R \cdot c1 )(平移向量)
(5)注意事项
- 输入
points1
和points2
必须是 相同数量 的点(如 68 个人脸关键点)。 - 如果点集分布差异过大(如极端遮挡),SVD 可能无法得到正确的旋转矩阵。
- 该变换不适用于仿射或透视变换(仅适用于相似变换,即旋转 + 缩放 + 平移)。
4. 检测并提取人脸关键点
def getKeyPoints(im):rects = detector(im, 1)shape = predictor(im, rects[0])s = np.matrix([[p.x, p.y] for p in shape.parts()])return s
这段代码的作用是检测输入图像 im
中的人脸,并提取人脸关键点(facial landmarks)。它使用了 dlib
库的预训练人脸检测器和关键点预测器。以下是逐步解析:
(1)人脸检测
rects = detector(im, 1)
detector
:
这是一个dlib
的预训练人脸检测器(通常是dlib.get_frontal_face_detector()
返回的对象)。- 输入:
im
是输入图像(需为灰度图或 RGB 图)。
1
表示对图像进行上采样一次(提高检测小脸的能力)。 - 输出:
rects
是一个列表,包含检测到的所有人脸矩形框(dlib.rectangle
对象)。
rects[0]
表示选择第一张检测到的人脸(假设图像中只有一张人脸)。
(2)关键点检测
shape = predictor(im, rects[0])
predictor
:
这是一个dlib
的预训练人脸关键点检测器(通常是dlib.shape_predictor
加载的模型,如shape_predictor_68_face_landmarks.dat
)。- 输入:
im
是原始图像,rects[0]
是检测到的人脸矩形框。 - 输出:
shape
是一个包含人脸关键点的对象(68 个点,涵盖五官轮廓)。
(3)关键点格式转换
s = np.matrix([[p.x, p.y] for p in shape.parts()])
shape.parts()
:
返回所有关键点的列表(dlib.point
对象),每个点有x
和y
属性。- 列表推导式:
将关键点转换为(x, y)
坐标的列表,例如[[x1, y1], [x2, y2], ...]
。 np.matrix
:
将列表转换为 NumPy 矩阵(形状为68×2
,68 个点,每个点 2D 坐标)。
(4)返回值
s
:
返回一个68×2
的矩阵,表示 68 个人脸关键点的坐标(例如,第 0 点是下巴,第 27 点是鼻尖等)。
5. 颜色校正
def normalColor(a, b):ksize = (111, 111)aGauss = cv2.GaussianBlur(a, ksize, 0)bGauss = cv2.GaussianBlur(b, ksize, 0)weight = aGauss / bGausswhere_are_inf = np.isinf(weight)weight[where_are_inf] = 0return b * weight
这段代码的作用是 对图像 b
进行颜色校正,使其颜色分布与图像 a
相似,通常用于图像融合或颜色迁移任务。以下是逐步解析:
(1) 高斯模糊处理
ksize = (111, 111) # 高斯核大小(非常大,用于提取低频颜色信息)
aGauss = cv2.GaussianBlur(a, ksize, 0) # 对图像a进行高斯模糊
bGauss = cv2.GaussianBlur(b, ksize, 0) # 对图像b进行高斯模糊
- 高斯模糊:
使用一个非常大的核(111×111
)对输入图像a
和b
进行模糊,目的是提取图像的低频颜色信息(即整体色调,忽略细节)。 - 为什么用大核?
核越大,模糊程度越高,越能保留图像的全局颜色特征而非局部纹理。
(2) 计算颜色权重
weight = aGauss / bGauss # 计算颜色调整权重
- 权重计算:
通过aGauss / bGauss
得到一个比例矩阵weight
,表示a
和b
在低频颜色上的差异。- 如果
aGauss
比bGauss
亮(例如aGauss=200
,bGauss=100
),则weight=2
,表示需要将b
的对应区域变亮。 - 如果
aGauss
比bGauss
暗(例如aGauss=50
,bGauss=100
),则weight=0.5
,表示需要将b
的对应区域变暗。
- 如果
(3) 处理除零和无穷大
where_are_inf = np.isinf(weight) # 找到无穷大的位置
weight[where_are_inf] = 0 # 将无穷大替换为0
- 问题:
当bGauss
中某些像素值为0时,aGauss / 0
会导致weight
出现无穷大(inf
)。 - 解决:
通过np.isinf
找到这些位置,并强制设为0
(即不调整这些像素的颜色)。
(4) 应用颜色校正
return b * weight
- 输出:
将原始图像b
与权重weight
逐像素相乘,得到颜色校正后的图像。- 例如,
b
的某个像素值为[100, 150, 200]
,weight
为[1.2, 0.8, 1.0]
,则输出像素为[120, 120, 200]
。
- 例如,
(5)注意事项
- 输入范围:
a
和b
应为浮点型([0, 1]
)或整型([0, 255]
),需保持一致。 - 核大小调整:
ksize
越大,颜色迁移越全局化;越小则保留更多局部对比(但可能引入噪声)。 - 除零问题:
如果bGauss
有大片黑色区域(值为0),会导致权重失效,需提前检查图像内容。
(6)数学本质
该操作近似于对图像 b
的每个颜色通道进行 逐像素的线性变换:
其中低频分量通过高斯模糊提取。
四、完整流程
- 加载图像并检测关键点
a = cv2.imread("chendulin.jpg")
b = cv2.imread("linyuner.jpg")detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")aKeyPoints = getKeyPoints(a)
bKeyPoints = getKeyPoints(b)
- 生成人脸掩模
aMask = getFacemask(a, aKeyPoints)
bMask = getFacemask(b, bKeyPoints)
- 计算变换矩阵并应用
M = getM(aKeyPoints[POINTStupLe], bKeyPoints[POINTStupLe])
bMaskWarp = cv2.warpAffine(bMask, M, dsize, borderMode=cv2.BORDER_TRANSPARENT,flags=cv2.WARP_INVERSE_MAP)
- 融合人脸区域
mask = np.max([aMask, bMaskWarp], axis=0)
bWarp = cv2.warpAffine(b, M, dsize,borderMode=cv2.BORDER_TRANSPARENT,flags=cv2.WARP_INVERSE_MAP)
- 颜色校正和最终融合
bcolor = normalColor(a, bWarp)
out = a * (1.0 - mask) + bcolor * mask
五、效果展示
如图,我们选择一张林允儿和陈都灵的照片进行换脸,效果显示如下:
六、总结
- 关键点检测:使用dlib的68点人脸检测模型精确定位面部特征
- 人脸对齐:通过仿射变换将源人脸与目标人脸对齐
- 区域融合:使用高斯模糊的掩模实现平滑过渡
- 颜色校正:保持目标图像的光照和肤色一致性
通过这篇博客,我们详细讲解了基于Python的人脸融合技术实现。希望这能帮助你理解计算机视觉中人脸处理的基本原理和方法。
理想的风会吹进现实,熬过的夜也会变成光。加油各位!🚀🚀🚀