【换脸系列1】军装照刷爆朋友圈?教你用Python+深度学习自制换脸软件!(改进)

【换脸系列1】军装照刷爆朋友圈?教你用Python+深度学习自制换脸软件!(改进)

系列项目地址:QuantumLiu/FaceSwapper

百度网盘:pan.baidu.com/s/1dFndV1

系列第一个,主要是定义了Faceswapper这个基础类,为本系列的后续开发打下了基础。板砖轻拍,后面二三篇更精彩。

介绍

很惭愧,这个应用基本上就是matthewearl/faceswap这个项目面向对象编程的包装,规范了接口,扩展了功能。大部分工作应当归功于原作者matthewearl (Matthew Earl)

运行方式和faceswap.py基本上一样

python faceswapper.py <头路径> <脸路径> <输出图片路径>(可选,默认./output.jpg)

或使用打包好的exe

faceswapper <头路径> <脸路径> <输出图片路径>(可选,默认./output.jpg)

我将换脸器抽象成了Faceswapper类,提供规范的加载照片资源的方法和增量加载的方法,规范了swap这个主要方法的接口,返回numpy数组形式的图片数据。可供各位开发者自行开发。

示例(找了几个恶搞没什么问题的放上来,自己在电脑上想怎么玩这么玩O(∩_∩)O)





背景

最近,随着解放军建军90周年的到来,人民网推出了一个“我的军装照”的H5应用,微信用户可以上传自己的照片,生成一张我军的军装照。

(图片来自网络)

这款H5于7月29日晚一经推出,浏览量就迅猛攀升。7月30日24时,浏览次数突破6000万,7月31日上午10时突破1亿,之后呈现井喷式增长。7月31日17时,浏览次数突破2亿,8月1日13时突破5亿,24时达7.31亿。截至8月2日17时,“军装照”H5的浏览次数累计8.2亿,独立访客累计1.27亿,一分钟访问人数峰值高达41万。

8亿人晒军装照,背后原来有这些黑科技支撑_搜狐娱乐_搜狐网

其实换脸软件很早就有,3、4年前我在高一的时候就玩儿的不亦乐乎,现在看到“军装换脸”的火爆,就想自己写一个实现换脸功能的软件。

已有项目

matthewearl/faceswap

这篇中文翻译也很给力

教你用200行Python代码“换脸”

这个faceswap.py脚本已经写的比较完整了,可以直接运行


根据他的思路,换脸的过程主要是:

  1. 检测面部位置。
  2. 获得面部特征点坐标
  3. 旋转、缩放(仿射变换)第二张图像,使之与第一张图像相适应。
  4. 调整第二张图像的色彩矫正肤色。
  5. 把第二张图像的特性混合到第一张图像中。

这个总结非常好,无可挑剔。程序也work。

改进

然而,他的代码更像一个demo,距离像人民日报你要部署到线上还有很大差距,有一些可以改进的地方:

  1. 只是普通脚本,面向过程编程,较难复用。
  2. 当使用两个分辨率差距较大的照片进行换脸时,会严重影响效果。
  3. 每次要重新载入照片资源并定位、提取特征,浪费资源、性能堪忧。

面向对象编程可以将数据和方法有机结合,在本应用中,如果要上线,我们希望资源只加载一次,需要服务时只需要调用方法就可以了。

我们将这个换脸脚本改进为一个完整的换脸器类Faceswapper

考虑到效率,采用字典的数据类型来存储照片资源和对应的特征点,键值是文件名

   #头像资源
    self.heads={}
    if heads_list:
        self.load_heads(heads_list)

def load_heads(self,heads_list):
    '''
    根据head_list添加更多头像资源
    '''
    self.heads.update({os.path.split(name)[-1]:(self.read_and_mark(name)) for name in heads_list})

def get_landmarks(self,im,fname,n=1):
    '''
    人脸定位和特征提取,定位到两张及以上脸或者没有人脸将抛出异常
    im:
        照片的numpy数组
    fname:
        照片名字的字符串
    返回值:
        人脸特征(x,y)坐标的矩阵
    '''
    rects = self.detector(im, 1)

    if len(rects) > n:
        raise TooManyFaces('No face in '+fname)
    if len(rects) < 0:
        raise NoFace('Too many faces in '+fname)
    return np.matrix([[p.x, p.y] for p in self.predictor(im, rects[0]).parts()])

def read_im(self,fname,scale=1):
    '''
    读取图片
    '''
    im = cv2.imread(fname, cv2.IMREAD_COLOR)
    if type(im)==type(None):
        print(fname)
        raise ValueError('Opencv read image {} error, got None'.format(fname))
    return im

def read_and_mark(self,fname):
    im=self.read_im(fname)
    return im,self.get_landmarks(im,fname)

这样,就很容易的实现了资源的存储和增量加载,提高了效率。

同时,我们还需要处理分辨率差距问题:

def resize(self,im_head,landmarks_head,im_face,landmarks_face):
    '''
    根据头照片和脸照片的大小(分辨率)调整图片大小,增强融合效果
    '''
    scale=np.sqrt((im_head.shape[0]*im_head.shape[1])/(im_face.shape[0]*im_face.shape[1]))
    if scale>1:
        im_head=cv2.resize(im_head,(int(im_head.shape[1]/scale),int(im_head.shape[0]/scale)))
        landmarks_head=(landmarks_head/scale).astype(landmarks_head.dtype)
    else:
        im_face=cv2.resize(im_face,(int(im_face.shape[1]*scale),int(im_face.shape[0]*scale)))
        landmarks_face=(landmarks_face*scale).astype(landmarks_face.dtype)
    return im_head,landmarks_head,im_face,landmarks_face

最后,我们得到了改进过后的完整程序:

class TooManyFaces(Exception):
    '''
    定位到太多脸
    '''
    pass


class NoFace(Exception):
    '''
    没脸
    '''
    pass


class Faceswapper():
    '''
    人脸交换器类
    实例化时载入多个头照片资源
    '''
    def __init__(self,heads_list=[],predictor_path="./data/shape_predictor_68_face_landmarks.dat"):
        '''
        head_list:
            头(背景和发型)来源图片的路径的字符串列表,根据此列表在实例化时载入多个头像资源,
            并获得面部识别点坐标,以字典形式存储,键名为文件名
        predictor_path:
            dlib资源的路径
        '''
        #五官等标记点
        self.PREDICTOR_PATH = predictor_path
        self.FACE_POINTS = list(range(17, 68))
        self.MOUTH_POINTS = list(range(48, 61))
        self.RIGHT_BROW_POINTS = list(range(17, 22))
        self.LEFT_BROW_POINTS = list(range(22, 27))
        self.RIGHT_EYE_POINTS = list(range(36, 42))
        self.LEFT_EYE_POINTS = list(range(42, 48))
        self.NOSE_POINTS = list(range(27, 35))
        self.JAW_POINTS = list(range(0, 17))

        # 人脸的完整标记点
        self.ALIGN_POINTS = (self.LEFT_BROW_POINTS + self.RIGHT_EYE_POINTS + self.LEFT_EYE_POINTS +
                                       self.RIGHT_BROW_POINTS + self.NOSE_POINTS + self.MOUTH_POINTS)

        # 来自第二张图(脸)的标记点,眼、眉、鼻子、嘴,这一部分标记点将覆盖第一张图的对应标记点
        self.OVERLAY_POINTS = [self.LEFT_EYE_POINTS + self.RIGHT_EYE_POINTS + self.LEFT_BROW_POINTS + self.RIGHT_BROW_POINTS,
            self.NOSE_POINTS + self.MOUTH_POINTS]

        # 颜色校正参数
        self.COLOUR_CORRECT_BLUR_FRAC = 0.6

        #人脸定位、特征提取器,来自dlib
        self.detector = dlib.get_frontal_face_detector()
        self.predictor = dlib.shape_predictor(self.PREDICTOR_PATH)

        #头像资源
        self.heads={}
        if heads_list:
            self.load_heads(heads_list)

    def load_heads(self,heads_list):
        '''
        根据head_list添加更多头像资源
        '''
        self.heads.update({os.path.split(name)[-1]:(self.read_and_mark(name)) for name in heads_list})

    def get_landmarks(self,im,fname,n=1):
        '''
        人脸定位和特征提取,定位到两张及以上脸或者没有人脸将抛出异常
        im:
            照片的numpy数组
        fname:
            照片名字的字符串
        返回值:
            人脸特征(x,y)坐标的矩阵
        '''
        rects = self.detector(im, 1)

        if len(rects) > n:
            raise TooManyFaces('No face in '+fname)
        if len(rects) < 0:
            raise NoFace('Too many faces in '+fname)
        return np.matrix([[p.x, p.y] for p in self.predictor(im, rects[0]).parts()])

    def read_im(self,fname,scale=1):
        '''
        读取图片
        '''
        im = cv2.imread(fname, cv2.IMREAD_COLOR)
        if type(im)==type(None):
            print(fname)
            raise ValueError('Opencv read image {} error, got None'.format(fname))
        return im

    def read_and_mark(self,fname):
        im=self.read_im(fname)
        return im,self.get_landmarks(im,fname)

    def resize(self,im_head,landmarks_head,im_face,landmarks_face):
        '''
        根据头照片和脸照片的大小(分辨率)调整图片大小,增强融合效果
        '''
        scale=np.sqrt((im_head.shape[0]*im_head.shape[1])/(im_face.shape[0]*im_face.shape[1]))
        if scale>1:
            im_head=cv2.resize(im_head,(int(im_head.shape[1]/scale),int(im_head.shape[0]/scale)))
            landmarks_head=(landmarks_head/scale).astype(landmarks_head.dtype)
        else:
            im_face=cv2.resize(im_face,(int(im_face.shape[1]*scale),int(im_face.shape[0]*scale)))
            landmarks_face=(landmarks_face*scale).astype(landmarks_face.dtype)
        return im_head,landmarks_head,im_face,landmarks_face

    def draw_convex_hull(self,im, points, color):
        '''
        勾画多凸边形
        '''
        points = cv2.convexHull(points)
        cv2.fillConvexPoly(im, points, color=color)

    def get_face_mask(self,im, landmarks,ksize=(11,11)):
        '''
        获得面部遮罩
        '''
        mask = np.zeros(im.shape[:2], dtype=np.float64)

        for group in self.OVERLAY_POINTS:
            self.draw_convex_hull(mask,
                             landmarks[group],
                             color=1)

        mask = np.array([mask, mask, mask]).transpose((1, 2, 0))

        mask = (cv2.GaussianBlur(mask, ksize, 0) > 0) * 1.0
        mask = cv2.GaussianBlur(mask, ksize, 0)

        return mask

    def transformation_from_points(self,points1, points2):
        """
        Return an affine transformation [s * R | T] such that:

            sum ||s*R*p1,i + T - p2,i||^2

        is minimized.
        计算仿射矩阵
        参考:https://github.com/matthewearl/faceswap/blob/master/faceswap.py
        """
        # Solve the procrustes problem by subtracting centroids, scaling by the
        # standard deviation, and then using the SVD to calculate the rotation. See
        # the following for more details:
        #   https://en.wikipedia.org/wiki/Orthogonal_Procrustes_problem

        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)

        # The R we seek is in fact the transpose of the one given by U * Vt. This
        # is because the above formulation assumes the matrix goes on the right
        # (with row vectors) where as our solution requires the matrix to be on the
        # left (with column vectors).
        R = (U * Vt).T

        return np.vstack([np.hstack(((s2 / s1) * R,
                                           c2.T - (s2 / s1) * R * c1.T)),
                             np.matrix([0., 0., 1.])])

    def warp_im(self,im, M, dshape):
        '''
        人脸位置仿射变换
        '''
        output_im = np.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

    def correct_colours(self,im1, im2, landmarks_head):
        '''
        颜色校正
        '''
        blur_amount = int(self.COLOUR_CORRECT_BLUR_FRAC * np.linalg.norm(
                                  np.mean(landmarks_head[self.LEFT_EYE_POINTS], axis=0) -
                                  np.mean(landmarks_head[self.RIGHT_EYE_POINTS], axis=0)))
        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)
        im2_blur += (128 * (im2_blur <= 1.0)).astype(im2_blur.dtype)
        return im2.astype(np.float64) *im1_blur.astype(np.float64) /im2_blur.astype(np.float64)

    def swap(self,head_name,face_path):
        '''
        主函数 人脸交换
        head_name:
            头资源的键名字符串
        face_path:
            脸来源的图像路径名
        '''
        im_head,landmarks_head,im_face,landmarks_face=self.resize(*self.heads[head_name],*self.read_and_mark(face_path))
        M = self.transformation_from_points(landmarks_head[self.ALIGN_POINTS],
                                       landmarks_face[self.ALIGN_POINTS])

        face_mask = self.get_face_mask(im_face, landmarks_face)
        warped_mask = self.warp_im(face_mask, M, im_head.shape)
        combined_mask = np.max([self.get_face_mask(im_head, landmarks_head), warped_mask],
                                  axis=0)

        warped_face = self.warp_im(im_face, M, im_head.shape)
        warped_corrected_im2 = self.correct_colours(im_head, warped_face, landmarks_head)

        out=im_head * (1.0 - combined_mask) + warped_corrected_im2 * combined_mask
        return out

    def save(self,output_path,output_im):
        '''
        保存图片
        '''
        cv2.imwrite(output_path, output_im)

if __name__=='__main__':
    '''
    命令行运行:
    python faceswapper.py <头路径> <脸路径> <输出图片路径>(可选,默认./output.jpg)
    '''
    head,face_path,out=sys.argv[1],sys.argv[2],(sys.argv[3] if len(sys.argv)>=4 else 'output.jpg')
    swapper=Faceswapper([head])
    output_im=swapper.swap(os.path.split(head)[-1],face_path)#返回的numpy数组
    swapper.save(out,output_im)
    output_im[output_im>254.9]=254.9
    cv2.imshow('',output_im.astype('uint8'))
    cv2.waitKey()

如果需要上线,只需要实例化一次Faceswapper对象,预先加载图片资源,之后根据用户上传的图片添加资源再和已有资源进行换脸,效率更高。返回一个numpy数组,可以很容易做成服务。

示意:

swapper=Faceswapper(heads)
swapper.load_heads([custom_img])
output_im=swapper.swap(head_key,custom_img)#返回的numpy数组

同时,类是可以被继承的,在下一篇【换脸系列2】七夕节,和你的TA交♂换身体吧!我们将继承这个类,开发全新功能!

编辑于 2017-08-24

文章被以下专栏收录

    一个沉迷于编程的阿语本科在读生的梦呓。以深度学习和python为主,可能涉及量化交易、CUDA编程、MATLAB、多语言混编和哲♂学。