基于 dlib 与 OpenCV 的人脸换脸实战:从关键点检测到无缝融合

1. 引言

人脸换脸(Face Swap)是计算机视觉领域中最具趣味性和挑战性的应用之一。它通过检测人脸关键点、计算仿射变换矩阵、调整颜色分布并融合图像,将一张人脸自然地"移植"到另一张图像上。本文基于 dlib 的 68 点人脸关键点检测器和 OpenCV 的图像处理能力,从零实现一个完整的换脸流程。

本文的代码参考了经典的人脸交换实现思路,并加入了大量中文注释,帮助读者理解每一步背后的数学原理和工程细节。无论你是刚接触 OpenCV 的初学者,还是希望深入理解图像配准与融合的进阶开发者,本文都能为你提供有价值的参考。

在开始之前,请确保你的环境中已经安装了以下依赖:

  • OpenCV:图像读取、仿射变换、高斯模糊、图像融合等核心操作。
  • dlib:提供预训练的人脸检测器和 68 点关键点预测器。
  • NumPy:矩阵运算和数组操作的基础库。
  • shape_predictor_68_face_landmarks.dat:dlib 官方提供的预训练模型文件,用于定位人脸 68 个关键点。

安装命令参考:

bash 复制代码
pip install opencv-python dlib numpy

模型文件可以从 dlib 官方网站下载,下载后放在与脚本相同的目录下即可。

2. 整体流程概览

换脸的核心思路可以概括为:检测人脸关键点 → 计算仿射变换 → 生成脸部掩膜 → 颜色校正 → 图像融合。下面用一个流程图来展示整体架构:

flowchart TD A[读取图像 a 和 b] --> B[检测 68 个关键点] B --> C[生成脸部掩膜 mask] C --> D[计算仿射变换矩阵 M] D --> E[将 b 的脸部变换到 a 的坐标系] E --> F[颜色校正 normalColor] F --> G[按掩膜融合输出] G --> H[显示结果]

整个流程可以拆解为以下几个关键步骤:

  1. 关键点检测:使用 dlib 检测两张图像中的人脸,并提取 68 个面部关键点。
  2. 掩膜生成:根据关键点构建人脸区域的凸包,生成二值掩膜。
  3. 仿射变换:选取右眼、左眼、鼻尖三个基准点,计算从 b 脸到 a 脸的仿射变换矩阵。
  4. 图像映射:将 b 的脸部区域按照变换矩阵映射到 a 的坐标系中。
  5. 颜色校正:通过高斯模糊计算颜色权重,使 b 脸的颜色分布与 a 脸保持一致。
  6. 融合输出:在掩膜区域内使用 b 的颜色,在非掩膜区域保留 a 的原始像素,最终合成换脸结果。

3. 人脸关键点检测

人脸关键点检测是换脸流程的第一步,也是最基础的一步。dlib 提供的预训练模型可以检测人脸 68 个关键点,这些点覆盖了眉毛、眼睛、鼻子、嘴巴和脸部轮廓等关键区域。

在代码中,我们通过以下函数获取关键点:

python 复制代码
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

这里有两个重要的对象:

  • detector:dlib 的正脸检测器,返回图像中所有人脸的矩形框。
  • predictor:68 点关键点预测器,输入人脸矩形框,输出 68 个关键点的坐标。

代码中取 rects[0] 表示只处理检测到的第一张人脸。如果你的图像中有多张人脸,需要根据实际需求选择目标人脸。

返回的关键点被组织成一个 68×2 的矩阵,每一行对应一个关键点的 (x, y) 坐标。这些点的索引顺序是固定的,例如:

  • 0-16:脸部轮廓
  • 17-21:右眉毛
  • 22-26:左眉毛
  • 27-35:鼻子
  • 36-41:右眼
  • 42-47:左眼
  • 48-60:嘴巴
  • 61-67:嘴巴内部轮廓

理解这些索引的含义,是后续构建掩膜和选取对齐点的前提。

4. 脸部掩膜生成

掩膜(Mask)的作用是标记出图像中需要替换的区域。在换脸任务中,我们需要生成一张与图像尺寸相同的二值图,其中人脸区域为 1,其余区域为 0。

代码中的 getFaceMask 函数实现了这一功能:

python 复制代码
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)
    im = np.array([im, im, im]).transpose((1, 2, 0))
    im = cv2.GaussianBlur(im, (35, 35), 0)
    return im

这里有几个值得注意的细节:

第一,凸包计算。 cv2.convexHull 用于计算一组点的凸包,即包含这些点的最小凸多边形。对于眼睛、眉毛、嘴巴等区域,凸包可以很好地勾勒出它们的轮廓。

第二,填充多边形。 cv2.fillConvexPoly 将凸包内部填充为数值 1。注意这里的 color=1 并不是颜色值,而是填充的数值,用于后续的加权融合计算。

第三,通道转换。 由于 OpenCV 的许多函数要求三通道输入,而掩膜本身是单通道的,因此代码将单通道掩膜复制为三通道:

python 复制代码
im = np.array([im, im, im]).transpose((1, 2, 0))

这一步将形状从 (h, w) 变为 (h, w, 3),虽然看起来仍然是灰度图,但格式上已经变成了彩色图,方便后续与彩色图像进行逐像素运算。

第四,高斯模糊。 最后使用 cv2.GaussianBlur 对掩膜进行平滑处理,核大小设为 (35, 35)。这一步非常关键,它让掩膜的边缘从硬边界变为渐变过渡,从而在融合时避免出现明显的接缝。核的大小需要根据图像分辨率自行调整,核越大,过渡越平滑,但也会损失更多的细节。

5. 仿射变换矩阵的计算

仿射变换是换脸流程的核心数学工具。它可以将一个平面上的点映射到另一个平面上,同时保持直线和平行关系不变。在换脸任务中,我们需要将 b 脸的关键点映射到 a 脸的对应位置。

代码中选取了三个基准点:右眼(索引 36)、左眼(索引 45)和鼻尖(索引 30):

python 复制代码
POINTStuple = [36, 45, 30]

这三个点分别对应右眼外眼角、左眼外眼角和鼻尖,它们构成了一个稳定的三角形,能够较好地描述人脸的整体姿态。

接下来,从两张图像的关键点中取出这三个点的坐标,并转换为 float32 类型:

python 复制代码
aPts = np.float32([aKeyPoints[i] for i in POINTStuple])
bPts = np.float32([bKeyPoints[i] for i in POINTStuple])

然后调用 OpenCV 的 cv2.getAffineTransform 计算仿射变换矩阵:

python 复制代码
M = cv2.getAffineTransform(aPts, bPts)

这里需要特别注意参数的顺序。getAffineTransform(src, dst) 的第一个参数是源点集,第二个参数是目标点集。在代码中,aPts 是源点,bPts 是目标点,这意味着变换矩阵 M 会将 a 脸的关键点映射到 b 脸的坐标系中。

在注释掉的代码中,有一段基于奇异值分解(SVD)的相似变换实现。它通过归一化、去均值、计算标准差等步骤,求解旋转矩阵 R 和缩放因子,最终得到变换矩阵。虽然这段代码被注释掉了,但它展示了仿射变换的数学本质,有兴趣的读者可以深入研究。

6. 图像仿射变换与映射

得到变换矩阵 M 之后,就可以将 b 的脸部区域映射到 a 的坐标系中。这里使用 cv2.warpAffine 函数:

python 复制代码
dsize = a.shape[:2][::-1]
bMaskWarp = cv2.warpAffine(bMask, M, dsize,
                           borderMode=cv2.BORDER_TRANSPARENT,
                           flags=cv2.WARP_INVERSE_MAP)

这里有两个容易踩坑的细节:

第一,目标尺寸的顺序。 a.shape[:2] 返回的是 (高, 宽),而 warpAffinedsize 参数要求的是 (宽, 高),因此需要通过 [::-1] 反转顺序。

第二,变换方向。 代码中使用了 cv2.WARP_INVERSE_MAP 标志。这个标志的含义是:传入的矩阵 M 是目标图像到源图像的逆变换。换句话说,warpAffine 会使用 M 的逆矩阵进行映射,将 b 的图像内容"反向"映射到 a 的坐标系中。

同样的操作也应用于 b 的原始图像:

python 复制代码
bWrap = cv2.warpAffine(b, M, dsize,
                       borderMode=cv2.BORDER_TRANSPARENT,
                       flags=cv2.WARP_INVERSE_MAP)

经过这一步,b 的脸部区域已经被"贴"到了 a 的对应位置上,但此时颜色可能还存在明显差异,需要进一步的颜色校正。

7. 颜色校正与归一化

由于两张图像的拍摄环境、光照条件和相机参数不同,直接叠加会产生明显的色差。为了解决这个问题,代码实现了 normalColor 函数:

python 复制代码
def normalColor(a, b):
    ksize = (135, 135)
    aGauss = cv2.GaussianBlur(a, ksize, 0)
    bGauss = cv2.GaussianBlur(b, ksize, 0)
    weight = aGauss / bGauss
    where_are_inf = np.isinf(weight)
    weight[where_are_inf] = 0
    return b * weight

这个函数的原理是:对两张图像分别进行高斯模糊,得到它们的低频分量(即整体光照和颜色分布)。然后计算两张图像低频分量的比值,作为颜色校正的权重。最后将 b 的原始像素乘以这个权重,使 b 的颜色分布向 a 靠拢。

这里有几个实现细节值得注意:

第一,核大小。 高斯模糊的核大小设为 (135, 135),这是一个非常大的核。核越大,保留的低频信息越多,颜色校正的效果越平滑。这个值需要根据图像分辨率自行调整。

第二,除零处理。bGauss 中存在 0 值时,除法会产生无穷大(inf)。代码通过 np.isinf 检测这些位置,并将其置为 0,避免后续计算出错。

第三,权重计算。 weight = aGauss / bGauss 的含义是:如果 a 的某个区域比 b 亮,权重就大于 1,b 的对应区域会被提亮;反之,权重小于 1,b 的区域会被压暗。这样,b 的整体光照就与 a 保持一致了。

8. 图像融合与输出

最后一步是将颜色校正后的 b 脸与原始 a 图像进行融合。融合的核心公式只有一行:

python 复制代码
out = a * (1.0 - mask) + bcolor * mask

这个公式的含义非常直观:

  • 在掩膜为 0 的区域(非人脸区域),out 完全取 a 的像素值。
  • 在掩膜为 1 的区域(人脸区域),out 完全取 bcolor 的像素值。
  • 在掩膜介于 0 和 1 之间的区域(边缘过渡区),out 是 a 和 bcolor 的加权平均。

由于掩膜经过了高斯模糊,边缘区域是渐变的,因此融合结果不会出现生硬的接缝。

在显示结果时,代码使用了 cv2.imshow('out', out / 255)。这里除以 255 是因为掩膜和颜色校正过程中产生的数值范围是 0 到 1,而 OpenCV 显示图像时通常期望 0 到 255 的范围。除以 255 可以将数值归一化到 0 到 1 之间,避免显示过曝。

最后,通过 cv2.waitKey() 等待用户按键,然后调用 cv2.destroyAllWindows() 关闭所有窗口。

9. 完整代码

为了方便读者直接运行,这里给出完整的代码。请确保 hmh1.jpgtest.jpgshape_predictor_68_face_landmarks.dat 与脚本位于同一目录下。图片可自行选择。

python 复制代码
import cv2
import dlib
import numpy as np
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))
第一种:关键点采集,相较于 FACE_POINTS 去掉了鼻子
POINTS = [LEFT_BROW_POINTS + RIGHT_EYE_POINTS +
LEFT_EYE_POINTS + RIGHT_BROW_POINTS + MOUTH_POINTS]
选 3 个对齐点:右眼、左眼、鼻尖
POINTStuple = [36, 45, 30]
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)
im = np.array([im, im, im]).transpose((1, 2, 0))
im = cv2.GaussianBlur(im, (35, 35), 0)
return im
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
def normalColor(a, b):
ksize = (135, 135)
aGauss = cv2.GaussianBlur(a, ksize, 0)
bGauss = cv2.GaussianBlur(b, ksize, 0)
weight = aGauss / bGauss
where_are_inf = np.isinf(weight)
weight[where_are_inf] = 0
return b * weight
读取两张图片
a = cv2.imread('hmh1.jpg')
b = cv2.imread('test.jpg')
初始化 dlib 的人脸检测器和关键点预测器
detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor('shape_predictor_68_face_landmarks.dat')
获取 A、B 两张图片的 68 个关键点
aKeyPoints = getKeyPoints(a)
bKeyPoints = getKeyPoints(b)
生成 A、B 两张图片的脸部掩膜
aMask = getFaceMask(a, aKeyPoints)
cv2.imshow('aMask', aMask)
cv2.waitKey()
bMask = getFaceMask(b, bKeyPoints)
cv2.imshow('bMask', bMask)
cv2.waitKey()
取出 3 个对齐点并转 float32
aPts = np.float32([aKeyPoints[i] for i in POINTStuple])
bPts = np.float32([bKeyPoints[i] for i in POINTStuple])
计算仿射变换矩阵
M = cv2.getAffineTransform(aPts, bPts)
将 b 的脸部掩膜根据 M 仿射变换到 a 上
dsize = a.shape[:2][::-1]
bMaskWarp = cv2.warpAffine(bMask, M, dsize,
borderMode=cv2.BORDER_TRANSPARENT,
flags=cv2.WARP_INVERSE_MAP)
cv2.imshow('bMaskWarp', bMaskWarp)
cv2.waitKey()
获取脸部最大值(两个脸模板叠加)
mask = np.max([aMask, bMaskWarp], axis=0)
cv2.imshow('mask', mask)
cv2.waitKey()
使用仿射矩阵 M,将 b 映射到 a
bWrap = cv2.warpAffine(b, M, dsize,
borderMode=cv2.BORDER_TRANSPARENT,
flags=cv2.WARP_INVERSE_MAP)
cv2.imshow('bWrap', bWrap)
cv2.waitKey()
求 b 图片仿射到图片 a 的颜色值,将 b 的颜色值改为 a 的颜色
bcolor = normalColor(a, bWrap)
cv2.imshow('bcolor', bcolor)
cv2.waitKey()
=========== 换脸:mask 区域用 bcolor,非 mask 区域用 a ===========
out = a * (1.0 - mask) + bcolor * mask
========== 输出原始人脸、换脸结果 ================
cv2.imshow('a', a)
cv2.imshow('b', b)
cv2.imshow('out', out / 255)
cv2.waitKey()
cv2.destroyAllWindows()

10. 总结

本文从零实现了一个基于 dlib 和 OpenCV 的换脸流程,完整覆盖了人脸关键点检测、脸部掩膜生成、仿射变换矩阵计算、图像映射、颜色校正与图像融合等核心环节。整个流程可以概括为:先通过 dlib 的 68 点关键点预测器定位两张人脸的特征点,再选取右眼、左眼和鼻尖三个基准点计算仿射变换矩阵,将 b 脸映射到 a 脸的坐标系中,最后借助高斯模糊生成渐变掩膜,并配合颜色校正消除两张图像之间的光照差异,从而得到自然融合的换脸结果。

在实现过程中,有几个细节对最终效果影响较大,值得读者重点关注。第一,掩膜的高斯模糊核大小需要根据图像分辨率调整,核越大过渡越平滑,但也会损失更多细节;第二,仿射变换中 cv2.WARP_INVERSE_MAP 标志决定了映射方向,理解逆变换的含义是避免结果错位的关键;第三,颜色校正的核大小同样需要根据图像尺寸调整,过大或过小都会影响光照匹配的效果。

本文提供的代码是一个基础版本,实际应用中还可以从多个方向进行优化。例如,可以引入人脸关键点的平滑处理来提升视频换脸的稳定性,也可以使用泊松融合等更高级的融合算法来进一步消除接缝,还可以结合深度学习模型提升关键点检测的精度和鲁棒性。希望读者在理解原理的基础上,动手运行代码并尝试调整参数,在实践中加深对图像配准与融合技术的理解。

相关推荐
爱吃火鸡面呀44 分钟前
MediaPipe 人体姿态估计与面部网格实战:从关键点检测到实时绘制
opencv
zx_741484811 小时前
【计算机视觉入门】OpenCV + MediaPipe + dlib:从仿射变换到换脸、手势、姿态与人脸网格
python·opencv·计算机视觉
棣廷1 小时前
初识OpenCV——疲劳检测
人工智能·opencv·目标检测
sali-tec4 小时前
C# 基于OpenCv的视觉工作流-章108-区域筛选
图像处理·人工智能·opencv·算法·计算机视觉
根目录下的猫12 小时前
RK3588适配的轻量级AI模型推荐
人工智能·后端·python·目标检测
YOLO数据集集合13 小时前
高铁轨道紧固件损坏检测数据集 | 高铁巡检 紧固件缺陷 轨道安全9093期
人工智能·目标检测·计算机视觉·目标跟踪·轨道·铁轨紧固件·铁轨
毋小黑21 小时前
753K 参数打赢 SwinIR:CRAFT 用「高频先验」给 Transformer 超分查漏补缺
人工智能·opencv·计算机视觉
TAN-90°-1 天前
Deep Learning for Computer Vision——Generative Models 2
人工智能·深度学习·神经网络·算法·目标检测·机器学习·计算机视觉
YOLO数据集集合1 天前
光伏板红外-可见光配对缺陷检测数据集 | 光伏板缺陷 红外可见光配准 多模态检测 无人机巡检 目标检测 YOLO格式9094期
人工智能·目标检测·计算机视觉·目标跟踪·红外·无人机视角·太阳能板