导读
因为目前公开的口罩人脸
数据比较少,如果想训练一个口罩人脸识别模型,必须依赖大量的人脸数据。为了收集到更多的口罩人脸数据,我们只能利用已有的公开人脸数据上通过程序来模拟人脸带口罩
。这篇文章向大家介绍一个简单版本使用python来给正常人脸带上口罩的思路和代码
原理介绍
想要给人脸戴口罩,就必须要用到人脸关键点
的信息,通过关键点信息,我们就能知道应该将口罩放在哪个位置
人脸关键点检测
这里我们使用的是68个人脸关键点
,关键点的位置在人脸上的分布如下图所示
后面我们只需要用到其中四个关键点,用来计算口罩的位置,鼻梁第28点
,下巴的第2、9、16点
根据关键点计算口罩的size
我们需要根据上面提到的四个关键点来计算口罩的width
和height
。口罩的宽利用点2和点16的欧式距离,口罩的高用点28和点9的欧式距离
。
实际使用中,建议将口罩分为左右两部分,因为有时候头部会向左或右偏转,分成两部分计算效果会好些。口罩的高度计算不变,计算左半部分口罩的宽利用点2
到直线
(由点28和点9组成)的距离,同理可知右半部分口罩的宽计算。
口罩角度的调整
因为人脸可能是歪的,利用点28和点9组成的直线结合垂直线计算偏转角度
(人脸是正的,点28和点9组成的直线是垂直水平线),通过偏转角度来选择口罩
口罩位置的确定
利用点28和点9来确定口罩所处位置的中心点
,针对头会左右旋转的问题,需要对口罩的中心位置的确定进行微调,具体细节实现可以参考代码。
代码实现
环境准备
python版本:3.7
第三方库安装
#安装OpenCVpip install opencv-python#安装numpypip install numpy#安装PIL pip install pillow#安装pytorch,pytorch (>=1.0)#建议安装cuda版本的pytorch#安装教程:https://pytorch.org/get-started/previous-versions/#安装人脸关键点检测face_alignmentpip install face-alignment
1
2
3
4
5
6
7
8
9
10
11
关键点模型下载不了的问题
如果在使用face-alignment
的时候无法下载模型文件,可以直接从下面的地址进行下载:
https://www.adrianbulat.com/downloads/python-fan/2DFAN-4.pth.tar
https://www.adrianbulat.com/downloads/python-fan/3DFAN-4.pth.tar
https://www.adrianbulat.com/downloads/python-fan/depth.pth.tar
将下载好的模型解压后得到pth
模型文件,对linux系统,将解压后得到的pth文件放到~/.face_alignment/data
目录下
口罩mask的提取
需要注意的是,准备的口罩mask最好是png(RGBA)透明背景
的图片。否则在将口罩mask复制到人脸上时会遮盖多余的部分,影响效果。
我这里实现了一个从纯色背景的口罩jpg图片中,提取出一个透明背景的png口罩图片,代码如下
import cv2import numpy as npimg = cv2.imread("mask_images/mask.jpg")#将图片分割为三个通道b_channel,g_channel,r_channel = cv2.split(img)alpha_channel = np.zeros(b_channel.shape,dtype=b_channel.dtype)gray_img = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)blur_img = cv2.GaussianBlur(gray_img,(3,3),cv2.BORDER_DEFAULT)canny_img = cv2.Canny(blur_img,50,150)contours,hierarchy = cv2.findContours(canny_img,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_NONE)#获取面积最大的轮廓contour = max(contours,key=cv2.contourArea)#获取最大轮廓的凸包hull = cv2.convexHull(contour)#初始化一个alpha通道alpha_channel = cv2.drawContours(alpha_channel,[hull],-1,255,-1)png_img = cv2.merge((b_channel,g_channel,r_channel,alpha_channel))#获取最大轮廓的最小外接矩形rect = cv2.minAreaRect(contour)#获取矩形四个顶点的坐标box = cv2.boxPoints(rect)#将矩形转换为水平的矩形x_min = int(np.min(box[:,0]))y_min = int(np.min(box[:,1]))x_max = int(np.max(box[:,0]))y_max = int(np.max(box[:,1]))png_img = png_img[y_min:y_max,x_min:x_max,:]cv2.imwrite("mask_images/mask.png",png_img)
给人脸添加口罩
import os,face_alignmentfrom PIL import Imageimport numpy as npclass WearFaceMask(object): def __init__(self,face_path,mask_path,save_path,enlarge_ratio=0.9,use_gpu=True,show=False): self.face_path = face_path self.mask_path = mask_path self.save_path = save_path self.enlarge_ratio = enlarge_ratio self.use_gpu = use_gpu self.show = show if use_gpu: self.fa=face_alignment.FaceAlignment( face_alignment.LandmarksType._2D, device='cuda') else: self.fa = face_alignment.FaceAlignment( face_alignment.LandmarksType._2D, device='cpu') def get_key_landmarks(self,face_landmarks): """从68个关键点中获取4个关键点的位置 用来定口罩佩戴的位置 :param face_landmarks:人脸68个关键点 :return: """ #获取下巴右边佩戴口罩的位置(关键点2) self.left_chin_point = face_landmarks[1] #获取鼻梁佩戴口罩的位置(关键点28) self.nose_point = face_landmarks[27] #获取下巴左边佩戴口罩的位置(关键点16) self.right_chin_point = face_landmarks[15] #获取下巴最下面佩戴口罩的位置(关键点9) self.bottom_chin_point = face_landmarks[8] @staticmethod def cal_point_to_line_dist(point,line_point1,line_point2): """计算点到直线的距离 :param point: 点的坐标 :param line_point1: 直线上第一点的坐标 :param line_point2: 直线上另一点的坐标 :return: 点到直线的距离 """ #计算点和直线上点组成的向量 vec1 = line_point1 - point vec2 = line_point2 - point dist = abs(np.cross(vec1,vec2)) / np.linalg.norm(line_point2 - line_point1) return dist def wear_face_mask(self): self._face_img = Image.open(self.face_path) self._mask_img = Image.open(self.mask_path) face_landmarks = self.fa.get_landmarks(np.asarray(self._face_img))[0].astype(np.int32) #获取需要的关键点信息 self.get_key_landmarks(face_landmarks) #获取口罩的宽和高 mask_width = self._mask_img.width mask_height = self._mask_img.height #计算口罩适应人脸后和高度 new_mask_height = int(np.linalg.norm(self.bottom_chin_point - self.nose_point)) #将口罩分割为左右两部分用来适配人脸 #左边口罩人脸 mask_left_img = self._mask_img.crop((0,0,mask_width//2,mask_height)) mask_left_width = self.cal_point_to_line_dist(self.left_chin_point, self.nose_point,self.bottom_chin_point) mask_left_width = int(mask_left_width * self.enlarge_ratio) mask_left_img = mask_left_img.resize((mask_left_width,new_mask_height)) #右边口罩人脸 mask_right_img = self._mask_img.crop((mask_width//2,0,mask_width,mask_height)) mask_right_width = self.cal_point_to_line_dist(self.right_chin_point, self.nose_point,self.bottom_chin_point) mask_right_width = int(mask_right_width * self.enlarge_ratio) mask_right_img = mask_right_img.resize((mask_right_width,new_mask_height)) #合并口罩 size = (mask_left_width+mask_right_width,new_mask_height) mask_img = Image.new("RGBA",size) mask_img.paste(mask_left_img,(0,0),mask_left_img) mask_img.paste(mask_right_img,(mask_left_width,0),mask_right_img) #计算人脸的旋转角度 angle = np.arctan2(self.bottom_chin_point[1]-self.nose_point[1], self.bottom_chin_point[0]-self.nose_point[0]) #旋转口罩 rotated_mask_img = mask_img.rotate(angle,expand=True) #计算mask的位置 mask_center_x = (self.nose_point[0]+self.bottom_chin_point[0]) // 2 mask_center_y = (self.nose_point[1]+self.bottom_chin_point[1]) // 2 offset = mask_img.width // 2 - mask_left_width #将弧度转换为角度 radian = angle * np.pi / 180 #对口罩的位置进行微调 box_x = mask_center_x + int(offset * np.cos(radian)) - rotated_mask_img.width // 2 box_y = mask_center_y + int(offset * np.sin(radian)) - rotated_mask_img.height // 2 self._face_img.paste(mask_img,(box_x,box_y),mask_img) self.save() def save(self): self._face_img.save(self.save_path) print(f'Save to {self.save_path}')face_path="imgs/test.jpg"mask_path="mask_images/mask.png"save_path="imgs/test_mask.jpg"WearFaceMask(face_path,mask_path,save_path).wear_face_mask()
接下来我们试试给最不喜欢戴口罩的特没谱同志带上口罩,看看效果如何。感觉好像效果还是差了那么点,下篇文章介绍另一种方法来提升一下这个效果。
代码我已经上传到gitee:https://gitee.com/pragmaticAgile/wear_face_mask.git