使用OpenCV 和 Dlib 实现人脸融合技术

发布于:2025-05-08 ⋅ 阅读:(23) ⋅ 点赞:(0)

引言

本文将介绍如何使用Python、OpenCV和dlib库实现人脸融合技术,将一张人脸的特征无缝融合到另一张人脸上。

一、技术概述

人脸融合是一种将两张人脸的特征进行智能融合的技术,主要包含以下几个关键步骤:

  1. 人脸关键点检测
  2. 人脸对齐和仿射变换
  3. 人脸区域分割和融合
  4. 颜色校正

二、环境准备

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

这段代码的作用是计算两组点集(points1points2)之间的相似变换矩阵(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  
  • 计算两组点的均值 c1c2(相当于中心点)。
  • 将点集中心化(减去均值),使得它们的中心都位于 (0, 0),方便后续计算旋转和缩放。
s1 = np.std(points1)  
s2 = np.std(points2)  
points1 /= s1  
points2 /= s2  
  • 计算两组点的标准差 s1s2(衡量点的分布范围)。
  • 对点集进行缩放归一化(除以标准差),使它们的尺度一致(类似于 Z-score 标准化)。

(2)计算旋转矩阵(SVD 分解)

U, S, Vt = np.linalg.svd(points1.T @ points2)  
R = (U @ Vt).T  
  • points1.T @ points2 进行奇异值分解(SVD),得到 USVt
  • 旋转矩阵 R 的计算方式为 R = U @ Vt,并转置(.T)使其适用于列向量变换。
  • 这一步的目的是找到最优旋转,使得 points1points2 对齐。

(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)注意事项

  • 输入 points1points2 必须是 相同数量 的点(如 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 对象),每个点有 xy 属性。
  • 列表推导式
    将关键点转换为 (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 / bGauss
    where_are_inf = np.isinf(weight)
    weight[where_are_inf] = 0
    return b * weight

这段代码的作用是 对图像 b 进行颜色校正,使其颜色分布与图像 a 相似,通常用于图像融合或颜色迁移任务。以下是逐步解析:


(1) 高斯模糊处理

ksize = (111, 111)  # 高斯核大小(非常大,用于提取低频颜色信息)
aGauss = cv2.GaussianBlur(a, ksize, 0)  # 对图像a进行高斯模糊
bGauss = cv2.GaussianBlur(b, ksize, 0)  # 对图像b进行高斯模糊
  • 高斯模糊
    使用一个非常大的核(111×111)对输入图像 ab 进行模糊,目的是提取图像的低频颜色信息(即整体色调,忽略细节)。
  • 为什么用大核?
    核越大,模糊程度越高,越能保留图像的全局颜色特征而非局部纹理。

(2) 计算颜色权重

weight = aGauss / bGauss  # 计算颜色调整权重
  • 权重计算
    通过 aGauss / bGauss 得到一个比例矩阵 weight,表示 ab 在低频颜色上的差异。
    • 如果 aGaussbGauss 亮(例如 aGauss=200bGauss=100),则 weight=2,表示需要将 b 的对应区域变亮。
    • 如果 aGaussbGauss 暗(例如 aGauss=50bGauss=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)注意事项

  1. 输入范围
    ab 应为浮点型([0, 1])或整型([0, 255]),需保持一致。
  2. 核大小调整
    ksize 越大,颜色迁移越全局化;越小则保留更多局部对比(但可能引入噪声)。
  3. 除零问题
    如果 bGauss 有大片黑色区域(值为0),会导致权重失效,需提前检查图像内容。

(6)数学本质
该操作近似于对图像 b 的每个颜色通道进行 逐像素的线性变换
在这里插入图片描述

其中低频分量通过高斯模糊提取。


四、完整流程

  1. 加载图像并检测关键点
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)
  1. 生成人脸掩模
aMask = getFacemask(a, aKeyPoints)
bMask = getFacemask(b, bKeyPoints)
  1. 计算变换矩阵并应用
M = getM(aKeyPoints[POINTStupLe], bKeyPoints[POINTStupLe])
bMaskWarp = cv2.warpAffine(bMask, M, dsize, 
                          borderMode=cv2.BORDER_TRANSPARENT,
                          flags=cv2.WARP_INVERSE_MAP)
  1. 融合人脸区域
mask = np.max([aMask, bMaskWarp], axis=0)
bWarp = cv2.warpAffine(b, M, dsize,
                      borderMode=cv2.BORDER_TRANSPARENT,
                      flags=cv2.WARP_INVERSE_MAP)
  1. 颜色校正和最终融合
bcolor = normalColor(a, bWarp)
out = a * (1.0 - mask) + bcolor * mask

五、效果展示

如图,我们选择一张林允儿和陈都灵的照片进行换脸,效果显示如下:
在这里插入图片描述

六、总结

  1. 关键点检测:使用dlib的68点人脸检测模型精确定位面部特征
  2. 人脸对齐:通过仿射变换将源人脸与目标人脸对齐
  3. 区域融合:使用高斯模糊的掩模实现平滑过渡
  4. 颜色校正:保持目标图像的光照和肤色一致性

通过这篇博客,我们详细讲解了基于Python的人脸融合技术实现。希望这能帮助你理解计算机视觉中人脸处理的基本原理和方法。

理想的风会吹进现实,熬过的夜也会变成光。加油各位!🚀🚀🚀


网站公告

今日签到

点亮在社区的每一天
去签到