基于Opencv和Dlib的人脸换脸实现

一、仿射变换

1.1 简介

仿射变换是一种几何变换,它是一组保持平直性和比例性的线性 变换和平移的组合。简单来说,仿射变换会将直线映射为直线,保持线段之间的比例关系,但可能会改变图形的角度和大小。

从一个二维坐标系变换到另一个二维坐标系,属于线性变换。通过已知3对坐标点可以求得变换矩阵

1.2 数学定义

公式为:

其中

在二维平面中仿射变换可以表示为:

其中:

1.3仿射变换的基本操作

仿射变换由以下基本操作组合而成:

1.3.1 平移

平移是简单的移动位置,其二维平面的变换矩阵为:

1.3.2 缩放

缩放会调整图形的大小,其变换矩阵为:

1.3.3 旋转

旋转会绕某个点进行角度的变化,其变换矩阵为:

其中 θ\thetaθ 是逆时针旋转的角度。

1.3.4剪切

剪切会改变图形的形状,例如将矩形变成平行四边形。其变换矩阵为:

其中 kx,ky是剪切因子。

1.3.5 仿射变换的矩阵形式(齐次坐标)

仿射变换通常使用 齐次坐标 表示,将 nnn-维向量扩展为 n+1n+1n+1 维。其通用形式为:

在齐次坐标中,仿射变换矩阵可以同时表示旋转、缩放、剪切和平移,使得所有操作都可以用矩阵乘法统一处理。

1.4仿射变换的性质

保持直线性

仿射变换会将直线映射为直线,保持图形的基本结构。

保持比例性

仿射变换不会改变线段之间的比例关系。

角度可能变化

仿射变换可能改变角度和形状,但不会破坏共线性或共面性。

二、仿射变换代码介绍

2.1重要函数介绍

python 复制代码
dst = cv2.warpAffine(
    src,        # 输入图像
    M,          # 2×3 仿射变换矩阵
    dsize,      # 输出图像尺寸 (宽度, 高度)
    flags=None, # 插值方式
    borderMode=None, # 边界填充方式
    borderValue=None # 边界填充颜色
)

参数介绍:

src: 输入的图像

M:仿射变换矩阵(必须是 float32 类型)

dsize:输出图像的尺寸 ,格式固定为:(宽度, 高度),元组形式

flags:插值方式

· 默认有四种方式:

cv2.INTER_LINEAR默认,双线插值

cv2.INTER_NEAREST:最近邻插值

cv2.INTER_CUBIC:三次插值

cv2.INTER_AREA:区域插值

borderMode:图像变换后,空白区域怎么填充

常用以下几种方式:

cv2.BORDER_CONSTANT固定颜色填充, 颜色由 borderValue 指定

cv2.BORDER_REPLICATE:复制图像边缘的像素向外延伸

cv2.BORDER_WRAP:平铺镜像(重复图像)

cv2.BORDER_REFLECT:镜像反射

borderValue:边界颜色

只有当 borderMode=cv2.BORDER_CONSTANT 时才生效,格式**(B,G,R)** ,默认黑色 (0,0,0)

2.2 代码介绍

这里对于仿射变换矩阵采用在原图和目标图像上各选择三个点来得到变换矩阵

即采用:

python 复制代码
M = cv2.getAffineTransform(原图上的三个点, 目标图上的三个点)

代码

python 复制代码
import cv2
import numpy as np

"""----------------仿射变换----------------"""
img = cv2.imread(r'..\data\face_image\img.png')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
height, width = img.shape[:2]

# 在原图像和目标图像上各选择三个点
mat_src = np.float32([[0, 0],[0, height-1],[width-1, 0]])
mat_dst = np.float32([[0, 0],[100, height-100],[width-100, 100]])

M = cv2.getAffineTransform(mat_src, mat_dst)# 得到变换矩阵M


dst = cv2.warpAffine(img, M, dsize=(width,height))# 进行仿射变换


imgs = np.hstack([img,dst])
imgs = cv2.cvtColor(imgs, cv2.COLOR_RGB2BGR)
cv2.namedWindow( 'imgs', cv2.WINDOW_NORMAL)#可自动调整窗口大小,避免图片太大不便查阅
cv2.imshow( "imgs",imgs)
cv2.waitKey(0)

三、图片换脸

这里使用了使用了Dlib库的人脸检测器、特征点预测器,以及OpenCV的图像处理函数。

加载了Dlib的正面人脸检测器detector,以及68个特征点预测模型predictor,后者会用来提取人脸的关键特征点。

如上图,将17-68之间的点(包括17和68)作为换脸的关键点

思路:读取人脸图片--->获取两张图片的68个关键点------>根据68个关键点获取两张图片的掩膜------>计算仿射变换矩阵------>仿射变换图片------>调整图片的颜色使得不突兀

代码:

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))
# 关键点集
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)  #填充凸包,数字在0~1之间
        # cv2.drawContours(im,[points],-1,1,-1)
    # 单通道im构成3通道im(3,行,列),改变形状(行、列、3)适应OpenCV
    im = np.array([im, im, im]).transpose((1, 2, 0))
    im = cv2.GaussianBlur(im,(25,25), 0)#这个参数比较重要,需要调整  !!!!!!!!!!!!!!
    return im

"""求出b脸仿射变换到a脸的变换矩阵M,此处用到的算法难以理解,大家可直接跳过"""
def getM(points1, points2):
    points1 = points1.astype(np.float64) #int8转换为浮点数类型
    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   # 除标准差,计算出归一化的结果

    # 奇异值分解,Singular Value Decomposition
    U, S, Vt = np.linalg.svd(points1.T * points2)
    R = (U * Vt).T# 通过U和Vt找到R
    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])  # 获取关键点
    s= np.matrix([[p.x, p.y] for p in shape.parts()])
    return s

"""修改b图的颜色值,与a图相同"""
def normalColor(a, b):
    ksize=(111,111)   #非常大的核,去噪等运算时为11就比较大了    # !!!!!!!!!!!!
    aGauss = cv2.GaussianBlur(a, ksize, 0)  #对a进行高斯滤波
    bGauss = cv2.GaussianBlur(b, ksize, 0)  #对b进行高斯滤波
    weight= aGauss/ bGauss  # 计算目标图像调整颜色的权重值,存在0除警告,可忽略。
    where_are_inf = np.isinf(weight)
    weight[where_are_inf] = 0
    return b * weight

a=cv2.imread(r'..\data\face_image\img.png')    #换脸A图片
b=cv2.imread(r'..\data\face_image\image.jpeg')   #换脸B图片

detector = dlib.get_frontal_face_detector() #构造脸部位置检测器
predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")#读取人脸关键点定位模型

aKeyPoints = getKeyPoints(a)    #获取A图片的68关键点
bKeyPoints = getKeyPoints(b)    #获取B图片的68关键点

aMask = getFaceMask(a, aKeyPoints)#获取图片A的人脸掩膜
cv2.imshow("aMask", aMask)
cv2.waitKey()

bMask = getFaceMask(b, bKeyPoints)#获取图片B的人脸掩膜
cv2.imshow("bMask", bMask)
cv2.waitKey()

"""求出b脸仿射变换到a脸的变换矩阵M"""
M = getM(aKeyPoints[POINTStuple],bKeyPoints[POINTStuple])

"""将b的脸部(bmask)根据M仿射变换到a上"""
dsize=a.shape[:2][::-1]
# 目标输出与图像a大小一致
# 需要注意,shape是(行、列),warpAffine参数dsize是(列、行)
# 使用a.shape[:2][::-1],获取a的(列、行)


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()
# ===========step8: 换脸(mask区域用bcolor,非mask区域用a)=============
out = a * (1.0 - mask) + bcolor * mask
# =========输出原始人脸、换脸结果===============
cv2.imshow("a",a)
cv2.imshow("b",b)
cv2.imshow("out",out/255)    #数值为浮点数,以为你是归一化后的数据,超过1的全是白色。手动实现归一化。
cv2.waitKey()
cv2.destroyAllWindows()

四、视频换脸

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))
# 关键点集
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)  #填充凸包,数字在0~1之间
        # cv2.drawContours(im,[points],-1,1,-1)
    # 单通道im构成3通道im(3,行,列),改变形状(行、列、3)适应OpenCV
    im = np.array([im, im, im]).transpose((1, 2, 0))
    im = cv2.GaussianBlur(im,(25,25), 0)#这个参数比较重要,需要调整  !!!!!!!!!!!!!!
    return im

"""求出b脸仿射变换到a脸的变换矩阵M,此处用到的算法难以理解,大家可直接跳过"""
def getM(points1, points2):
    points1 = points1.astype(np.float64) #int8转换为浮点数类型
    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   # 除标准差,计算出归一化的结果

    # 奇异值分解,Singular Value Decomposition
    U, S, Vt = np.linalg.svd(points1.T * points2)
    R = (U * Vt).T# 通过U和Vt找到R
    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])  # 获取关键点
    s= np.matrix([[p.x, p.y] for p in shape.parts()])
    return s

"""修改b图的颜色值,与a图相同"""
def normalColor(a, b):
    ksize=(111,111)   #非常大的核,去噪等运算时为11就比较大了    # !!!!!!!!!!!!
    aGauss = cv2.GaussianBlur(a, ksize, 0)  #对a进行高斯滤波
    bGauss = cv2.GaussianBlur(b, ksize, 0)  #对b进行高斯滤波
    weight= aGauss/ bGauss  # 计算目标图像调整颜色的权重值,存在0除警告,可忽略。
    where_are_inf = np.isinf(weight)
    weight[where_are_inf] = 0
    return b * weight

a=cv2.VideoCapture(0)  #换脸A图片
if not a.isOpened():
    print('Cannot open video')
    exit()

b=cv2.imread(r'..\data\face_image\img.png')   #换脸B图片

detector = dlib.get_frontal_face_detector() #构造脸部位置检测器
predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")#读取人脸关键点定位模型


bKeyPoints = getKeyPoints(b)    #获取B图片的68关键点

bMask = getFaceMask(b, bKeyPoints)#获取图片B的人脸掩膜
# cv2.imshow("bMask", bMask)
# cv2.waitKey()


while True:
    ret, frame = a.read()
    if not ret:
        break
    aKeyPoints = getKeyPoints(frame)    #获取A图片的68关键点

    aMask = getFaceMask(frame, aKeyPoints)#获取图片A的人脸掩膜
    # cv2.imshow("aMask", aMask)
    # cv2.waitKey()

    """求出b脸仿射变换到a脸的变换矩阵M"""
    M = getM(aKeyPoints[POINTStuple],bKeyPoints[POINTStuple])

    """将b的脸部(bmask)根据M仿射变换到a上"""
    dsize=frame.shape[:2][::-1]
    # 目标输出与图像a大小一致
    # 需要注意,shape是(行、列),warpAffine参数dsize是(列、行)
    # 使用a.shape[:2][::-1],获取a的(列、行)


    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(frame, bWrap)
    # cv2.imshow("bcolor",bcolor)
    # cv2.waitKey()
    # ===========step8: 换脸(mask区域用bcolor,非mask区域用a)=============
    out = frame * (1.0 - mask) + bcolor * mask
    # =========输出原始人脸、换脸结果===============
    # cv2.imshow("a",frame)
    # cv2.imshow("b",b)
    cv2.imshow("out",out/255)    #数值为浮点数,以为你是归一化后的数据,超过1的全是白色。手动实现归一化。
    if cv2.waitKey(60) == 27:
        break
cv2.destroyAllWindows()
相关推荐
没有退路那我就不要散步2 小时前
升级NPU驱动和固件,对上层的AI推理服务有多大影响?
人工智能
CSDN官方博客2 小时前
【奖励到账】CSDN AI 社区镜像创作激励活动第十二批奖励补发发放!
人工智能
电子科技圈2 小时前
赋能高端音频功能促进多样化设备创新——XMOS USB Audio平台实现四大功能升级
人工智能·mcu·音视频·智能家居·边缘计算·语音识别·智能硬件
nunca_te_rindas2 小时前
deepseek专家模式--20260408
人工智能
xinxiangwangzhi_2 小时前
立体匹配--SelectiveStereo(2024)
计算机视觉
AI成长日志2 小时前
【AI原生开发实战】2.1 Prompt工程基础:编写高质量提示词
人工智能·prompt·ai-native
ar01232 小时前
AR远程协助平台:重塑工业与服务协作的新模式
人工智能·ar
ar01232 小时前
AR远程指导:赋能工业智能化的关键力量
人工智能·ar
开开心心就好2 小时前
支持自定义名单的实用随机抽签工具
windows·计算机视觉·计算机外设·excel·散列表·启发式算法·csdn开发云