一、仿射变换
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()