换脸系列——眼鼻口替换

前言

想着整理一下换脸相关的技术方法,免得以后忘记了,最近脑袋越来越不好使了。应该会包含三个系列: 仅换眼口鼻;换整个面部;3D换脸

先看看2D换脸吧,网上已经有现成的教程了,这里拿过来整理一下,做个记录。

国际惯例,参考博客:

Switching Eds: Face swapping with Python, dlib, and OpenCV

opencv人脸关键点检测模型

这里面一般涉及到脸与脸之间的映射变换矩阵,这里记录一个opencv中的函数findHomography,用于找到多个二维点之间的最优变换矩阵。

流程

整个换脸包含四步:

  • 检测人脸关键点
  • 旋转、缩放、平移第二张图片,使其与第一张图片的人脸对齐
  • 调整第二张图片的色彩平衡,使其与第一张图片对应
  • 将两张图片融合

先加载一些必要的库

import cv2
import numpy as np
import matplotlib.pyplot as plt

检测人脸关键点

使用dlib库检测人脸关键点,不过由于dlib的安装貌似必须有cmake和c++编译器,个人电脑暂时没有,所以使用opencv的人脸关键点检测算法

先去这里下载人脸框检测模型haarcascade_frontalface_alt2.xml这里下载人脸关键点检测模型LBF.model,然后预加载模型:

cas = cv2.CascadeClassifier('./model/haarcascade_frontalface_alt2.xml')
obj = cv2.face.createFacemarkLBF()
obj.loadModel('./model/lbfmodel.yaml')

检测人脸关键点:

def detect_facepoint(img):
    img_gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
    faces = cas.detectMultiScale(img_gray,2,3,0,(30,30))
    landmarks = obj.fit(img_gray,faces)
    assert landmarks[0],'no face detected'
    if(len(landmarks[1])>1):
        print('multi face detected,use the first')
    return faces[0],np.squeeze(landmarks[1][0])

画人脸关键点

def draw_kps(img,face_box,kps):
    img_show = img.copy()
    cv2.rectangle(img_show,(face_box[0],face_box[1]),(face_box[0]+face_box[2],face_box[1]+face_box[3]),(0,255,0),3)
    for i in range(kps.shape[0]):
        cv2.circle(img_show,(kps[i,0],kps[i,1]),2,(0,255,0),-1)
    img_show = cv2.cvtColor(img_show,cv2.COLOR_BGR2RGB)
    return img_show

测试一波

在这里插入图片描述

人脸对齐

有关键点以后,需要将两个关键点的大小形状变换地尽可能接近才能进行换脸,主要涉及到旋转、平移、缩放。假设定义如下变量:旋转R、平移T、缩放S,第一个人脸关键点是qi(i=168)q_i(i=1\cdots 68),第二个人脸关键点是pi(i=168)p_i(i=1\cdots 68),那么我们需要最小化
i=168sRpiT+TqiT2 \sum_{i=1}^{68} \parallel sRp_i^T+T-q_i^T \parallel ^2
也就是关键点pp经过旋转、缩放、平移以后,要和关键点qq相近

以下对齐顺序是将第二个图像与第一个图像对齐

方法1:SVD分解+仿射变换(推荐)

这里原文提到了一个Ordinary Procrustes Analysis的算法,专门用于提取这个旋转矩阵的,用的SVD分解方法:

def transformation_from_points(points1, points2):
    points1 = points1.copy()
    points2 = points2.copy()
    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(np.dot(points1.T , points2))
    R = (np.dot(U , Vt)).T 
    return np.vstack([np.hstack(((s2 / s1) * R,
                                       np.array([c2.T - np.dot((s2 / s1) * R , c1.T)]).T )),
                         np.array([0., 0., 1.])])

得到变换矩阵以后,就可以使用opencvwarpAffine进行仿射变换

def wrap_im(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 align_img1(img1,img2,landmarks1,landmarks2):
    trans_mat = transformation_from_points(landmarks1, landmarks2)
    img2_align = wrap_im(img2,trans_mat,img1.shape)
    return img2_align

额外加一个比较好的SVD理论证明

方法2:透视变换(不推荐)

这篇文章提到的一个方法,直接用opencvfindHomography找到变换矩阵,再用warpPerspective做透视变换。函数如下:

def align_img2(img1,img2,landmarks1,landmarks2):
    trans_mat,mask = cv2.findHomography(landmarks2, landmarks1, cv2.RANSAC,5.0)
    img2_align = cv2.warpPerspective(img2.copy(),trans_mat,(img1.shape[1],img1.shape[0]))
    return img2_align

对比

分别调用对比看看

img2_align = align_img1(img1,img2,face_kps1,face_kps2)
plt.figure(figsize=[8,8])
plt.subplot(131)
plt.imshow(cv2.cvtColor(img1.copy(),cv2.COLOR_BGR2RGB))
plt.subplot(132)
plt.imshow(cv2.cvtColor(img2_align.copy(),cv2.COLOR_BGR2RGB))
img2_align2 = align_img2(img1,img2,face_kps1,face_kps2)
plt.subplot(133)
plt.imshow(cv2.cvtColor(img2_align2.copy(),cv2.COLOR_BGR2RGB))

在这里插入图片描述

这么一看,咱们还是用方法1的仿射变换吧,咳咳。透视变换功能过于强大。

获取被替换部分的掩膜

按照参考博客和本博客的目的,只替换眼、鼻、口

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 = 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

提取完mask,当然需要将mask也按照仿射变换的方法进行校正

mask = get_face_mask(img2, face_kps2)
warped_mask = align_img1(img1, mask,face_kps1,face_kps2)
combined_mask = np.max([get_face_mask(img1, face_kps1), warped_mask],
                          axis=0)

可视化

plt.imshow(np.multiply(img2_align,combined_mask).astype('uint8')[...,::-1])

在这里插入图片描述

颜色校正

直接贴图,肯定会由于颖宝和老干部的肤色不搭,会有非常明显的贴图边缘痕迹。

output_im = img1 * (1.0 - combined_mask) + img2_align * combined_mask

在这里插入图片描述

很明显,mask的边缘部分,有非常明显的颜色差

所以需要矫正一波颜色,在后续博客中,会提供其它颜色校正方法。

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 * 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)
    # Avoid divide-by-zero errors.
    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))

矫正完毕以后:

img2_correct = correct_colours(img1,img2_align,face_kps1)
plt.imshow(cv2.cvtColor(img2_correct.copy().astype('uint8'),cv2.COLOR_BGR2RGB))
plt.axis('off')

在这里插入图片描述

再来瞅瞅结果:

output_im = img1 * (1.0 - combined_mask) + img2_correct * combined_mask

在这里插入图片描述

好了,边缘基本没有贴图痕迹了,但是整体效果。。。。。。。。

打扰了~~

后面用其它算法优化

后记

简单的记录一下只替换五官的情况下,整个换脸算法的流程。

博客代码:

链接:https://pan.baidu.com/s/1a-TNuIVslJiOTWgKse58Ag
提取码:kabv

本文已经同步到微信公众号中,公众号与本博客将持续同步更新运动捕捉、机器学习、深度学习、计算机视觉算法,敬请关注
在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章