声明:此篇文章所用的明星照片只为用于演示代码的效果,无诋毁她人肖像之意
一、案例实现的思想
-
此案例的核心是基于人脸68个关键点 检测模型来实现的,人脸68个关键带点检测后的效果如下:
-
通过对上图中红色区域的转换,来实现换脸的操作
-
参照以下链接文章,可以帮助对此片案例内容的理解
- 基于 dlib 库的人脸68个关键点定位
- 基于 dlib 库的人脸关键部位的轮廓轮廓检测
-
实现步骤与结果如下图:
二、编辑代码
步骤解析:
-
第一步:对人脸的关键部位在68个关键点中的点集确定下来,并将红框内的各部位的点击存储在一个列表中
python""" 根据人脸68个关键点检测模型,将脸部各部位对应的点集以列表形式存储 """ 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)
-
第二步:读取两张人脸图片,并获取两张人脸图片的68个关键点
pythondef getKeyPoints(im): # 获取关键点 rects = detector(im, 1) # 获取人脸方框位置 shape = predictor(im, rects[0]) # 获取关键点 s = np.matrix([[p.x, p.y] for p in shape.parts()]) # 将关键点转换为坐标(x,y)的形式 return s a = cv2.imread("dlrb_3.jpg") # 换脸A图片 b = cv2.imread("zly.jpg") # 换脸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关键点
-
第三步:分别获取两张人脸红色区域(上图中红色)的掩膜
pythondef 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之间 # 单通道im构成3通道im(3,行,列),改变形状(行、列、3)适应0penCV im = np.array([im, im, im]).transpose((1, 2, 0)) im = cv2.GaussianBlur(im, (25, 25), 0) # 需要根据具体调整 return im 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
-
图像的几何变换主要包括:平移、旋转、缩放、剪切、仿射、透视等。
-
图像的几何变换主要分为:刚性变换、相似变换、仿射变换和透视变换(投影变换)
-
刚性变换:平移+旋转
-
相似变换:缩放+剪切
-
仿射变换:从一个二维坐标系变换到另一个二维坐标系,属于线性变换。通过已知3对坐标点可以求得变换矩阵
-
透视变换:从一个二维坐标系变换到一个三维坐标系,属于非线性变换。通过已知4对坐标点可以求得变换矩阵。
-
下图中内容是对仿射变换的简单概述
python""" 求出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)) M = getM(aKeyPoints[POINTStuple], bKeyPoints[POINTStuple])
-
-
第五步:将b的脸部(bmask)根据M仿射变换到a上
pythondsize = a.shape[:2][::-1] # 目标输出与图像a大小一致 # 需要注意,shape是(行、列),warpAffine参数dsize是(列、行) # 使用a.shape[:2][::-1],获取a的(列、行) # 函数warpAffine(src,M,dsize,dst=None, flags=None, borderMode=None, borderValue=None) # src:输入图像 # M:运算矩阵,2行3列的, # dsize:运算后矩阵的大小,也就是输出图片的尺寸 # dst:输出图像 # flags:插值方法的组合,与resize函数中的插值一样,可以查看cv2.resize # borderMode:边界模式,BORDER_TRANSPARENT表示边界透明 # borderValue:在恒定边框的情况下使用的borderValue值;默认情况下,它是 0 bMaskWarp = cv2.warpAffine(bMask, M, dsize, borderMode=cv2.BORDER_TRANSPARENT, flags=cv2.WARP_INVERSE_MAP) cv2.imshow("bMaskWarp", bMaskWarp) cv2.waitKey()
- 结果如下:
- 结果如下:
-
第六步:获取脸部最大值(两个脸模板相加)
pythonmask = np.max([aMask, bMaskWarp], axis=0) cv2.imshow("mask", mask) cv2.waitKey()
- 结果如下:
- 结果如下:
-
第七步:使用仿射矩阵M,将b映射到a
pythonbWrap = cv2.warpAffine(b, M, dsize, borderMode=cv2.BORDER_TRANSPARENT, flags=cv2.WARP_INVERSE_MAP) cv2.imshow("bWrap", bWrap) cv2.waitKey()
- 结果如下:
- 结果如下:
-
第八步:求b图片的仿射到图片a的颜色值,b的颜色值改为a的颜色
pythondef 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 bcolor = normalColor(a, bWrap) cv2.imshow("bcolor", bcolor) cv2.waitKey()
- 结果如下:
- 结果如下:
-
第九步:换脸(mask区域用bcolor,非mask区城用a)
python# 换脸 out = a * (1.0 - mask) + bcolor * mask # =========输出原始人脸、换脸结果=============== cv2.imshow("a", a) cv2.imshow("b", bOriginal) cv2.imshow("out", out / 255) cv2.waitKey() cv2.destroyAllWindows()
- 最终结果如下:
- 最终结果如下:
-
完整代码如下:
pythonimport cv2 import dlib import numpy as np """ 根据人脸68个关键点检测模型,将脸部各部位对应的点集以列表形式存储 """ 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之间 # 单通道im构成3通道im(3,行,列),改变形状(行、列、3)适应0penCV 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()]) # 将关键点转换为坐标(x,y)的形式 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 """ 读取两张人脸图片,并获取两张人脸图片的68个关键点 """ a = cv2.imread("dlrb_3.jpg") # 换脸A图片 b = cv2.imread("zly.jpg") # 换脸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关键点 bOriginal = b.copy() # 不对原来的图片b进行破坏和修改 """ 分别获取两张人脸的掩膜 """ 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的(列、行) # 函数warpAffine(src,M,dsize,dst=None, flags=None, borderMode=None, borderValue=None) # src:输入图像 # M:运算矩阵,2行3列的, # dsize:运算后矩阵的大小,也就是输出图片的尺寸 # dst:输出图像 # flags:插值方法的组合,与resize函数中的插值一样,可以查看cv2.resize # borderMode:边界模式,BORDER_TRANSPARENT表示边界透明 # borderValue:在恒定边框的情况下使用的borderValue值;默认情况下,它是 0 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", bOriginal) cv2.imshow("out", out / 255) cv2.waitKey() cv2.destroyAllWindows()