川普撞脸希拉里(基于 OpenCV 的面部特征交换)-2

基于 OpenCV 的面部特征交换——实验二

一、课程介绍

今天这门课程将通过 OpenCV 库来实现人脸面部特征交换,其实就是将第二张人脸的眼睛、鼻子和嘴巴通过程序自动裁剪适配并覆盖到第一张人脸上,并且为了使得修改后的照片看着更加自然,我们还需要调整皮肤颜色。

说明:本次实验课程的项目来源于 https://github.com/matthewearl/faceswap ,该项目基于 MIT 许可证。

该项目我将通过两次实验课来实现,这是本项目的第二次实验。

1.1 课程知识点

通过本次课程,我们将接触到以下几个知识点:

  • OpenCV 库的使用
  • dlib 库的使用
  • docopt 库的使用

1.2 主要流程

实验的流程为:

  • 环境配置
  • 设计思路
  • 编程实现

1.3 实验环境

Python:3.4.3
OpenCV:3.1.0
dlib:1.9.0

1.4 效果截图

最终我们程序的转换效果如下。

这是原图。

此处输入图片的描述

此处输入图片的描述

转换之后的结果是这样的。

此处输入图片的描述

二、环境配置

所谓“工欲善其事,必先利其器”,开始编写代码之前我们需要先把用到的库安装好。

依赖的库有:

  • dlib
  • opencv
  • docopt

2.1 安装 dlib

dlib 是一个基于 C++ 编写的扩展库,包含有许多常用的机器学习算法以及图像处理函数。并且还支持大量的数值计算,如矩阵、大整数随机运算等。但是在编译安装 dlib 之前我们还需要先给系统装上各种依赖环境,步骤如下。

参考:dlib 官网

安装 Python 的开发库 python3-dev 和 python3-setuptools 。

$ sudo apt-get update
$ sudo apt-get install python3-dev python3-setuptools

另外还要安装 Boost Python 开发文件,它为 Python 编程提供了简单易用的 C++ 函数库接口。

$ sudo apt-get install libboost-python-dev

dlib 本来可以直接通过 pip3 指令进行编译安装,但是由于虚拟机的内存资源不足无法完成编译,因此我这里提供一份编译好的 dlib 动态库。可以通过 wget 命令进行下载,并将 dlib.so 复制到 /usr/local/lib/python3.4/dist-packages/ 目录之下,这样就可以全局使用该模块了。

$ wget http://labfile.oss.aliyuncs.com/courses/686/dlib.so
$ sudo cp dlib.so /usr/local/lib/python3.4/dist-packages/

2.2 安装 OpenCV

OpenCV 是一款功能强大的跨平台计算机视觉开源库,可以用于解决人机交互、物体检测、人脸识别等领域的问题。库本身是采用 C++ 编写的,但是同时也对 Python, Java, C# 等语言提供接口支持。

本门课程考虑到 OpenCV 的安装过程相对较繁琐且耗时较长,因此实验环境已经配置好 OpenCV 3.1 的环境。

此处输入图片的描述

参考:如果想知道如何编译 OpenCV 的同学可以参考以下官方文档。

2.3 安装 docopt

docopt 是 Python 的一个第三方参数解析库,可以根据使用者提供的文档描述自动生成解析器。因此使用者可以用它来定义交互参数与解析参数。

安装 docopt 库的过程就非常简单了。

$ sudo pip3 install docopt

参考:http://docopt.org/

三、设计思路

我们先来回顾一下上节课介绍的这个程序需要完成的两大个功能。

  1. 支持从命令行获取指定图像路径
  2. 读取指定图像并进行处理保存

第一点要求通过借助 docopt 库可以非常快速便捷地构建命令行解析器,具体用法在前一次试验中已经介绍过了。

而针对第二点,我们的处理方法主要分为以下几个步骤。

  1. 借助 dlib 库检测出图像中的脸部特征
  2. 计算将第二张图像脸部特征对齐到一张图像脸部特征的变换矩阵
  3. 综合考虑两张照片的面部特征获得合适的面部特征掩码
  4. 根据第一张图像人脸的肤色修正第二张图像
  5. 通过面部特征掩码拼接这两张图像
  6. 保存图像

四、编程实现

进入 Code 目录,创建 course_686 文件夹作为项目目录,之后所有的资源文件都位于该文件夹之下。

$ cd Code
$ mkdir face_swap && cd face_swap

4.1 实现命令行解析器

新建文件 face_swap.py 编写以下代码。

"""
faceswap can put facial features from one face onto another.

Usage: faceswap [options] <image1> <image2>

Options:
    -v --version     show the version.
    -h --help        show usage message.
"""

import cv2
import dlib
import numpy as np
from docopt import docopt

__version__ = '1.0'

def main():
    arguments = docopt(__doc__, version=__version__)

if __name__ == '__main__':
    main()

在这部分我们导入了需要用到的库,并且编写了帮助文档,程序接受一个可选参数和两个必填参数。其中 <image1> 和 <image2> 指定了图像的文件路径。

4.2 实现脸部特征交换

为了让整个程序逻辑更加清晰,我首先将程序的主体部分代码展现出来,这里用到了很多自定义函数,这些都将在后边一一实现,并且这里还定义了一系列的常量和初始化工作。

程序中使用到的 shape_predictor_68_face_landmarks.dat 是 dlib 官方提供的模型数据,有了这个模型之后我们就不需要自己再耗费时间去训练模型,直接拿来使用即可。

下载方式:

wget https://labfile.oss.aliyuncs.com/courses/686/shape_predictor_68_face_landmarks.dat
...
# 加载训练模型
PREDICTOR_PATH = "shape_predictor_68_face_landmarks.dat"
# 图像放缩因子
SCALE_FACTOR = 1 
FEATHER_AMOUNT = 11


FACE_POINTS = list(range(17, 68))  # 脸
MOUTH_POINTS = list(range(48, 61))  # 嘴巴
RIGHT_BROW_POINTS = list(range(17, 22))  # 右眉毛
LEFT_BROW_POINTS = list(range(22, 27))  # 左眉毛
RIGHT_EYE_POINTS = list(range(36, 42))  # 右眼睛
LEFT_EYE_POINTS = list(range(42, 48))  # 左眼睛
NOSE_POINTS = list(range(27, 35))  # 鼻子
JAW_POINTS = list(range(0, 17))  # 下巴

# 选取了左右眉毛、眼睛、鼻子和嘴巴位置的特征点索引
ALIGN_POINTS = (LEFT_BROW_POINTS + RIGHT_EYE_POINTS + LEFT_EYE_POINTS +
                               RIGHT_BROW_POINTS + NOSE_POINTS + MOUTH_POINTS)

# 选取用于叠加在第一张脸上的第二张脸的面部特征
# 特征点包括左右眼、眉毛、鼻子和嘴巴
OVERLAY_POINTS = [
    LEFT_EYE_POINTS + RIGHT_EYE_POINTS + LEFT_BROW_POINTS + RIGHT_BROW_POINTS,
    NOSE_POINTS + MOUTH_POINTS,
]

# 定义用于颜色校正的模糊量,作为瞳孔距离的系数
COLOUR_CORRECT_BLUR_FRAC = 0.6

# 实例化脸部检测器
detector = dlib.get_frontal_face_detector()
# 加载训练模型
# 并实例化特征提取器
predictor = dlib.shape_predictor(PREDICTOR_PATH)

# 定义了两个类处理意外
class TooManyFaces(Exception):
    pass

class NoFaces(Exception):
    pass

...
...

def main():
    arguments = docopt(__doc__, version=__version__)

    # 获取图像与特征点
    im1, landmarks1 = read_im_and_landmarks(arguments['<image1>'])
    im2, landmarks2 = read_im_and_landmarks(arguments['<image2>'])

    # 选取两组图像特征矩阵中所需要的面部部位
    # 计算转换信息,返回变换矩阵
    M = transformation_from_points(landmarks1[ALIGN_POINTS],
                                    landmarks2[ALIGN_POINTS])
    # 获取 im2 的面部掩码
    mask = get_face_mask(im2, landmarks2)
    # 将 im2 的掩码进行变化,使之与 im1 相符
    warped_mask = warp_im(mask, M, im1.shape)
    # 将二者的掩码进行连通
    combined_mask = np.max([get_face_mask(im1, landmarks1), warped_mask],
                                  axis=0)
    # 将第二幅图像调整到与第一幅图像相符
    warped_im2 = warp_im(im2, M, im1.shape)
    # 将 im2 的皮肤颜色进行修正,使其和 im1 的颜色尽量协调
    warped_corrected_im2 = correct_colours(im1, warped_im2, landmarks1)

    # 组合图像,获得结果
    output_im = im1 * (1.0 - combined_mask) + warped_corrected_im2 * combined_mask

    # 保存图像
    cv2.imwrite('output.jpg', output_im)
...
4.2.1 实现 read_im_and_landmarks()
...
# 获取特征点
def get_landmarks(im):
    # detector 是特征检测器
    # predictor 是特征提取器
    rects = detector(im, 1)

    # 如果检测到多张脸
    if len(rects) > 1:
        raise TooManyFaces
    # 如果没有检测到脸
    if len(rects) == 0:
        raise NoFaces
    # 返回一个 n*2 维的矩阵,该矩阵由检测到的脸部特征点坐标组成
    return np.matrix([[p.x, p.y] for p in predictor(im, rects[0]).parts()])

# 读取图片文件并获取特征点
def read_im_and_landmarks(fname):
    # 以 RGB 模式读取图像
    im = cv2.imread(fname, cv2.IMREAD_COLOR)
    # 对图像进行适当的缩放
    im = cv2.resize(im, (im.shape[1] * SCALE_FACTOR,
                         im.shape[0] * SCALE_FACTOR))
    s = get_landmarks(im)

    return im, s
...
4.2.2 实现 transformation_from_points()
# 计算转换信息,返回矩阵
def transformation_from_points(points1, points2):
    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)

    R = (U * Vt).T

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

这里的变换矩阵是根据以下公式计算出来的。

\sum_{i=1}^{68}\left \| sRp_{i}^{T}+T-q_{i}^{T} \right \|^{2}i=168sRpiT+TqiT2

建议直接复制使用,如果对原理感兴趣的同学不妨参考
https://en.wikipedia.org/wiki/Procrustes_analysis#Ordinary_Procrustes_analysis

4.2.3 get_face_mask
...
# 绘制凸多边形
def draw_convex_hull(im, points, color):
    points = cv2.convexHull(points)
    cv2.fillConvexPoly(im, points, color=color)

# 获取面部的掩码
def get_face_mask(im, landmarks):
    im = np.zeros(im.shape[:2], dtype=np.float64)

    for group in OVERLAY_POINTS:
        draw_convex_hull(im,
                         landmarks[group],
                         color=1)

    im = np.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
...

get_face_mask 用于获取面部特征部分(眉毛、眼睛、鼻子以及嘴巴)的图像掩码。效果如下:

此处输入图片的描述

图像掩码作用于原图之后,原图中对应掩码部分为白色的部分才能显示出来,黑色的部分则不予显示,因此通过图像掩码我们就能实现对图像“裁剪”。

4.2.4 warp_im

由 get_face_mask 获得的图像掩码还不能直接使用,因为一般来讲用户提供的两张图像的分辨率大小很可能不一样,而且即便分辨率一样,图像中的人脸由于拍摄角度和距离等原因也会呈现出不同的大小以及角度,所以如果不能只是简单地把第二个人的面部特征抠下来直接放在第一个人脸上,我们还需要根据两者计算所得的面部特征区域进行匹配变换,使得二者的面部特征尽可能重合。

...
# 变换图像
def warp_im(im, M, dshape):
    output_im = np.zeros(dshape, dtype=im.dtype)
    # 仿射函数,能对图像进行几何变换
    # 三个主要参数,第一个输入图像,第二个变换矩阵 np.float32 类型,第三个变换之后图像的宽高
    cv2.warpAffine(im,
                   M[:2],
                   (dshape[1], dshape[0]),
                   dst=output_im,
                   borderMode=cv2.BORDER_TRANSPARENT,
                   flags=cv2.WARP_INVERSE_MAP)
    return output_im
...

效果如下:

此处输入图片的描述

此处输入图片的描述

4.2.5 correct_colours()

最后我们还需要修改皮肤颜色,使两张图片在拼接时候显得更加自然。

# 修正颜色
def correct_colours(im1, im2, landmarks1):
    blur_amount = COLOUR_CORRECT_BLUR_FRAC * np.linalg.norm(
                              np.mean(landmarks1[LEFT_EYE_POINTS], axis=0) -
                              np.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)

    # 避免出现 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))

五、运行

运行方式:

$ python3 face_swap.py <image1> <image2>

另外本次课程的实验源码与图片可以通过 wget 命令从以下地址下载:

http://labfile.oss.aliyuncs.com/courses/686/dt.jpg
http://labfile.oss.aliyuncs.com/courses/686/hc.jpg
http://labfile.oss.aliyuncs.com/courses/686/faceswap.py
https://labfile.oss.aliyuncs.com/courses/686/shape_predictor_68_face_landmarks.dat

六、总结

通过本次课程我们接触到了 docopt, dlib 与 OpenCV 的使用,并实现了人脸特征交换程序。

七、参考

  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值