【Python实战】:基于 OpenCV + dlib 的传统换脸术原理与实战

📌 目录

  • 一、 环境准备与素材要求

  • 二、 核心原理拆解与代码解析

      1. 人脸检测与 68 点关键点提取
      1. 生成面部掩膜(Mask)
      1. 计算变换矩阵(核心数学原理)
      1. 仿射变换与颜色迁移
      1. 图像融合(进阶:泊松无缝融合)
  • 三、 完整实战代码

  • 四、 避坑指南与效果优化

  • 五、 结语

一、 环境准备与素材要求

在开始写代码前,请确保你的环境已经准备就绪。

🛠️ 依赖安装

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

📁 必备模型文件

传统换脸依赖于 dlib 的 68 点特征提取模型。请提前下载 shape_predictor_68_face_landmarks.dat(网上搜索即可获取)。

  • image3.jpg:底图(A图,目标图),比如一张证件照。

  • teacherli.jpg:源图(B图,提供脸的图)。

⚠️ 素材避坑建议 :要想换脸自然,两张图最好是绝对正脸、无刘海遮挡、光线均匀、人脸比例相近。如果表情差异过大(如一个大哭一个大类),传统 2D 换脸会因为几何形变产生严重的恐怖谷效应。

二、 核心原理拆解与代码解析

1. 人脸检测与 68 点关键点提取

我们使用 dlib 的 get_frontal_face_detector(基于 HOG 特征 + SVM)来检测人脸框,再用 shape_predictor(基于级联回归树)提取 68 个关键点。

python 复制代码
import cv2
import dlib
import numpy as np

detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")

def getKeyPoints(im):
    rects = detector(im, 1) # 1表示上采样1次,提升小脸检测率
    shape = predictor(im, rects[0])
    # 转换为 numpy 矩阵方便后续数学计算
    s = np.matrix([[p.x, p.y] for p in shape.parts()])
    return s

2. 生成面部掩膜(Mask)

我们要给 B 图的脸做个"选区"。这里用到了 凸包(Convex Hull) 算法,将五官区域包围起来并填充,然后进行高斯模糊以实现边缘羽化过渡。

python 复制代码
# 排除下巴(JAW_POINTS),只保留眉毛、眼睛、鼻子、嘴巴,这样换脸更贴合
JAW_POINTS = list(range(0, 17))
# ... 其他点的定义 ...
POINTS = [LEFT_BROW_POINTS + RIGHT_EYE_POINTS + LEFT_EYE_POINTS + RIGHT_BROW_POINTS + NOSE_POINTS + MOUTH_POINTS]
POINTStuple = tuple(POINTS)

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))
    # 高斯模糊,羽化边缘,使融合更自然(15x15 比较合适,太大容易"雾化")
    im = cv2.GaussianBlur(im, (15, 15), sigmaX=0)
    return im

3. 计算变换矩阵(核心数学原理)

如何把 B 图的五官完美贴合到 A 图?我们需要求解一个仿射变换矩阵 M 。代码使用了普氏分析(Procrustes Analysis) ,结合 SVD(奇异值分解) 来计算两组点集之间的旋转、缩放和平移。

python 复制代码
def getM(points1, points2):
    """
    计算从 points2 (B脸) 到 points1 (A脸) 的仿射变换矩阵
    """
    points1 = points1.astype(np.float64)
    points2 = points2.astype(np.float64)

    # 1. 去中心化(减去均值,消除平移影响)
    c1 = np.mean(points1, axis=0)
    c2 = np.mean(points2, axis=0)
    points1 -= c1
    points2 -= c2

    # 2. 标准化(除以标准差,消除缩放比例影响)
    s1 = np.std(points1)
    s2 = np.std(points2)
    points1 /= s1
    points2 /= s2

    # 3. 奇异值分解 (SVD) 求解最优旋转矩阵 R
    U, S, Vt = np.linalg.svd(points1.T * points2)
    R = (U * Vt).T

    # 4. 组合最终的 2x3 仿射变换矩阵
    return np.hstack(((s2 / s1) * R, c2.T - (s2 / s1) * R * c1.T))

4. 仿射变换与颜色迁移

拿到矩阵 M 后,我们用 cv2.warpAffine 对 B 图和其掩膜进行形变。

但 A、B 两图的肤质和光照往往不同,直接贴图会像"打补丁"。这里使用高斯滤波提取低频色彩信息,进行颜色迁移。

python 复制代码
def normalColor(a, b):
    """将 b 图的颜色迁移至 a 图的色调"""
    ksize = (51, 51) # 核不能太大,防止带入背景色
    aGauss = cv2.GaussianBlur(a, ksize, 0) # A图低频色彩
    bGauss = cv2.GaussianBlur(b, ksize, 0) # B图低频色彩
    weight = aGauss / bGauss               # 计算颜色比例权重
    weight[np.isinf(weight)] = 0           # 排除除以0的异常
    return b * weight

三、 完整实战代码

整合上述模块,提供一个可以直接跑的脚本:

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))

POINTS = [LEFT_BROW_POINTS + RIGHT_EYE_POINTS + LEFT_EYE_POINTS + RIGHT_BROW_POINTS + NOSE_POINTS + MOUTH_POINTS]
POINTStuple = tuple(POINTS)

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, (15, 15), sigmaX=0)
    return im

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, detector, predictor):
    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):
    ksize = (51, 51)
    aGauss = cv2.GaussianBlur(a, ksize, 0)
    bGauss = cv2.GaussianBlur(b, ksize, 0)
    weight = aGauss / bGauss
    weight[np.isinf(weight)] = 0
    return b * weight

if __name__ == '__main__':
    # 1. 读取图片
    a = cv2.imread("image3.jpg")      # 底图 A
    b = cv2.imread("teacherli.jpg")   # 源图 B

    # 2. 初始化模型
    detector = dlib.get_frontal_face_detector()
    predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")

    # 3. 获取关键点
    aKeyPoints = getKeyPoints(a, detector, predictor)
    bKeyPoints = getKeyPoints(b, detector, predictor)

    # 4. 获取掩膜
    aMask = getFaceMask(a, aKeyPoints)
    bMask = getFaceMask(b, bKeyPoints)

    # 5. 计算变换矩阵 M (B -> A)
    M = getM(aKeyPoints[POINTStuple], bKeyPoints[POINTStuple])
    dsize = a.shape[:2][::-1]

    # 6. 图像与掩膜进行仿射变换
    bMaskWarp = cv2.warpAffine(bMask, M, dsize, borderMode=cv2.BORDER_TRANSPARENT, flags=cv2.WARP_INVERSE_MAP)
    bWrap = cv2.warpAffine(b, M, dsize, borderMode=cv2.BORDER_TRANSPARENT, flags=cv2.WARP_INVERSE_MAP)

    # 7. 掩膜融合
    mask = np.max([aMask, bMaskWarp], axis=0)

    # 8. 颜色迁移
    bcolor = normalColor(a, bWrap)

    # 9. 泊松融合 (Seamless Clone)
    mask_uint8 = (mask * 255).astype(np.uint8)
    if len(mask_uint8.shape) == 3:
        mask_uint8 = cv2.cvtColor(mask_uint8, cv2.COLOR_BGR2GRAY)
    
    M_moments = cv2.moments(mask_uint8)
    if M_moments["m00"] != 0:
        cX = int(M_moments["m10"] / M_moments["m00"])
        cY = int(M_moments["m01"] / M_moments["m00"])
    else:
        cX, cY = dsize[0] // 2, dsize[1] // 2
    
    center = (cX, cY)
    
    try:
        # 使用 B 图的形变图像与 A 图进行泊松融合
        out = cv2.seamlessClone(bWrap, a, mask_uint8, center, cv2.NORMAL_CLONE)
    except Exception as e:
        print("泊松融合失败,退回 Alpha 混合。", e)
        out = a * (1.0 - mask) + bcolor * mask
        out = np.clip(out, 0, 255).astype(np.uint8)

    # 10. 显示结果
    cv2.imshow("Original A", a)
    cv2.imshow("Original B", b)
    cv2.imshow("Result", out)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

四、 避坑指南与效果优化

在实际测试中,如果你选用的两张图表情差异很大(如一张大笑,一张抿嘴) ,或者肤色差异极大(冷白皮 vs 暖黄皮),你可能会遇到以下问题及解决思路:

  1. 嘴巴变形(恐怖谷效应)

    • 原因:68 个关键点包含嘴巴。如果 A 图闭嘴,B 图大笑,强行几何映射会拉伸下巴,导致极度不自然。

    • 解决 :修改 POINTS 列表,去掉嘴巴的 48-61 个关键点,即只换眼鼻眉,保留原图的嘴部特征。

  2. 肤色割裂感

    • 原因normalColor 的高斯核太大(如原代码的 111),会混入背景颜色。

    • 解决 :将 ksize 调小至 (31, 31)(51, 51),并配合 cv2.seamlessClone 泊松融合。

  3. 头发/刘海遮挡

    • 原因:凸包会将背景(如黑发)也包进去。

    • 解决:不要使用全脸掩膜,只选取眼睛和鼻子区域。或者在代码运行前,提前用 PS 把源图 B 的刘海抠掉。

相关推荐
陈年老古董1 小时前
MediaPipe 姿态检测与脸部关键点检测
笔记·python·opencv·学习·dlib
a3535413823 小时前
DFT到FFT的推导
opencv
别动我齐刘海3 小时前
ROS2 Jazzy + C++ 实战路线——ros2_control
c++·人工智能·python·opencv·机器学习·机器人·github
浔溺4 小时前
al+大数据每日学习笔记41
opencv·计算机视觉
江畔柳前堤20 小时前
字节跳动·大模型应用知识手册
前端·人工智能·深度学习·opencv·目标检测·重构·transformer
棣廷1 天前
初识OpenCV——Dlib CNN人脸检测
人工智能·深度学习·opencv
zx_741484811 天前
【计算机视觉入门】基于 dlib 与 OpenCV 的三大案例:人脸检测、年龄性别预测与疲劳检测
opencv·计算机视觉·dnn
yyk333241 天前
OpenCV中的表情识别
opencv
爱吃火鸡面呀1 天前
基于 dlib 与 OpenCV 的人脸换脸实战:从关键点检测到无缝融合
opencv·目标检测