本篇以后不再赘述QT相关的界面实现,直接介绍Python实现人脸检测、人像预处理和图 本篇开始将不再缀术QT界面相关的具体实现方法,直接介绍Python实现的人脸检测和人脸扶正实现方法,达到采集人脸数据集的目的,为后面的人脸识别模型训练做准备。
本系统的人像采集模块,会实时显示摄像头画面,并实时循环地从画面总捕获人脸图像,并在对其进行人脸扶正后保存到本地以ID命名的文件夹中,直到采集够一定数量的图片才停止整个采集过程。
在人脸检测部分,我使用了训练好的Haar人脸检测模型,达到快速检测人像的目的。HAAR人脸检测方法是一种基于统计的人脸检测方法,它利用HAAR-LIKE特征和AdaBoost算法训练出能够检测人脸和非人类的强分类器,达到快速和高效实现人脸检测的目的,尤其在人脸检测速度上,显著优于DLIB等其它人脸检测方法,非常适合用于对响应速度要求较高的人脸检测场景。Haar人脸检测模型网上直接找到下载就行。
在开始人脸检测时,人脸捕获线程会抽取当前帧图像作为HAAR人脸检测算法的输入,为了确保采集到的人像图片尺寸稳定(避免过大或过小),在算法中首先限制了最小人像大小为150*150,最大人像大小为220*220,有效筛去了过小或过大的人像,避免影响后期的模型训练。同时利用OPENCV自带的拉普拉斯模糊检测算法,对捕获的人像进行模糊检测,进一步筛去过于模糊的图像。至此,系统就得到了大小合适且清晰度较好的人脸图像,具体流程如图1所示,HAAR人脸检测算法会实时检测出画面中的正脸人脸位置并打上红框进行提示,如图2所示,之后对截取到的图像进行尺寸判断,若不符合,则重新获取摄像头画面进行检测,若符合则继续进行清晰度检测,若清晰度低于预设的阈值则重新获取摄像头画面进行检测,否则将截取到的该人像传递给人像对齐模块。
图 2 HAAR人脸检测演示
首先介绍下DLIB,DLIB是一个基于C++的开源工具库,包含了许多机器学习的成熟算法与模型。DLIB具有独立封装的可移植代码和全面的文档说明,可以轻易地移植到自己所需的项目中。本系统中的人像对齐处理选择DLIB 库进行开发。
在实际的人脸识别过程中,因为每次系统输入的图像尺寸不同,人脸区域大小不同,人脸的角度也可能相差很多,非常影响分类效果,所以需要对检测到的人脸图像进行归一化操作,使最终输入进行训练或识别的人脸图像保持较统一的样式。在本系统中,人脸对齐首先借助DLIB的landmark人脸脸部特征点提取技术检测得到脸部的68个关键点坐标,如图3所示,再通过计算两眼连线和水平线的夹角得出人脸倾斜角,最后通过旋转图片实现人脸对齐。
图 3 landmark特征点
本系统中,通过HAAR人脸检测,系统已经能稳定地获取到尺寸合适且清晰度较好的人脸正脸图像。为了进一步优化数据集以利于分类模型的训练,本系统利用DLIB人脸特征点检测方法对上一步获得的人脸图像的进行对齐操作。
由于原本HAAR人脸检测是以正脸为检测目标,筛去了很大部分侧脸、歪脸的情况,同时为了避免人脸对齐时旋转图像造成的黑边干扰,本系统在上一步HAAR检测人脸结束后,在画面中截取人像时,向四周延伸20像素,即160*160像素会扩展为200*200像素,再将此作为人像对齐算法的输入,对齐完成后再删去四周填充的像素,只截取中心的160*160像素,作为最终保存到本地的人像图片。实际变化过程如图4所示:
图 4 人像对齐过程
采集后的部分图像效果如图:
以下为本人脸采集模块代码,已经尽可能地封装函数,并含有注释应该很容易理解整个实现细节,其中face=single_face_alignment(image)即是在调用人脸扶正函数将haar检测后截取的image人像进行扶正。
import math
import numpy as np
import sys
import os
import cv2
import dlib
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import QThread, pyqtSignal, QTimer
from PyQt5.QtGui import QImage, QPixmap
from PyQt5.QtWidgets import QFileDialog, QMessageBox, QDockWidget, QListWidget, QProgressBar, QPushButton
from docutils.nodes import classifier
from db.SQL_connect import SQL
from pyqt.pyqt_form_collect import Ui_CollectWindow
IMAGE_NUM=50
PATH = "E:\\PyCharm_workspace\\bishe\\TrainingData_2\\"
classifier_z = cv2.CascadeClassifier('E:\\PyCharm_workspace\\bishe\haarcascades\\haarcascade_frontalface_default.xml')
def get_landmarks(image):#获取DLIB特征点坐标
predictor_model = 'shape_predictor_68_face_landmarks.dat'
detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor(predictor_model)
img_gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
rects = detector(img_gray, 0)
if rects:
landmarks = np.matrix([[p.x, p.y] for p in predictor(image, rects[0]).parts()])
return landmarks
else:
return None
def single_face_alignment(face):#根据获得的坐标对人脸进行扶正
#face = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
landmarks = get_landmarks(face)
if not landmarks is None:
eye_center = ((landmarks[36, 0] + landmarks[45, 0]) * 1. / 2, # 计算两眼的中心坐标
(landmarks[36, 1] + landmarks[45, 1]) * 1. / 2)
dx = (landmarks[45, 0] - landmarks[36, 0]) # note: right - right
dy = (landmarks[45, 1] - landmarks[36, 1])
angle = math.atan2(dy, dx) * 180. / math.pi # 计算角度
RotateMatrix = cv2.getRotationMatrix2D(eye_center, angle, scale=1) # 计算仿射矩阵
align_face = cv2.warpAffine(face, RotateMatrix, (face.shape[0], face.shape[1])) # 进行放射变换,即旋转
return align_face
else:
return None
class SaveThread(QThread):
singoutMeg=pyqtSignal(int)
def __init__(self,image,num,id):
super(SaveThread,self).__init__()
self.image=image
self.id=id
self.num=num
def run(self):
self.path=PATH+self.id
if not os.path.exists(self.path):
os.mkdir(self.path)
gray = cv2.cvtColor(self.image, cv2.COLOR_BGR2GRAY) # 灰度化
faceRects = classifier_z.detectMultiScale(gray, scaleFactor=1.2, minNeighbors=3, minSize=(128, 128))
if len(faceRects) > 0:
faceRect=faceRects[0]
x,y,w,h = faceRect
image_name=self.path+'\\'+str(self.num)+'.jpg'
image = self.image[y-10:y+h+10, x-10:x+w+10] # 将当前帧含人脸部分保存为图片,注意这里存的还是彩色图片,前面检测时灰度化是为了降低计算量;这里访问的是从y位开始到y+h-1位
score=cv2.Laplacian(image,cv2.CV_64F).var()#拉普拉斯模糊度检测
if score>120 and image.shape[0]>150 and image.shape[0]<260:
face=single_face_alignment(image)
if not face is None:
cv2.imwrite(image_name, face[10:-10,10:-10])
self.singoutMeg.emit(self.num+1)
else:
self.singoutMeg.emit(self.num)
else:
self.singoutMeg.emit(self.num)
else:
self.singoutMeg.emit(self.num)
class CvThread(QThread):
singoutSource=pyqtSignal(QImage)
singoutImage=pyqtSignal(object)
def __init__(self):
super(CvThread,self).__init__()
self.cap = cv2.VideoCapture(0 + cv2.CAP_DSHOW)
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) # 设置分辨率
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
def run(self):
self.ok=True
self.startsave=False
while self.cap.isOpened() and self.ok:
ok,frame=self.cap.read()
frame = cv2.flip(frame, 1)
if self.startsave:
self.singoutImage.emit(frame)
frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
qimg = QImage(frame.data, frame.shape[1], frame.shape[0], QImage.Format_RGB888)
self.singoutSource.emit(qimg)
self.startsave=False
cv2.waitKey(30)
def start_save(self):
self.startsave=True
def end(self):
self.ok=False
self.cap.release()
self.quit()
class Collect_Window(QtWidgets.QMainWindow, Ui_CollectWindow):
signout = pyqtSignal(str)
def __init__(self):
super(Collect_Window, self).__init__()
self.setupUi(self)
self.is_camera_opened=False
self.bt_open.clicked.connect(self.Open_camera)
self.bt_start.clicked.connect(self.Collect)
def Open_camera(self):
if self.is_camera_opened:
self.is_camera_opened=~self.is_camera_opened
self.bt_open.setText('打开摄像头')
self.cvThread.end()
self.page_camera.clear()
self.mesage.setText('摄像头已关闭')
self.bt_start.setEnabled(False)
self.signout.emit('closed')
else:
if self.et_id.text() == '' or self.et_name.text()=='':
QMessageBox.warning(self, "警告", "ID和姓名不能为空,请输入!")
return
elif SQL.is_exist_2(self.et_id.text(), self.et_name.text()):
self.is_camera_opened = ~self.is_camera_opened
self.signout.emit('opened')
self.bt_open.setText('关闭摄像头')
self.mesage.setText("摄像头已开启")
self.cvThread = CvThread()
self.cvThread.singoutSource.connect(self.ShowCamera)
self.cvThread.singoutImage.connect(self.SaveImage)
self.cvThread.start()
self.bt_start.setEnabled(True)
else:
QMessageBox.warning(self, "警告", "用户信息有误,请重新输入!")
return
def Collect(self):
self.mesage.setText('开始采集')
self.progressBar.setValue(0)
self.num=0
self.cvThread.start_save()
def SaveImage(self,object):
self.savethread=SaveThread(object,self.num,self.et_id.text())
self.savethread.start()
self.savethread.singoutMeg.connect(self.ContinueSave)
def ContinueSave(self,num):
if num