【AI美顏演算法】300行Python實現基於人臉特徵的美顏演算法

先上效果圖:

GitHub地址:QuantumLiu/AIMakeup

預編譯版本下載:pan.baidu.com/s/1geA2gt

AI美顏

人類一直是一個看臉的物種,人人都希望可以變得更美是無可爭議的,而美顏類應用的出現拯救了所有人,從此人類進入了美(照)顏(騙)時代。

。。。。

每次寫技術blog都要寫一堆廢話引言,現在懶得寫,大概就是美顏很重要,美女主播靠它活,沒了它大部分妹子不敢發朋友圈blabla。

美顏演算法的基礎是人臉識別技術,市面上的美顏應用普遍使用了CV科技類公司的人臉識別(特徵點提取)介面:

MeituKiss超級自拍神器手機,前置攝像頭搭載自動美顏技術。Face++為該款手機提供領先的人臉檢測和關鍵點檢測技術,實現實時美顏,自動美肌,智能美型,極致美瞳。

美顏相機是一款專為愛自拍的女生量身定製的美圖軟體。Face++為美顏相機提供領先的人臉檢測和關鍵點檢測技術,在圖像中精準定位人臉和五官位置,讓多款細膩的人像特效瞬間呈現,讓用戶快速擁有驚艷的面容。

也有小米新機所搭載的AI美顏技術:

需要人臉識別技術的原因是顯而易見的,每張圖片面部所佔區域不同,五官位置各異,我們針對不同區域要做的美化操作也各不相同。我們需要人臉識別技術為我們定位面部並標記面部不同的區域。如果直接對整張圖片進行暴力操作,效果會非常難看~

人臉識別(特徵提取)技術目前已經較為成熟,曠視、face++提供的介面可以精確提取100個以上的特徵點,根據這些特徵點,我們可以針對不同部位進行美化:皮膚磨皮、美白,嘴唇增紅、眼睛提亮、瘦臉等等。

在本項目中,使用了開源庫dlib C++ Library來提取面部特徵。這個庫我在另外兩篇人臉識別文章【換臉系列1】軍裝照刷爆朋友圈?教你用Python+深度學習自製換臉軟體!(改進)和【換臉系列2】浪漫七夕?和你的TA交♂換身體吧!(單身狗慎入)有過使用和介紹。這是一個優秀的cv&dl&人臉識別庫,提供的預訓練模型可以提取68個特徵點,精確度較高。

實現

本章代碼只起演示作用,不完整不保證運行,完整代碼請看GitHub

對象分析

美顏演算法的處理對象是人像圖片(廢話),我們分析一下一張人像圖片內的對象:

圖片里有若干人臉區域和非人臉區域,我們只希望處理那些人臉區域。

每個人臉,由五官、臉頰、下巴、額頭等部件組成。(奇怪的名字,逃

每個部件都有自己的特點,美顏演算法需要針對不同的部件進行具體的美化操作,即美顏操作的最小對象是部件。

現在,我們知道了一張圖片有兩種主要對象:臉、部件,若干部件構成一張臉。我們希望,每個部件對象都有若干方法來美化自身。

我們發現,臉和部件對象都需要坐標點來實例化,而且臉部整體也需要若干方法來美化自身。所以我們可以讓臉對象繼承自部件對象。

class Organ(): passclass Forehead(Organ): pass

勾畫有效區域

我們首先要知道每個臉和部件的位置(坐標),我們用dlib來檢測並提取特徵點坐標。

PREDICTOR_PATH = "/home/matt/dlib-18.16/shape_predictor_68_face_landmarks.dat"detector = dlib.get_frontal_face_detector()predictor = dlib.shape_predictor(PREDICTOR_PATH)def get_landmarks(im): rects = detector(im, 1) return [numpy.matrix([[p.x, p.y] for p in predictor(im, rect).parts()]) for rect in rects]

這樣我們就得到了一個列表,每個元素是一個人臉的特徵點坐標數

根據標記點序號,我們可以得到人臉和各個部件的特徵坐標:

#五官名稱self.organs_name=[jaw,mouth,nose,left eye,right eye,left brow,right brow]#五官等標記點self.organs_points=[list(range(0, 17)),list(range(48, 61)),list(range(27, 35)),list(range(42, 48)),list(range(36, 42)),list(range(22, 27)),list(range(17, 22))]

我們怎麼利用這些坐標來針對每個部件的有效區域進行美化處理呢?

首先,獲得特徵點坐標的邊界,再根據區域大小,適當擴大一部分,取對應的儲存圖片數據的numpy數組的切片。這樣我們就得到了全局圖片的一個局部切片(引用)。

def get_rect(self): 獲得定位方框 ys,xs=self.landmark[:,1],self.landmark[:,0] self.top,self.bottom,self.left,self.right=np.min(ys),np.max(ys),np.min(xs),np.max(xs)self.shape=(int(self.bottom-self.top),int(self.right-self.left))self.size=self.shape[0]*self.shape[1]*3self.move=int(np.sqrt(self.size/3)/20)patch=im[np.max([self.top-self.move,0]):np.min([self.bottom+self.move,shape[0]]),np.max([self.left-self.move,0]):np.min([self.right+self.move,shape[1]])]

接下來,我們要根據特徵點在一個和局部切片形狀相同的mask圖層上勾畫部件輪廓。這個mask層只在有效區域值為0~1,其他部分為0.我們使用opencv的convexHull和fillConvexPoly函數。同時,我們不希望遮罩很僵硬,使用要用高斯模糊處理。

def _draw_convex_hull(self,im, points, color): 勾畫多凸邊形 points = cv2.convexHull(points) cv2.fillConvexPoly(im, points, color=color) def get_mask_re(self,ksize=None): 獲得局部相對坐標遮罩 if ksize==None: ksize=self.ksize landmark_re=self.landmark.copy() landmark_re[:,1]-=np.max([self.top-self.move,0]) landmark_re[:,0]-=np.max([self.left-self.move,0]) mask = np.zeros(self.patch_bgr.shape[:2], dtype=np.float64) self._draw_convex_hull(mask, landmark_re, color=1) mask = np.array([mask, mask, mask]).transpose((1, 2, 0)) mask = (cv2.GaussianBlur(mask, ksize, 0) > 0) * 1.0 return cv2.GaussianBlur(mask, ksize, 0)[:]

如圖,這是得到的鼻子和嘴的mask和切片patch

這樣我們就可以逐步得到眼睛、鼻子、嘴巴、眉毛的mask和patch,我們把它們放在全局圖片並相加,就能得到全部五官的有效區域。

特別的,dlib的特徵提取器並不能返回額頭的邊界坐標,所以我們需要自己計算額頭的特徵坐標。

畫額頭

基於小學時培養的繪畫素養,我假設額頭大體是一個中心在眉心附近的,長軸=短軸=臉寬且與雙眼連線平行的一個 [pi,2pi] 的半橢圓。

要畫出這樣一個半橢圓,我們首先要知道臉寬、中心點、長軸偏移角度,然後使用opencv的ellipse函數。

#畫橢圓 radius=(np.linalg.norm(face_landmark[0]-face_landmark[16])/2).astype(int32) center_abs=tuple(((face_landmark[0]+face_landmark[16])/2).astype(int32)) angle=np.degrees(np.arctan((lambda l:l[1]/l[0])(face_landmark[16]-face_landmark[0]))).astype(int32) mask=np.zeros(mask_organs.shape[:2], dtype=np.float64) cv2.ellipse(mask,center_abs,(radius,radius),angle,180,360,1,-1)

然而這個橢圓(半圓)只是一個對腦門部位的粗略估計,我們還需要將其他部件、頭髮、背景等部分剔除出去。

我們根據鼻子的膚色來判定一個區域是否為真正的腦門,最後用convexHull勾畫一個包括所有腦門區域的點的特徵點輪廓

#剔除與五官重合部分 mask[mask_organs[:,:,0]>0]=0 #根據鼻子的膚色判斷真正的額頭面積 index_bool=[] for ch in range(3): mean,std=np.mean(im_bgr[:,:,ch][mask_nose[:,:,ch]>0]),np.std(im_bgr[:,:,ch][mask_nose[:,:,ch]>0]) up,down=mean+0.5*std,mean-0.5*std index_bool.append((im_bgr[:,:,ch]<down)|(im_bgr[:,:,ch]>up)) index_zero=((mask>0)&index_bool[0]&index_bool[1]&index_bool[2]) mask[index_zero]=0 index_abs=np.array(np.where(mask>0)[::-1]).transpose() landmark=cv2.convexHull(index_abs).squeeze() return landmark

再根據求得的坐標實例化一個部件類,去除與其他部件重合的部分,獲得mask、patch。

「臉」對象

現在,我們已經獲得了全部的面部部件的坐標點和各自的遮罩層。於是,可以求得所有部件的集合,以及整個臉部的patch和除了部件之外的部分的mask,來實例化一個「臉對象」。

我們用一個字典organs來儲存部件的集合,鍵值是部件名稱如nose,通過get_mask_abs()方法來獲得部件相當於全局圖片的遮罩。

我們根據所有的坐標點,調用super方法調用父類Organ的實例化函數,再用mask減去其他部件的mask得到純臉部的mask。

mask_organs=(self.organs[mouth].get_mask_abs()+mask_nose+self.organs[left eye].get_mask_abs()+self.organs[right eye].get_mask_abs()+self.organs[left brow].get_mask_abs()+self.organs[right brow].get_mask_abs()) forehead_landmark=self.get_forehead_landmark(im_bgr,landmarks,mask_organs,mask_nose) self.organs[forehead]=Forehead(im_bgr,img_hsv,temp_bgr,temp_hsv,forehead_landmark,mask_organs,forehead) mask_organs+=self.organs[forehead].get_mask_abs() # 人臉的完整標記點 self.FACE_POINTS = np.concatenate([landmarks,forehead_landmark]) super(Face,self).__init__(im_bgr,img_hsv,temp_bgr,temp_hsv,self.FACE_POINTS,face) mask_face=self.get_mask_abs()-mask_organs self.patch_mask=self.get_patch(mask_face)

至此,我們勾畫出來全部有效區域,接下來我們要為這些有效區域添加美化方法。

美化方法

本項目目前實現了提亮美白、增加鮮艷度、磨皮、銳化四種基本的美化方法,Organ類及其子類Face、Forehead都有這些方法,這樣我們就可以根據需要組合使用這些方法,對不同部位進行具體的美化。

提亮美白

我們知道,圖片的顏色空間除了三原色的RGB(BGR)還有HSV和HSL。

HSV即色相、飽和度、明度(英語:Hue, Saturation, Value),又稱HSB,其中B即英語:Brightness。

色相(H)是色彩的基本屬性,就是平常所說的顏色名稱,如紅色、黃色等。

飽和度(S)是指色彩的純度,越高色彩越純,低則逐漸變灰,取0-100%的數值。

明度(V),亮度(L),取0-100%。

相對於BGR,HSV更接近人類的視覺直覺,在本項目中,美白和增加鮮艷度就在hsv空間進行。

hsv空間的圖片,v通道的值代表各像素的亮度,所以我們只需要增加有效區域的v值就可以了

def whitening(self,rate=0.15,confirm=True): 提亮美白 arguments: rate:float,-1~1,new_V=min(255,V*(1+rate)) confirm:wether confirm this option if confirm: self.confirm() self.patch_hsv[:,:,-1]=np.minimum(self.patch_hsv[:,:,-1]+self.patch_hsv[:,:,-1]*self.patch_mask[:,:,-1]*rate,255).astype(uint8) self.im_bgr[:]=cv2.cvtColor(self.im_hsv, cv2.COLOR_HSV2BGR)[:] self.update_temp() else: self.patch_hsv_temp[:]=cv2.cvtColor(self.patch_bgr_temp, cv2.COLOR_BGR2HSV)[:] self.patch_hsv_temp[:,:,-1]=np.minimum(self.patch_hsv_temp[:,:,-1]+self.patch_hsv_temp[:,:,-1]*self.patch_mask[:,:,-1]*rate,255).astype(uint8) self.patch_bgr_temp[:]=cv2.cvtColor(self.patch_hsv_temp, cv2.COLOR_HSV2BGR)[:]

注意到,我們使用

self.patch_hsv_temp[:,:,-1]*self.patch_mask[:,:,-1]*rate

來表示對於有效區域的更改值,在其他美化方法中我們還會使用類似的方法。同時我們預留了confirm參數,如果為FALSE,則更改只在一個全局的臨時copy上進行。

增加鮮艷度

同理,增加鮮艷度的操作也在hsv空間進行,增大s通道的值。

def brightening(self,rate=0.3,confirm=True): 提升鮮艷度 arguments: rate:float,-1~1,new_S=min(255,S*(1+rate)) confirm:wether confirm this option patch_mask=self.get_mask_re((1,1)) if confirm: self.confirm() patch_new=self.patch_hsv[:,:,1]*patch_mask[:,:,1]*rate patch_new=cv2.GaussianBlur(patch_new,(3,3),0) self.patch_hsv[:,:,1]=np.minimum(self.patch_hsv[:,:,1]+patch_new,255).astype(uint8) self.im_bgr[:]=cv2.cvtColor(self.im_hsv, cv2.COLOR_HSV2BGR)[:] self.update_temp() else: self.patch_hsv_temp[:]=cv2.cvtColor(self.patch_bgr_temp, cv2.COLOR_BGR2HSV)[:] patch_new=self.patch_hsv_temp[:,:,1]*patch_mask[:,:,1]*rate patch_new=cv2.GaussianBlur(patch_new,(3,3),0) self.patch_hsv_temp[:,:,1]=np.minimum(self.patch_hsv[:,:,1]+patch_new,255).astype(uint8) self.patch_bgr_temp[:]=cv2.cvtColor(self.patch_hsv_temp, cv2.COLOR_HSV2BGR)[:]

磨皮

磨皮即是去除皮膚上痘痘、皺紋等等噪音,讓皮膚更加平滑。這裡我們使用高斯濾波器和雙邊濾波器在BGR空間進行操作。

其中kernelsize是根據patch大小計算得到的。

def get_ksize(self,rate=15): size=max([int(np.sqrt(self.size/3)/rate),1]) size=(size if size%2==1 else size+1) return (size,size) def smooth(self,rate=0.6,ksize=None,confirm=True): 磨皮 arguments: rate:float,0~1,im=rate*new+(1-rate)*src confirm:wether confirm this option if ksize==None: ksize=self.get_ksize(80) index=self.patch_mask>0 if confirm: self.confirm() patch_new=cv2.GaussianBlur(cv2.bilateralFilter(self.patch_bgr,5,*ksize),ksize,0) self.patch_bgr[index]=np.minimum(rate*patch_new[index]+(1-rate)*self.patch_bgr[index],255).astype(uint8) self.im_hsv[:]=cv2.cvtColor(self.im_bgr, cv2.COLOR_BGR2HSV)[:] self.update_temp() else: patch_new=cv2.GaussianBlur(cv2.bilateralFilter(self.patch_bgr_temp,3,*ksize),ksize,0) self.patch_bgr_temp[index]=np.minimum(rate*patch_new[index]+(1-rate)*self.patch_bgr_temp[index],255).astype(uint8) self.patch_hsv_temp[:]=cv2.cvtColor(self.patch_bgr_temp, cv2.COLOR_BGR2HSV)[:]

銳化

銳化演算法我使用的是最簡單的卷積銳化演算法

用一個形如

的卷積核去對圖像進行卷積。

def sharpen(self,rate=0.3,confirm=True): 銳化 ksize=self.get_ksize(10) patch_mask=self.get_mask_re((3,3)) kernel = np.zeros( ksize, np.float32) center=int(ksize[0]/2) kernel[center,center] = 2.0 #Identity, times two! #Create a box filter: boxFilter = np.ones( ksize, np.float32) / (ksize[0]*ksize[1]) #Subtract the two: kernel = kernel - boxFilter index=patch_mask>0 if confirm: self.confirm() sharp=cv2.filter2D(self.patch_bgr,-1,kernel) self.patch_bgr[index]=np.minimum(((1-rate)*self.patch_bgr)[index]+sharp[index]*rate,255).astype(uint8) self.update_temp() else: sharp=cv2.filter2D(self.patch_bgr_temp,-1,kernel) self.patch_bgr_temp[:]=np.minimum(self.patch_bgr_temp+self.patch_mask*sharp*rate,255).astype(uint8) self.patch_hsv_temp[:]=cv2.cvtColor(self.patch_bgr_temp, cv2.COLOR_BGR2HSV)[:]

至此,所有的基本美化方法就都成型了,我們在需要美化圖片的時候只需要調用對應部位的美化方法就可以了。

化妝器對象

dlib的模型載入需要時間,我們希望一次載入可以處理多個圖片,讀取和保存圖片、提取特徵也有各自的方法,所以我們需要一個化妝器對象來維持秩序數據和方法的生存周期。

class Makeup(): 化妝器 def __init__(self,predictor_path="./data/shape_predictor_68_face_landmarks.dat"): self.photo_path=[] self.PREDICTOR_PATH = predictor_path self.faces={} #人臉定位、特徵提取器,來自dlib self.detector = dlib.get_frontal_face_detector() self.predictor = dlib.shape_predictor(self.PREDICTOR_PATH) def get_faces(self,im_bgr,im_hsv,temp_bgr,temp_hsv,name,n=1): 人臉定位和特徵提取,定位到兩張及以上臉或者沒有人臉將拋出異常 im: 照片的numpy數組 fname: 照片名字的字元串 返回值: 人臉特徵(x,y)坐標的矩陣 rects = self.detector(im_bgr, 1) if len(rects) <1: raise NoFace(Too many faces in +name) return {name:[Face(im_bgr,im_hsv,temp_bgr,temp_hsv,np.array([[p.x, p.y] for p in self.predictor(im_bgr, rect).parts()]),i) for i,rect in enumerate(rects)]} def read_im(self,fname,scale=1): 讀取圖片 im = cv2.imdecode(np.fromfile(fname,dtype=np.uint8),-1) if type(im)==type(None): print(fname) raise ValueError(Opencv error reading image "{}" , got None.format(fname)) return im def read_and_mark(self,fname): im_bgr=self.read_im(fname) im_hsv=cv2.cvtColor(im_bgr, cv2.COLOR_BGR2HSV) temp_bgr,temp_hsv=im_bgr.copy(),im_hsv.copy() return im_bgr,temp_bgr,self.get_faces(im_bgr,im_hsv,temp_bgr,temp_hsv,fname)

read_and_mark方法接受文件路徑作為參數,讀取圖片並定位所有人臉,逐個實例化為Face對象。方法返回全局bgr圖片和全局臨時圖片,以及一個鍵名為fname,鍵值為一個Face對象的列表,代表圖片內的所有Face對象。

美顏操作

作為示例,我們對所有部分進行美白,除眼睛之外的部位進行磨皮,眼睛銳化實現亮眼,嘴唇提升鮮艷度實現紅唇。

處理一張圖片的主函數如下:

if __name__==__main__: path=./heads/x.jpg mu=Makeup() im,temp_bgr,faces=mu.read_and_mark(path) imc=im.copy() cv2.imshow(ori,imc) for face in faces[path]: face.whitening() face.smooth(0.7) face.organs[forehead].whitening() face.organs[forehead].smooth(0.7) face.organs[mouth].brightening(0.6) face.organs[mouth].smooth(0.7) face.organs[mouth].whitening() face.organs[left eye].whitening() face.organs[right eye].whitening() face.organs[left eye].sharpen(0.7) face.organs[right eye].sharpen(0.7) face.organs[left brow].whitening() face.organs[right brow].whitening() face.organs[left brow].sharpen() face.organs[right brow].sharpen() face.organs[nose].whitening() face.organs[nose].smooth(0.7) face.organs[nose].sharpen() cv2.imshow(new,im.copy()) cv2.waitKey() print(Quiting)

效果如圖

GUI

為了方便直觀的編輯圖片,我又用pyqt編寫了GUI版本,效果如文章開頭。

Future Works

瘦臉、大眼演算法。這個要研究一下,局部扭曲和放大。

只提升嘴唇的鮮艷度,把牙齒剔除,否則會造成黃牙。

有時間試一下對一整個視頻文件進行美顏處理~

總結

本項目用盡量簡潔高效的演算法實現了AI美顏演算法的demo,對人臉不同部位進行精確的美化操作。

總的來說,效果。。。。算了,直男癌不評價了,去挑口紅了(逃

推薦閱讀:

眼球追蹤技術,可用於操作瀏覽器等,你有什麼創意或看法?
[開源] 一個機器翻譯平台 + 一個人臉識別平台
C# 實現人臉識別一 (運用虹軟人臉識別引擎)
人肉搜索的典型陷阱——側臉識別

TAG:深度学习DeepLearning | 人脸识别 | 化妆 |