早前看过国外博主的一篇用200行代码实现图像换脸操作的文章,主要通过仿射变换和调色的方法,原理呢,晦涩难懂,具体效果呢,其实也并不是很理想,至少要比直接调用Face++的人脸融合接口要差很多,用于学习图像处理相关的知识还是不错的。下一篇文章就介绍一下如何调用Face++的人脸融合接口实现换脸操作。这篇文章介绍的方法主要是以下三步:
识别面部的landmark模型对第二张图像进行仿射变换(旋转、缩放、平移),使得第二张图像的面部表情与第一张图像一致调整第二张图像的颜色使得与第一张图像颜色相匹配
在dlib里,通过68组特征点从而确定一张独一无二的 人脸。训练模型是人脸识别的关键,用于查找图片的关键点。下载地址:http://dlib.net/files/。下载文件:shape_predictor_68_face_landmarks.dat.bz2。当然你也可以训练自己的人脸关键点模型。
PREDICTOR_PATH = "/home/matt/dlib-18.16/shape_predictor_68_face_landmarks.dat" detector = dlib.get_frontal_face_detector() #面部检测 predictor = dlib.shape_predictor(PREDICTOR_PATH) #特征提取 def get_landmarks(im): rects = detector(im, 1) if len(rects) > 1: raise TooManyFaces if len(rects) == 0: raise NoFaces return numpy.matrix([[p.x, p.y] for p in predictor(im, rects[0]).parts()])get_landmarks()函数通过输入一张图像,返回一个68*2的矩阵,矩阵每一行的x,y元素表征每一个关键点的特征。特征提取器需要一个粗略的矩形检测边框作为输入,而这个输入是由面部检测器提供,它返回了一系列框起人脸的矩形。
仿射变换就是线性变换再加上平移。旋转 (线性变换)缩放操作(线性变换)平移 (向量加)如果没有了第3个平移的操作,那它就是线性变换。
将这三种变换写成矩阵形式:
这个式子中,s就是缩放比例,θ就是旋转角度,最后的T代表平移的位移,其中R是一个正交矩阵。
图像的变换要对图像的每一个像素点进行操作,假设其中的一个像素点的坐标是(x,y),我们用矩阵形式表示:
我们通常使用2x3矩阵来表示仿射变换:
其中矩阵A控制旋转和伸缩,矩阵B控制平移,矩阵M是完整的仿射变换矩阵。经过仿射变换后的点的矩阵坐标是T,我们已经知道放射变换就是线性变换加上平移,用矩阵表示的话就是
计算可得
图像平移的代码:
import cv2 import numpy as np img = cv2.imread('Rachel.jpg', 0) rows, cols = img.shape M = np.float32([[1, 0, 200], [0, 1, 100]]) dst = cv2.warpAffine(img, M, (cols, rows)) cv2.imshow('img', dst) k = cv2.waitKey(0) if k == ord('s'): cv2.imwrite('Rachel3.jpg', dst) cv2.destroyAllWindows()得到经过仿射变换后的点的坐标是(x+200,y+100),即将整个图像平移(200,100)
旋转图像的代码:
import cv2 img = cv2.imread('Rachel.jpg', 0) rows, cols = img.shape M = cv2.getRotationMatrix2D((cols / 2, rows / 2), 90, 1) dst = cv2.warpAffine(img, M, (cols, rows)) # 仿射变换 cv2.imshow('Rachel', dst) cv2.waitKey(0) cv2.destroyAllWindows()第一个参数是中心点的坐标,以(cols / 2, rows / 2)为中心点,逆时针旋转90度(若是正值,则顺时针旋转)。
那么如何通过仿射变换任意变换图形呢?我们需要源图像和目标图像上分别一一映射的三个点来定义仿射变换
img = cv2.imread('Rachel.jpg') rows, cols, ch = img.shape pts1 = np.float32([[0, 0], [cols - 1, 0], [0, rows - 1]]) #原始的三个点 pts2 = np.float32([[cols * 0.2, rows * 0.1], [cols * 0.9, rows * 0.2], [cols * 0.1, rows * 0.9]]) #新的三个点 M = cv2.getAffineTransform(pts1, pts2) #计算仿射变换矩阵 dst = cv2.warpAffine(img, M, (cols, rows)) #生成仿射变换 cv2.imshow('image', dst) k = cv2.waitKey(0) if k == ord('s'): cv2.imwrite('Rachel1.jpg', dst) cv2.destroyAllWindows()现在呢,我们通过特征提取器获得了两张人脸的68*2landmark矩阵,每一行都都表征不同面部特征,接下来,我们要计算通过什么样的仿射变换(旋转、缩放、平移),使得第一个矢量的点尽可能的匹配第二个矢量的点。
一个想法是使用仿射变换将第一个图像变换覆盖第二个图像。如何判断这种对齐的效果呢?使用最小二乘法,使得变化后所有点与目标点距离和最小。
T:平移 (区别于小T转置) R:旋转 S:缩放 。两个形状矩阵分别为p和q,矩阵的每一行代表一个特征点的x,y坐标,
假设有68个特征点坐标,则,写成数学形式:
pi就是p矩阵的第i行,写成矩阵形式:
代表Frobenius范数,就是每一项的平方和。
形如(1):
根据维基百科的定义,这个最小值问题是有解析解的。只需将下式(2)进行变化,写成(1)式的样子。这里的变化就需要对原始点集p和q进行一些处理:
这里给出了对原始点集的变化步骤。结合代码来看:
#消除平移T的影响 c1 = numpy.mean(points1, axis=0) c2 = numpy.mean(points2, axis=0) points1 -= c1 points2 -= c2 #消除缩放系数S的影响 s1 = numpy.std(points1) s2 = numpy.std(points2) points1 /= s1 points2 /= s2这两步处理以后,R就可以变成求解下面的式子:
这里的A,B不再是原始的数据点集,而是变成了处理以后点集。根据维基百科的求解方法:
确切的说,应该是特征值分解(针对m*m的矩阵A),为什么用特征值分解呢?前面提到,这个R是一个正交矩阵(如果AAT=E(E为单位矩阵)或ATA=E,则n阶实矩阵A称为正交矩阵),我查了奇异值分解的定义,就比较明了了:
这样就解出了R:
U, S, Vt = numpy.linalg.svd(points1.T * points2) R = (U * Vt).T总结这个过程的步骤如下:
将输入矩阵转换为浮点数。 这是后续操作所必需的。减去每个点集的质心。 一旦找到了针对所得点集的最佳缩放比例和旋转度,就可以使用质心c1和c2来找到完整的解。同样,将每个点集除以其标准偏差。 这消除了问题的缩放分量。使用奇异值分解计算旋转部分。 有关其工作原理的详细信息,请参见“正交前凸问题”上的Wikipedia文章。返回完整的变换作为仿射变换矩阵。最后通过openCV的cv2.warpAffine函数对第二个图进行变换,从而匹配第一个图。
def warp_im(im, M, dshape): output_im = numpy.zeros(dshape, dtype=im.dtype) cv2.warpAffine(im, M[:2], (dshape[1], dshape[0]), dst=output_im, borderMode=cv2.BORDER_TRANSPARENT, flags=cv2.WARP_INVERSE_MAP) return output_im两个图像之间的肤色和光照差异导致重叠区域边缘周围的不连续性。 让我们尝试纠正该问题:
COLOUR_CORRECT_BLUR_FRAC = 0.6 LEFT_EYE_POINTS = list(range(42, 48)) RIGHT_EYE_POINTS = list(range(36, 42)) def correct_colours(im1, im2, landmarks1): blur_amount = COLOUR_CORRECT_BLUR_FRAC * numpy.linalg.norm( numpy.mean(landmarks1[LEFT_EYE_POINTS], axis=0) - numpy.mean(landmarks1[RIGHT_EYE_POINTS], axis=0)) blur_amount = int(blur_amount) if blur_amount % 2 == 0: blur_amount += 1 im1_blur = cv2.GaussianBlur(im1, (blur_amount, blur_amount), 0) im2_blur = cv2.GaussianBlur(im2, (blur_amount, blur_amount), 0) # Avoid divide-by-zero errors. im2_blur += 128 * (im2_blur <= 1.0) return (im2.astype(numpy.float64) * im1_blur.astype(numpy.float64) / im2_blur.astype(numpy.float64))此函数尝试更改im2的颜色以匹配im1的颜色。 它通过将im2除以im2的高斯模糊,然后再乘以im1的高斯模糊来实现。 这里的想法是RGB缩放色彩校正,但是每个像素都有自己的局部缩放因子,而不是在整个图像中使用恒定的缩放因子。
通过这种方法,可以在一定程度上解决两个图像之间的照明差异。 例如,如果从一侧照亮图像1,但是图像2具有均匀的照度,则颜色校正后的图像2在未照亮的一侧也会显得更暗。
就是说,这是解决该问题的一个很粗略的解决方案,适当大小的高斯核是关键。 第一张图片中的面部特征太小,将在第二张图片中显示。 面部区域外有太大的内核杂散,无法覆盖像素,并且会发生变色。 在这里,使用0.6 *瞳孔距离的内核。
建立一个蒙版。值为1的区域(此处显示为白色)与应显示图像2的区域对应,而值为0的区域(此处显示为黑色)与应显示图像1的区域对应。 0到1之间的值对应于图像1和image2的混合。
LEFT_EYE_POINTS = list(range(42, 48)) RIGHT_EYE_POINTS = list(range(36, 42)) LEFT_BROW_POINTS = list(range(22, 27)) RIGHT_BROW_POINTS = list(range(17, 22)) NOSE_POINTS = list(range(27, 35)) MOUTH_POINTS = list(range(48, 61)) OVERLAY_POINTS = [ LEFT_EYE_POINTS + RIGHT_EYE_POINTS + LEFT_BROW_POINTS + RIGHT_BROW_POINTS, NOSE_POINTS + MOUTH_POINTS, ] FEATHER_AMOUNT = 11 def draw_convex_hull(im, points, color): points = cv2.convexHull(points) #寻找凸包 cv2.fillConvexPoly(im, points, color=color) #填充凸多边形 def get_face_mask(im, landmarks): im = numpy.zeros(im.shape[:2], dtype=numpy.float64) for group in OVERLAY_POINTS: draw_convex_hull(im, landmarks[group], color=1) im = numpy.array([im, im, im]).transpose((1, 2, 0)) im = (cv2.GaussianBlur(im, (FEATHER_AMOUNT, FEATHER_AMOUNT), 0) > 0) * 1.0 im = cv2.GaussianBlur(im, (FEATHER_AMOUNT, FEATHER_AMOUNT), 0) return im mask = get_face_mask(im2, landmarks2) warped_mask = warp_im(mask, M, im1.shape) combined_mask = numpy.max([get_face_mask(im1, landmarks1), warped_mask], axis=0)注意:
img.shape[:2] 取彩色图片的长、宽。 如果img.shape[:3] 则取彩色图片的长、宽、通道。 关于img.shape[0]、[1]、[2] img.shape[0]:图像的垂直尺寸(高度) img.shape[1]:图像的水平尺寸(宽度) img.shape[2]:图像的通道数 在矩阵中,[0]就表示行数,[1]则表示列数。get_face_mask()用来生成这个蒙版。
它以白色绘制两个凸多边形:一个围绕眼睛区域,另一个围绕鼻子和嘴巴区域。 然后,它将蒙版的边缘向外羽化11个像素。 羽化有助于隐藏任何剩余的不连续性。对于两个图像都生成这样的面罩。 使用放射变换,将第二个遮罩转换为图像1的坐标空间。然后通过采用逐个元素的最大值将这些蒙版组合为一个蒙版。 组合两个遮罩可确保遮盖图像1中的特征,并确保图像2中的特征完整显示。
最后,通过蒙版给出最终图片:
output_im = im1 * (1.0 - combined_mask) + warped_corrected_im2 * combined_mask