前言
想着整理一下換臉相關的技術方法,免得以後忘記了,最近腦袋越來越不好使了。應該會包含三個系列: 僅換眼口鼻;換整個面部;3D換臉
先看看2D換臉吧,網上已經有現成的教程了,這裏拿過來整理一下,做個記錄。
國際慣例,參考博客:
Switching Eds: Face swapping with Python, dlib, and 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
,第一個人臉關鍵點是,第二個人臉關鍵點是,那麼我們需要最小化
也就是關鍵點經過旋轉、縮放、平移以後,要和關鍵點相近
以下對齊順序是將第二個圖像與第一個圖像對齊
方法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.])])
得到變換矩陣以後,就可以使用opencv
的warpAffine
進行仿射變換
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:透視變換(不推薦)
這篇文章提到的一個方法,直接用opencv
的findHomography
找到變換矩陣,再用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
本文已經同步到微信公衆號中,公衆號與本博客將持續同步更新運動捕捉、機器學習、深度學習、計算機視覺算法,敬請關注