前言
這次再用dlib來做一個很酷的應用:換臉。在百度可以搜出一大堆轉載的,里面雖然講的不是很詳細(數學部分 ),個人感覺大多數人對于奇異值分解 、仿射變換矩陣 怎么實現根本不敢興趣,只想上代碼實現功能,所以后面就省去了數學的那部分。 一篇文章的鏈接:教你用200行Python代碼“換臉” 代碼的github鏈接:https://github.com/matthewearl/faceswap/blob/master/faceswap.py 我很大程度上都是參考其中的代碼,但是有些部分會不太一樣。完整工程下載鏈接在文章的最后。
功能實現
第一步,我們要把零散的功能全部實現,后面驗證都正確后,再把這些“零件”拼在一起。
導入包和定義路徑
import cv2
import dlib
import os
import numpy
as np
import globcurrent_path = os.getcwd()
predictor_68_points_path = current_path +
'/model/shape_predictor_68_face_landmarks.dat'
predictor_5_points_path = current_path +
'/model/shape_predictor_5_face_landmarks.dat'
predictor_path = predictor_68_points_path
face_path = current_path +
'/faces/'
導入包,然后獲取當前路徑。指定要用到的模型文件和測試圖片的路徑。這里的模型文件有兩個,一個是人臉的68個特征點的檢測器(shape_predictor_68_face_landmarks.dat),一個是5個特征點的檢測器(shape_predictor_5_face_landmarks.dat)。自行選擇即可。
獲取特征點
程序實現
detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor(predictor_path)
class TooManyFaces (Exception) :pass class NoFace (Exception) :pass def get_landmark (image) :face_rect = detector(image,
1 )
if len(face_rect) >
1 :print(
'Too many faces.We only need no more than one face.' )
raise TooManyFaces
elif len(face_rect) ==
0 :print(
'No face.We need at least one face.' )
raise NoFace
else :print(
'left {0}; top {1}; right {2}; bottom {3}' .format(face_rect[
0 ].left(), face_rect[
0 ].top(), face_rect[
0 ].right(), face_rect[
0 ].bottom()))
return np.matrix([[p.x, p.y]
for p
in predictor(image, face_rect[
0 ]).parts()])
獲取人臉的特征點的這些套路不愿意再重復介紹了,前面的博客都有講。偷個懶,直接上鏈接:python dlib學習(二):人臉特征點標定。 有兩個異常類:NoFace和TooManyFaces,分別對應沒有檢測到人臉和檢測到超過一個人的臉。我們只是實現簡單的換臉,只需要圖片中有一張臉就足夠了,如果不符合情況就拋出異常。 還有一點,由于后面涉及矩陣計算,為了加快計算,把得到的這些特征點轉換成numpy矩陣。
編寫測試函數
接下來測試一下這段程序,編寫一段測試程序。比較簡單,直接貼代碼了:
def test_get_landmark () :for img_path
in glob.glob(os.path.join(face_path,
"*.jpg" )):print(
"Processing file: {}" .format(img_path))img = cv2.imread(img_path, cv2.IMREAD_COLOR)img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)landmarks = get_landmark(img)
print landmarks
運行程序,然后會讀取前面指定的faces文件夾中的所有圖片: 如果圖片只有一張圖片,那么會正常打印出如下信息(人臉位置、所有檢測到的特征點等等): 我故意放了一張有多個人臉的圖片,那么程序會拋出異常直接終止: 測試通過后,再往下寫程序。
使用普氏分析(Procrustes analysis)調整臉部
程序實現
因為圖片中的人臉可能會有一定的傾斜,而且不同圖片中人臉的位置也不一樣。所以,我們需要把人臉的位置進行調整。
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 -= c1points2 -= c2s1 = np.std(points1)s2 = np.std(points2)points1 /= s1points2 /= s2U, 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. ])])
這部分會比較難懂,下面是我直接從前面那篇文章摘出的相關描述。 代碼分別實現了下面幾步:
將輸入矩陣轉換為浮點數。這是之后步驟的必要條件。 每一個點集減去它的矩心。一旦為這兩個新的點集找到了一個最佳的縮放和旋轉方法,這兩個矩心c1和c2就可以用來找到完整的解決方案。 同樣,每一個點集除以它的標準偏差。這消除了問題的組件縮放偏差。 使用Singular Value Decomposition計算旋轉部分。可以在維基百科上看到關于解決正交普氏問題的細節(https://en.wikipedia.org/wiki/Orthogonal_Procrustes_problem)。 利用仿射變換矩陣(https://en.wikipedia.org/wiki/Transformation_matrix#Affine_transformations)返回完整的轉化。
實質上最后就是得到了一個轉換矩陣,第一幅圖片中的人臉可以通過這個轉換矩陣映射到第二幅圖片中,與第二幅圖片中的人臉對應。(吐槽:奇異值分解直接上numpy解出來,真是省了不少事啊) 得到了轉換矩陣后,就可以使用它進行映射了吧:
def wrap_image (image, M, dshape) :output_image = np.zeros(dshape, dtype=image.dtype)cv2.warpAffine(image, M[:
2 ], (dshape[
1 ], dshape[
0 ]), dst=output_image, flags=cv2.WARP_INVERSE_MAP, borderMode=cv2.BORDER_TRANSPARENT)
return output_image
這里也是使用了opencv的warpAffine函數,自己從底層實現會比較復雜。python代碼也得以精簡。
測試函數
def test_warp_image () :jobs_image_path = os.path.join(face_path,
"jobs2.jpg" )obama_image_path = os.path.join(face_path,
"obama.jpg" )jobs_img = cv2.imread(jobs_image_path, cv2.IMREAD_COLOR)cv2.imshow(
"jobs_img" , jobs_img)obama_img = cv2.imread(obama_image_path, cv2.IMREAD_COLOR)cv2.imshow(
"obama_img" , obama_img)jobs_landmark = get_landmark(jobs_img)obama_landmark = get_landmark(obama_img)transformation_matrix = transformation_from_points(jobs_landmark, obama_landmark)print(
'warpping images...' )output_image = warp_image(obama_img, transformation_matrix, dshape=jobs_img.shape)print(
'showing the results!' ) print(
'Please press any button to continue.' )cv2.namedWindow(
"output_image" , cv2.WINDOW_AUTOSIZE)cv2.imshow(
"output_image" , output_image)cv2.waitKey(
0 )cv2.destroyAllWindows()
直接看結果,很直觀:
獲取人臉掩模
我們已經得到了對齊后的人臉圖片,那么接下來的目標就是得到人臉的位置。我們可以使用一個掩模(mask)來表示,屬于人臉的區域像素值為1,不屬于人臉的區域像素值為0。在提取時我們直接將原圖片乘以掩模,就可以得到人臉,而其余區域像素值為0;如果將原圖片乘以1 ? m a s k ,即人臉區域會是0,其余區域會保留下來。上面這兩個結果相加,既可以實現初步的換臉。
程序實現
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 (img, points, color) :points = cv2.convexHull(points)cv2.fillConvexPoly(img, points, color)
def get_face_mask (img, landmarks) :img = np.zeros(img.shape[:
2 ], dtype=np.float64)
for group
in OVERLAY_POINTS:draw_convex_hull(img, landmarks[group], color=
1 )img = np.array([img, img, img]).transpose((
1 ,
2 ,
0 )) img = (cv2.GaussianBlur(img, (FEATHER_AMOUNT, FEATHER_AMOUNT),
0 ) >
0 ) *
1.0 img = cv2.GaussianBlur(img, (FEATHER_AMOUNT, FEATHER_AMOUNT),
0 )
return img
測試函數
def test_face_mask () :jobs_image_path = os.path.join(face_path,
"jobs.jpg" )obama_image_path = os.path.join(face_path,
"obama.jpg" )jobs_img = cv2.imread(jobs_image_path, cv2.IMREAD_COLOR)obama_img = cv2.imread(obama_image_path, cv2.IMREAD_COLOR)jobs_landmark = get_landmark(jobs_img)obama_landmark = get_landmark(obama_img)jobs_mask = get_face_mask(jobs_img, jobs_landmark)obama_mask = get_face_mask(obama_img, obama_landmark)cv2.imshow(
"jobs_img" , jobs_img)cv2.imshow(
"obama_img" , obama_img)cv2.imshow(
"jobs_mask" , jobs_mask)cv2.imshow(
"obama_mask" , obama_mask)cv2.waitKey(
0 )cv2.destroyAllWindows()
運行結果,很直觀:
顏色校正
有了前面的函數,其實我們已經實現了換臉的大部分功能了。人臉已經對齊了,我們有人臉的特征點,可以進行凸包檢測來得到人臉的區域,然后把第二幅圖凸包中的位置摳出來放到第一幅圖片中,就可以了。但是,這樣得到的結果是十分難看的,因為背景光照或者膚色等等因素的影響,看起來會十分不自然。
程序實現
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 )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))
實現思路就是利用高斯模糊來幫助我們校正顏色,使用im2除以im2的高斯模糊,乘以im1來校正顏色。總體來說,這個方法比較粗糙和暴力,很多因素都忽略了。結果也只能從一定程度上獲得提高,有時反而會被修正的更“差”。因為有很多的影響因素,選取到一個合適的高斯核的大小,才可能取得比較理想的結果。如果太小,第一個圖像的面部特征將顯示在第二個圖像中。過大,內核之外區域像素被覆蓋,并發生變色。這里的內核用了一個0.6 *的瞳孔距離。
測試函數
這里的測試函數會用到前面所有的程序,并把過程中生成的圖片全部打印出來。 注:在進行一系列的操作后,我們程序中的圖像的灰度不是初始的0-255或是0-1了,我們要顯示這些圖片必須要先進行歸一化。我在程序中調用了opencv中的cv2.normalize()函數實現。
def test_all () :jobs_image_path = os.path.join(face_path,
"jobs.jpg" )obama_image_path = os.path.join(face_path,
"obama.jpg" )jobs_img = cv2.imread(jobs_image_path, cv2.IMREAD_COLOR)jobs_img = cv2.resize(jobs_img, (jobs_img.shape[
1 ] * SCALE_FACTOR,jobs_img.shape[
0 ] * SCALE_FACTOR))cv2.imshow(
"1" , jobs_img)cv2.waitKey(
0 )img1 = jobs_imgobama_img = cv2.imread(obama_image_path, cv2.IMREAD_COLOR)obama_img = cv2.resize(obama_img, (obama_img.shape[
1 ] * SCALE_FACTOR,obama_img.shape[
0 ] * SCALE_FACTOR))cv2.imshow(
"2" , obama_img)cv2.waitKey(
0 )img2 = obama_imglandmark1 = get_landmark(img1)landmark2 = get_landmark(img2)transformation_matrix = transformation_from_points(landmark1[ALIGN_POINTS], landmark2[ALIGN_POINTS])mask = get_face_mask(img2, landmark2)cv2.imshow(
"3" , mask)cv2.waitKey(
0 )warped_mask = warp_image(mask, transformation_matrix, img1.shape)cv2.imshow(
"4" , warped_mask)cv2.waitKey(
0 )combined_mask = np.max([get_face_mask(img1, landmark1), warped_mask], axis=
0 )cv2.imshow(
"5" , combined_mask)cv2.waitKey(
0 )warped_img2 = warp_image(img2, transformation_matrix, img1.shape)cv2.imshow(
"6" , warped_img2)cv2.waitKey(
0 )warped_corrected_img2 = correct_colours(img1, warped_img2, landmark1)warped_corrected_img2_temp = np.zeros(warped_corrected_img2.shape, dtype=warped_corrected_img2.dtype)cv2.normalize(warped_corrected_img2, warped_corrected_img2_temp,
0 ,
1 , cv2.NORM_MINMAX)cv2.imshow(
"7" , warped_corrected_img2_temp)cv2.waitKey(
0 )output = img1 * (
1.0 - combined_mask) + warped_corrected_img2 * combined_maskcv2.normalize(output, output,
0 ,
1 , cv2.NORM_MINMAX)cv2.imshow(
"8" , output.astype(output.dtype))cv2.waitKey(
0 )cv2.destroyAllWindows()
運行程序后,每次按任意鍵就會顯示下一步的結果,下面是截圖。
原圖片1:
原圖片2:
提取圖片2掩模:
將圖片2掩模映射到圖片1:
將映射后的掩模與圖片1的原始掩模融合:
將圖片2映射到圖片1:
顏色校正:
使用前面的掩模合成圖片:
封裝
功能實現了,但是都是函數形式,也不利于調用,可擴展性也比較差。我把前面的程序整合了一下封裝成了類,可以更容易地使用。 文件名:FaceChanger.py
import cv2
import dlib
import os
import numpy
as np
import glob
class TooManyFaces (Exception) :pass class NoFace (Exception) :pass class FaceChanger (object) :def __init__ (self, which_predictor='68' ) :print(
'Starting your FaceChanger...' )self.current_path = os.getcwd()print(
'Current path:{0}' .format(self.current_path))predictor_68_points_path = self.current_path +
'/model/shape_predictor_68_face_landmarks.dat' predictor_5_points_path = self.current_path +
'/model/shape_predictor_5_face_landmarks.dat' if which_predictor ==
'68' :predictor_name =
'shape_predictor_68_face_landmarks.dat' self.predictor_path = predictor_68_points_path
elif which_predictor ==
'5' :predictor_name =
'shape_predictor_5_face_landmarks.dat' self.predictor_path = predictor_5_points_path
else :predictor_name =
'shape_predictor_68_face_landmarks.dat' self.predictor_path = predictor_68_points_pathprint(
'Your predictor is:{0}' .format(predictor_name))print(
'Searching for faces...' )self.face_path = self.current_path +
'/faces/' self.face_list = glob.glob(os.path.join(self.face_path,
'*.jpg' ))print(
'{0} faces have been found.' .format(len(self.face_list)))print(
'Here are the names of those pictures:' )name = self.face_list[
0 ].strip().split(
'/' )[-
1 ]
for face_file
in self.face_list[
1 :]:name +=
' ' + face_file.strip().split(
'/' )[-
1 ]print(
'%s' %(name))print(
'You can choose two of theses pictures, and change the face between them.' )self.SCALE_FACTOR =
1 self.FEATHER_AMOUNT =
11 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 self.detector = dlib.get_frontal_face_detector()self.predictor = dlib.shape_predictor(self.predictor_path)self.image1 =
None self.image2 =
None self.landmarks1 =
None self.landmarks2 =
None def load_images (self, image1_name, image2_name) :assert image1_name.strip().split(
'.' )[-
1 ] ==
'jpg' assert image2_name.strip().split(
'.' )[-
1 ] ==
'jpg' image1_path = os.path.join(self.face_path, image1_name)image2_path = os.path.join(self.face_path, image2_name)self.image1 = cv2.imread(image1_path, cv2.IMREAD_COLOR)self.image2 = cv2.imread(image2_path, cv2.IMREAD_COLOR)self.landmarks1 = self.get_landmark(self.image1)self.landmarks2 = self.get_landmark(self.image2)
def run (self, showProcedure=False, saveResult=True) :if self.image1
is None or self.image2
is None :print(
'You need to load two images first.' )
return if showProcedure ==
True :print(
'Showing the procedure.Press any key to continue your process.' )cv2.imshow(
"1" , self.image1)cv2.waitKey(
0 )cv2.imshow(
"2" , self.image2)cv2.waitKey(
0 )M = self.transformation_from_points(\self.landmarks1[self.ALIGN_POINTS], self.landmarks2[self.ALIGN_POINTS])mask = self.get_face_mask(self.image2, self.landmarks2)
if showProcedure ==
True :cv2.imshow(
"3" , mask)cv2.waitKey(
0 )warped_mask = self.warp_image(mask, M, self.image1.shape)
if showProcedure ==
True :cv2.imshow(
"4" , warped_mask)cv2.waitKey(
0 )combined_mask = np.max([self.get_face_mask(self.image1, self.landmarks1), \warped_mask], axis=
0 )
if showProcedure ==
True :cv2.imshow(
"5" , combined_mask)cv2.waitKey(
0 )warped_img2 = self.warp_image(self.image2, M, self.image1.shape)
if showProcedure ==
True :cv2.imshow(
"6" , warped_img2)cv2.waitKey(
0 )warped_corrected_img2 = self.correct_colours(self.image1, warped_img2, self.landmarks1)warped_corrected_img2_temp = np.zeros(warped_corrected_img2.shape, dtype=warped_corrected_img2.dtype)cv2.normalize(warped_corrected_img2, warped_corrected_img2_temp,
0 ,
1 , cv2.NORM_MINMAX)
if showProcedure ==
True :cv2.imshow(
"7" , warped_corrected_img2_temp)cv2.waitKey(
0 )output = self.image1 * (
1.0 - combined_mask) + warped_corrected_img2 * combined_maskoutput_show = np.zeros(output.shape, dtype=output.dtype)cv2.normalize(output, output_show,
0 ,
1 , cv2.NORM_MINMAX)cv2.normalize(output, output,
0 ,
255 , cv2.NORM_MINMAX)
if showProcedure ==
True :cv2.imshow(
"8" , output_show.astype(output_show.dtype))cv2.waitKey(
0 )cv2.destroyAllWindows()
if saveResult
is True :cv2.imwrite(
"output.jpg" , output)
def get_landmark (self, image) :face_rect = self.detector(image,
1 )
if len(face_rect) >
1 :print(
'Too many faces.We only need no more than one face.' )
raise TooManyFaces
elif len(face_rect) ==
0 :print(
'No face.We need at least one face.' )
raise NoFace
else :print(
'left {0}; top {1}; right {2}; bottom {3}' .format(face_rect[
0 ].left(), face_rect[
0 ].top(), face_rect[
0 ].right(), face_rect[
0 ].bottom()))
return np.matrix([[p.x, p.y]
for p
in self.predictor(image, face_rect[
0 ]).parts()])
def transformation_from_points (self, 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 -= c1points2 -= c2s1 = np.std(points1)s2 = np.std(points2)points1 /= s1points2 /= s2U, 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. ])])
def warp_image (self, image, M, dshape) :output_image = np.zeros(dshape, dtype=image.dtype)cv2.warpAffine(image, M[:
2 ], (dshape[
1 ], dshape[
0 ]), dst=output_image, flags=cv2.WARP_INVERSE_MAP, borderMode=cv2.BORDER_TRANSPARENT)
return output_image
def correct_colours (self, im1, im2, landmarks1) :blur_amount = self.COLOUR_CORRECT_BLUR_FRAC * np.linalg.norm(np.mean(landmarks1[self.LEFT_EYE_POINTS], axis=
0 ) -np.mean(landmarks1[self.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 )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 draw_convex_hull (self, img, points, color) :points = cv2.convexHull(points)cv2.fillConvexPoly(img, points, color)
def get_face_mask (self, img, landmarks) :img = np.zeros(img.shape[:
2 ], dtype=np.float64)
for group
in self.OVERLAY_POINTS:self.draw_convex_hull(img, landmarks[group], color=
1 )img = np.array([img, img, img]).transpose((
1 ,
2 ,
0 )) img = (cv2.GaussianBlur(img, (self.FEATHER_AMOUNT, self.FEATHER_AMOUNT),
0 ) >
0 ) *
1.0 img = cv2.GaussianBlur(img, (self.FEATHER_AMOUNT, self.FEATHER_AMOUNT),
0 )
return img
這段代碼就可以直接使用了。 調用方法示例如下:
from FaceChanger
import *
fc = FaceChanger()
fc.load_images(
'ibrahimovic.jpg' ,
'pique.jpg' )
fc.run(showProcedure=
True )
創建類的實例; 導入兩張圖片; 運行即可(可以選擇是否顯示過程中的圖片);
如果選擇顯示圖片,會顯示如下結果。圖片8是合成的圖片。最后還會在當前文件夾保存生成的圖片。
完整工程下載鏈接:http://download.csdn.net/download/hongbin_xu/10170440。
總結
以上是生活随笔 為你收集整理的python dlib学习(十):换脸 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。