換臉系列——眼鼻口替換

前言

想着整理一下換臉相關的技術方法,免得以後忘記了,最近腦袋越來越不好使了。應該會包含三個系列: 僅換眼口鼻;換整個面部;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

本文已經同步到微信公衆號中,公衆號與本博客將持續同步更新運動捕捉、機器學習、深度學習、計算機視覺算法,敬請關注
在這裏插入圖片描述

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