人脸识别 之 OpenCV (Haar、DNN)人脸检测

一、基于 OpenCV Haar:

        OpenCV提供了两个程序可以训练自己的级联分类器 opencv_haartraining opencv_traincascade。opencv_traincascade 是一个新程序,使用 OpenCV 2.x API 以 C++ 编写。这二者主要的区别是 opencv_traincascade 支持 Haar 和 LBP (Local Binary Patterns) 两种特征,并易于增加其他的特征。与Haar特征相比,LBP特征是整数特征,因此训练和检测过程都会比Haar特征快几倍。LBP和Haar特征用于检测的准确率,是依赖训练过程中的训练数据的质量和训练参数。训练一个与基于Haar特征同样准确度的LBP的分类器是可能的。
        与其他分类器模型的训练方法类似,同样需要训练数据与测试数据;其中训练数据包含正样本pos与负样本neg。训练程序 opencv_haartraining.exe 与 opencv_traincascade.exe 对输入的数据格式是有要求的,所以需要相关的辅助程序:
        opencv_createsamples 用来准备训练用的正样本数据和测试数据。opencv_createsamples 能够生成能被 opencv_haartraining 和 opencv_traincascade 程序支持的正样本数据。它的输出为以 *.vec 为扩展名的文件,该文件以二进制方式存储图像。
        我们可以直接调用OpenCV自带的Haar级联特征分类器来实现人脸定位。除此以外,OpenCV还提供了使用Hog特征和LBP算法的级联分类器。Hog级联分类器主要用于行人检测。
为了训练针对特定类型对象的级联分类器,OpenCV提供了专门的软件工具。在OpenCV根目录下的build文件夹下,查找 build\x64\vc15\bin 目录(不同的OpenCV版本,路径会略有差异),会找到 opencv_createsamples.exe 和 opencv_traincascade.exe,这两个exe文件可以用来训练级联分类器。
训练级联分类器很耗时,如果训练的数据量较大,可能需要好几天才能完成。在OpenCV中,有一些训练好的级联分类器供用户使用。这些分类器可以用来检测人脸、脸部特征(眼睛、鼻子)、人类和其他物体。这些级联分类器以XML文件的形式存放在OpenCV源文件的data目录下,加载不同级联分类器的XML文件就可以实现对不同对象的检测。
        OpenCV自带的级联分类器存储在OpenCV根文件夹的data文件夹下。该文件夹包含三个子文件夹:haarcascades、hogcascades、lbpcascades,里面分别存储的是Harr级联分类器、HOG级联分类器、LBP级联分类器。其中,Harr级联分类器多达20多种(随着版本更新还会继续增加),提供了对多种对象的检测功能。
部分级联分类器如下表所示:
训练级联分类器 教程开始:
1、下载 opencv-3.4.15-vc14_vc15.exe,运行后自动解压。添加 Path 环境变量:
D:\opencv-3.4.15\build\x64\vc15\bin
2、创建文件夹 pos_img、neg_img、xml,结构如下:
3、数据准备,正负样本均为灰度图。
正样本(预检测物体):500张,20px * 20px。
负样本(不包含检测物体的背景图):1500张,尺寸需大于正样本。
说明:
如果在训练时要用到500正样本和1500负样本,那么在采集的时候需要多采集一些,因为在训练时,要填写 -numPos 和 -numNeg 这两个参数,而根据实际训练情况,要满足以下要求:实际准备的正样本数量(读入 vec-file 的正样本数) >= numPos + (numStage - 1) * numPos * (1 - minHitRate)
vec-file:通过 opencv_createSamples 利用 posdata.txt 生成的 pos.vec 文件(正样本数就是 posdata.txt 读取的原始正样本的数量)
numPos:每一级stage训练所需的正样本数目(本人原始正样本数为565,那numPos就设置为500)
numStage:训练的级数,默认为20
minHitRate:分类器的每一级希望得到最小检测率(即正样本被判断有效的比例),最优设置0.9999
因为首先我们要知道训练程序中写的numNeg参数表示每级训练用到的负样本数,本人把它设置为1500。其实这个numNeg的大小和原始负样本数量没有任何关联, 因为numNeg是通过滑动窗口在原始负样本上不断滑动采集,得到的预处理负样本图像的数量(滑动窗口的大小就是正样本的大小,这里是20*20)
负样本的尺寸如果不够大,或者和正样本一样大,滑动窗口就无法在原始负样本上滑动来采集足够多的训练图像,再加上如果收集到的原始负样本的数量可能并不比正样本多多少,也许就是3、4倍。这样的话就会导致(执行opencv_traincascade训练程序过程中中断)的错误,同时会报(Train dataset for temp stage can not be filled. Branch training terminated。)这时我们发现训练图中的FA( FalseAlarm,虚警率 )为0,表示负样本已全部被正确分类了,也就不会有负样本继续参与下一轮的训练了,所以就退出了。
所以在准备原始负样本的时候一定要保证尺寸足够大,同时包含的内容足够复杂。
---numPos 和 numNeg的比例---
numPos : numNeg = 1:3,这是最优的比例
1.当numPos和numNeg比例接近的时候1:1,对numNeg内负样本的看中程度很低,在实际的生活中负样本肯定远远多于正样本。
2.当numPos和numNeg比例较大的时候1:10,对numNeg内负样本多于看中而忽略了正样本的统计特性,造成正样本权重总和很小,当权重小于一定程度的时候可能很大一部分正样本都不参与训练了。
--- Haar和LBP特征 ---
基于LBP特征的分类器几乎能和基于Haar特征的分类器拥有一样的性能,并且由于LBP属于整数型的特征,所以在训练的时候要比Haar特征快得多。所以在训练的参数中要添加 -featureType LBP,这样能加快训练速度
4、生成样本说明文件,cmd 执行:
dir /b/s/p/w pos_img\*.jpg > posdata.txt
dir /b/s/p/w neg_img\*.jpg > negdata.txt
5、删除 posdata.txt、negdata.txt 文件内的最后一行空行;内容格式如下:
说明:
1:样本数目为1;
0 0:样本起始坐标;
20 20:样本宽高;
6、生成正样本描述文件,执行结束后生成一个 pos.vec 文件。
opencv_createsamples.exe -info posdata.txt -vec pos.vec -num 500 -w 20 -h 20

说明:
-info:正样本说明文件(上面生成的txt文件)
-vec:正样本描述文件
-num:正样本数量
-w -h:正样本宽高
7、样本训练,训练级联分类器,生成 xml(在 xml 目录下生成了一堆xml文件,我们选用cascade.xml)
# opencv3.2 没有 opencv_haartraining.exe 需使用 opencv_traincascade.exe
opencv_traincascade.exe -data xml -vec pos.vec -bg negdata.txt -numPos 500 -numNeg 1500 -numStages 20 -featureType LBP -w 20 -h 20

说明:
-data:存放 xml 文件的目录,该文件夹一定要事先创建好,否则系统会报错
-vec:正样本vec文件源,由 opencv_createsamples.exe 程序生成的正样本vec文件
-bg:负样本路径txt文件
-numPos:正样本数量
-numNeg:负样本数量
-numStages:训练分类器的级数
-featureType:默认使用Haar特征,还有LBP和HOG可供选择(HOG为opencv2.4.0版本以上)
-w -h:样本宽高
-minHitRate:分类器的每一级希望得到最小检测率(即正样本被判断有效的比例)
-maxFalseAlarmRate:分类器的每一级希望的最大误检率(负样本判定为正样本的概率)
-mode:选择训练中使用的Haar特征类型。BASIC只使用右上特征,ALL使用所有右上特征及45度旋转特征(使用Haar特征的前提下,否则不使用此参数)
8、最后目录结构
9、测试分类器性能
        opencv_performance

二、基于 OpenCV DNN:

        OpenCV 3.4 版本之前自带的人脸检测器是基于 Haar+Adaboost 的,速度还可以,但是检出率很低,误检也很多,脸的角度稍大就检不出来,还经常会把一些乱七八糟的东西当做人脸,实在不敢恭维。

        OpenCV 3.4 版本主要增强了 dnn模块,特别是添加了对 faster-rcnn 的支持,并且带有 OpenCL加速,效果还不错。

支持 Caffe、Tensorflow 模型:

        1、Caffe 模型,需要两个文件:模型参数 deploy.prototxt 位于 D:\opencv-3.4.15\sources\samples\dnn\face_detector ,文件中定义了每层的结构信息;模型配置文件即模型框架:res10_300x300_ssd_iter_140000_fp16.caffemodel,可执行 download_weights.py 下载,或者直接访问 weights.meta4 文件里的链接下载。

        2、Tensorflow 模型,需要两个文件:opencv_face_detector.pbtxt 位于 D:\opencv-3.4.15\sources\samples\dnn\face_detector ,opencv_face_detector_uint8.pb,可执行 download_weights.py 下载,或者直接访问 weights.meta4 文件里的链接下载。

        其中 Tensorflow 版本的模型做了更加进一步的压缩优化,大小只有2MB左右,非常适合移植到移动端使用,实现人脸检测功能,而 Caffe 版本的是 fp16 的浮点数模型,精准度更好。

Python 源码、测试效果

import cv2
import numpy as np
import os

face_database_folder_path =
"../face_img"
face_database_imgs = []
face_database_labels = []
face_database_names = []
index =
1
for filename in os.listdir(face_database_folder_path):
if filename.endswith('.jpg'):
print('处理第 %s 张图片' % index)
else:
continue
image = cv2.imread(face_database_folder_path + '/' + filename)
gray_img = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
face_database_imgs.append(gray_img)
face_database_labels.append(index -
1)
face_database_names.append(filename.split(
'.')[0])
index +=
1
recognizer = cv2.face.LBPHFaceRecognizer_create()
# 使用之前训练好的模型
# recognizer.read('my_trainner.yml')
recognizer.train(face_database_imgs, np.array(face_database_labels))
# 保存模型,方便下次直接使用训练好的模型
# recognizer.save('my_trainner.yml')

input_shape = (300, 300)
mean = (
104.0, 177.0, 123.0) # 这个是在Net训练的时候设定的,在训练的时候 transform_param 中设置的。
# Caffe 模型
net = cv2.dnn.readNetFromCaffe("deploy.prototxt", "res10_300x300_ssd_iter_140000_fp16.caffemodel")
# Tensorflow 模型
# net = cv2.dnn.readNetFromTensorflow("opencv_face_detector_uint8.pb", "opencv_face_detector.pbtxt")
cameraCapture = cv2.VideoCapture(0)
success, image = cameraCapture.read()
while success and cv2.waitKey(1) == -1:
success, image = cameraCapture.read()
# image = cv2.resize(image, input_shape)
(h, w) = image.shape[:2]
# 构造blob
blob = cv2.dnn.blobFromImage(image, 1.0, input_shape, mean)
# 检测人脸
net.setInput(blob)
detections = net.forward()

default_confidence =
0.5
bboxes = []
# 遍历
for i in range(detections.shape[2]):
confidence = detections[
0, 0, i, 2]
# 过滤弱检测
if confidence > default_confidence:
# 获取检测框坐标
box = detections[0, 0, i, 3:7] * np.array([w, h, w, h])
(startX, startY, endX, endY) = box.astype(
"int")
bboxes.append([startX, startY, endX, endY, confidence])

if len(bboxes) == 0:
print('未检测到人脸')
else:
for startX, startY, endX, endY, confidence in bboxes:
# 绘制框
text = "{:.2f}%".format(confidence * 100)
y = startY -
10 if (startY - 10) > 10 else (startY + 10)
cv2.rectangle(image, (startX, startY), (endX, endY), (
0, 0, 255), 2)
cv2.putText(image, text, (startX, y), cv2.FONT_HERSHEY_SIMPLEX,
0.45, (0, 0, 255), 1)

# 生成灰度图
gray_img = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
predict_image = gray_img[startY:endY, startX:endX]
# cv2.imshow("Camera", predict_image)
# cv2.waitKey(10)
label, confidence = recognizer.predict(predict_image)
print('Label:%s,Name:%s,confidence:%.2f' % (label, face_database_names[label], confidence))
cv2.putText(image, face_database_names[label], (startX, startY -
20), cv2.FONT_HERSHEY_SIMPLEX, 1, 255, 2)

cv2.imshow("Camera", image)
if cv2.waitKey(10) & 0xFF == ord("q"):
break

cameraCapture.release()
cv2.destroyAilwindows()

检测效果明显好于 Haar,效果图如下: