PaddleHub, 依托于paddlepaddle的深度学习框架,里面包含有CV,NLP等的预训练模型,其安装代码为
pip install paddlehub
此外还有opencv,matplotlib,math等常用包。安装方法不再赘述。
本次主要用到了两个与训练包,face_landmark_localization和ace2p。
face_landmark_localization:用于面部检测,其训练集为AFW/AFLW,网络结构为Face_Landmark。送入一张面部照片可以将一下面部点的坐标返回
ace2p:人体解析(Human Parsing)是细粒度的语义分割任务,其旨在识别像素级别的人类图像的组成部分(例如,身体部位和服装)训练集为LIP,网络结构为ACE2P。ACE2P通过融合底层特征,全局上下文信息和边缘细节,端到端地训练学习人体解析任务。该结构针对Intersection over Union指标进行针对性的优化学习,提升准确率。以ACE2P单人人体解析网络为基础的解决方案在CVPR2019第三届LIP挑战赛中赢得了全部三个人体解析任务的第一名。该PaddleHub Module采用ResNet101作为骨干网络,接受输入图片大小为473x473x3。
这里将面部识别和分割进行封装在了一起
import cv2
import paddlehub as hub
import numpy as np
class face_Seg(object):
'''
imgFile:原始图片数据
origLandMark:第一次detection的坐标
faceImg:裁剪后的图片
resLandMark:修改后的坐标
mapImg:分割后的标注图片
resFaceImg:最终分割后图片
'''
def __init__(self, imgFile):
self.imgFile = imgFile
self.origLandMark = None
self.faceImg = None
self.resLandMark = None
self.mapImg = None
self.resFaceImg = None
self.oriMapImg = None
self.orgBox = None
@staticmethod
def keypoint_detection(images):
return hub.Module(name="face_landmark_localization").keypoint_detection(images=images)
@staticmethod
def human_parser(images):
return hub.Module(name="ace2p").segmentation(images=images)
'''
获取原始图像
'''
def getOrgImgFile(self):
return self.imgFile
'''
获取面部监测点信息
'''
def getOrLandMark(self):
if np.all(self.origLandMark == None):
try:
res = self.keypoint_detection(images=[self.imgFile])
except:
print("无法打开图片或检测有问题!")
return None
self.origLandMark = np.array(res[0]['data'][0])
return self.origLandMark
'''
获取面部图片的裁剪后图像
'''
def getFaceImg(self):
if np.all(self.faceImg == None):
LandMark = self.getOrLandMark()
x = LandMark[:,0]
y = LandMark[:,1]
self.orgBox = (int(y.min()),int(y.max()),int(x.min()),int(x.max()))
self.faceImg = self.imgFile[int(y.min()):int(y.max()), int(x.min()):int(x.max())]
resLM = list(map(lambda a:[a[0]-int(x.min()),a[1]-int(y.min())], LandMark))
self.resLandMark = np.array(resLM)
return self.faceImg
'''
获取原图中截取的部分
'''
def getOrgBox(self):
if self.orgBox == None:
self.getFaceImg()
return self.orgBox
'''
获取分割图片标记
'''
def getMapImg(self):
if np.all(self.mapImg == None):
res = self.human_parser(images=[self.getFaceImg()])
self.mapImg = np.array(res[0]['data'])
return self.mapImg
'''
仅返回脸部区域分割结果,和对应脸部区域的特征点坐标
'''
def getResult(self, savepath = None):
if np.all(self.resFaceImg == None):
mapimg = self.getMapImg()
self.resFaceImg = self.getFaceImg().copy()
X,Y = mapimg.shape
for i in range(X):
for j in range(Y):
if mapimg[i,j] != 13:
self.resFaceImg[i,j] = [255,255,255]
if savepath != None:
cv2.imwrite(savepath, self.resFaceImg)
print("成功保存图片!")
return self.resFaceImg, self.resLandMark
'''
将分割图片扩展到原始图片大小,并返回
'''
def getOriMapImg(self):
if np.all(self.oriMapImg == None):
X,Y,Z = self.imgFile.shape
mapimg = self.getMapImg()
self.oriMapImg = np.zeros((X,Y))
LandMark = self.getOrLandMark()
x = LandMark[:,0]
y = LandMark[:,1]
for i in range(X):
for j in range(Y):
if x.min()<i<x.max() and y.min()<j<y.max():
self.oriMapImg[j,i] = mapimg[j-int(y.min())-1, i-int(x.min())-1]
return self.oriMapImg
在对图像进行处理前需要定义两个函数,首先是计算两个向量之间的夹角,主要是判断人像需要旋转的角度,第二个函数就是将面部图片进行旋转,根据第一个计算出来的角度进行旋转。代码如下
import math
import numpy as np
import matplotlib.pyplot as plt
'''
计算两个向量的夹角,用于校准两张图片的脸部轴心位置
'''
def angle_between(v1,v2):
angle1 = math.atan2(v1[0], v1[1])
angle2 = math.atan2(v2[0], v2[1])
return (angle2-angle1)/math.pi*180
'''
旋转图片并对图片矫正
'''
def rotate_bound(image, angle, center=None):
(h, w) = image.shape[:2]
# 定义旋转中心,可以选择自定义的中心进行旋转
if np.all(center == None):
(cX, cY) = (w // 2, h // 2)
else:
(cX, cY) = (int(center[0]), int(center[1]))
M = cv2.getRotationMatrix2D((cX, cY), -angle, 1.0)
cos = np.abs(M[0, 0])
sin = np.abs(M[0, 1])
nW = int((h * sin) + (w * cos))
nH = int((h * cos) + (w * sin))
M[0, 2] += (nW / 2) - cX
M[1, 2] += (nH / 2) - cY
return cv2.warpAffine(image, M, (nW, nH))
首先我们计算背景图片和扣脸图片的夹角,这里将我男神彭于晏的帅气面庞抠出来
# 面部基本点检测,男神的脸
tar = face_Seg.keypoint_detection(images = [cv2.imread('./tar.jpg')])
LandMark = np.array(tar[0]['data'][0])
# 面部基本点检测,背景图片
pic = face_Seg(cv2.imread('./pic.png'))
baseLandMark = pic.getOrLandMark()
# 获取两个面部轴心向量
lineA = LandMark[27] - LandMark[8]
lineB = baseLandMark[27] -baseLandMark[8]
angle = angle_between(lineB,lineA)
# 旋转男神图像使得图像与背景图像的面部轴心夹角一致
tar = rotate_bound(cv2.imread('./tar.jpg'), angle,LandMark[8])
# 将旋转后图像放入face_Seg类中方便进一步处理
tar = face_Seg(tar)
此时我们有两个照片1,背景图片pic,和面部图片tar这里还需要将tar放入face_Seg类中,便于进行下一步的抠图计算。
# 获取面部切割结果并将面部切割的结果保存为face.jpg
FaceImg, LandMark = tar.getResult('face.jpg')
# 原图片处理,与上面一样
picFaceImg, picLandMark = pic.getResult('picface.jpg')
# 将两个脸的大小调整到一致
X,Y,Z = picFaceImg.shape
FaceImg = cv2.resize(FaceImg, (X,Y))
# 图片合并过程
result = pic.getOrgImgFile()
mask = 255 * np.ones(FaceImg.shape, FaceImg.dtype)
center = pic.getOrgBox()
center = (int((center[2]+center[3])/2) ,int((center[0]+center[1])/2))
flags = cv2.NORMAL_CLONE
FaceImg1 = FaceImg+5 # 调整亮度,是图像与背景融合更好
output = cv2.seamlessClone(FaceImg1, result, mask, center, flags)
plt.imshow(output)
cv2.imwrite('OK.jpg',output)
最后看一下效果吧,jupyter中看原图代码为
# 图片显示
from PIL import Image
import matplotlib.pyplot as plt
# 原图显示
plt.figure(figsize=[20,6])
plt.subplot(1,3,1)
plt.imshow(Image.open('pic.png'))
# 融合的图片
plt.subplot(1,3,2)
plt.imshow(Image.open('tar.jpg'))
# 融合后的图片
plt.subplot(1,3,3)
plt.imshow(Image.open('OK.jpg'))
plt.show()
整体来看图像契合还是比较好的,但是对于亮度的处理比较粗糙,需要改进。
另外还有一种思路就是,我们通过采集视频将每个欧拉角度下的人脸数据都采集下来,那么就可以按照欧拉角度将人脸进行贴合更换,应该会更好。