文章目录
- 通过opencv实现简单的人脸识别
- 1.环境配置
- 2.收集数据集
- 3.人脸数据的处理
- 4.通过神经网络训练模型
- 5.进行人脸识别
- 6.总结
通过opencv实现简单的人脸识别
网上有很多通过opencv实现的简单人脸识别,本文于其他文章差别不大,仅为作者复现代码后的一个学习记录。实现的过程大致为收集人脸数据集,通过神经网络训练出模型,然后将实时人脸放到模型中进行识别,其实就是解决一个简单的二分类问题。所以它的实用性其实并不高,但是对于初入计算机视觉的小白们,这是理解如何通过神经网络训练出模型的一个好机会。
1.环境配置
1.window,ubantu环境均可
2.python3.6,3.7版本均可
3.opencv最新版即可(直接pip)
4.scipy(我是1.3.1版本,一般不用额外装)
5.sklearn
6.Keras
7.tensorflow(建议1.4.0左右的版本)
2.收集数据集

我们接触到的人脸识别系统首先都需要有个录脸的过程,收集数据集这个步骤就类似于录脸。目的在于收集我们的人脸数据,让计算机可以对你的人脸进行训练。
收集数据的第一步就是打开摄像头,下述写了几个通过opencv打开摄像头的基本函数。
cv2.namedWindow(window_name) #窗口名字
cv2.VideoCapture(camera_id)#camera_id=0内置摄像头,也可以为本地视频路径
ok, frame = cap.read()#返回两个值(1.当前帧是否有效 2.当前帧截取的图片)
cap.release() #关闭摄像头
cv2.destroyAllWindows() #释放全部窗口
classfier = cv2.CascadeClassifier()
打开摄像头后,我们通过opencv内置的级联分类器CascadeClassifier进行捕捉人脸的步骤。
classfier = cv2.CascadeClassifier("D:/anaconda/Lib/site-packages/cv2/data/haarcascade_frontalface_alt2.xml")
可以看到分类器后面有一个xml文件的路径,当下载opencv后,找到anaconda的安装路径,然后根据上面给定的路径找一般都能找到。这个xml文件是opencv官方共享的具有普适性的训练数据,然后通过这些数据可以获得一个良好的人脸分类器。官方也共享了笑脸分类器,眼镜分类器等,有兴趣的可以在官方网址了解其他文件的作用,将xml文件路径改了试一试。
(官方网址:https://github.com/opencv/opencv/tree/master/data/haarcascades)
classfier.detectMultiScale()
得到人脸分类器的目的是获得视频中人脸的位置,并通过cv2.rectangle()对视频中捕捉的人脸进行画框。
classfier.detectMultiScale(const Mat& image, vector<Rect>& objects,double scaleFactor=1.1, int minNeighbors=3,int flags=0, Size minSize=Size(), Size maxSize=Size())
1.image表示的是分类器检测到的每一帧的图片作为输入
2.object表示检测到的人脸目标序列,一般可不写
3.scaleFactor默认为1.1,表示每次检测到的人脸目标缩小的比例
4.minNeighbors默认为3,表示检测过程中目标必须被检测三次才能被确定为人脸(分类器中是有个窗口对全局图片进行扫描的,即扫描过程中,窗口中出现了三次人脸可以确定该目标为人脸)
5.flag默认为0,一般可不写
6.minSize表示可截取的最小目标大小
7.maxSize表示可截取的最大目标大小
该函数输出的结果是人脸的位置(x,y,w,h),x,y指人脸位置的左上角坐标,w,h指长宽
收集人脸数据
在对一些核心函数有了解之后,我们开始进行人脸数据的收集。首先就是创建一个face文件夹,该文件夹中再新建两个文件夹分别命名为自己的姓名和other。

一个文件夹下收集的人脸必须是同一个人的(所以完成这件事需要两个人),收集两份人脸的目的为了用于二分类,以提高模型的精度。每个文件夹下收集1000张图片(总共2000张),在收集过程中人脸分类器可能会将的灯,标志等错误录入,这时就需要手动删除这些错误图片,防止后面训练时造成误差。

附上该步骤完整代码(命名为face_capture)
import cv2
import sysfrom PIL import Image#定义函数Capture_picture;目的:获取人脸数据集;参数:窗口名,相机编号,收集图片的数量,保存图片的路径
def Capture_picture(window_name, camera_id, picture_num, path_name):cv2.namedWindow(window_name)cap = cv2.VideoCapture(camera_id) #camera_id = 0为内置摄像头,= 1为外接摄像头,也可以为本地视频的路径#opencv内置的级联分类器,找到anaconda的安装位置往下找就可以找到classfier = cv2.CascadeClassifier("D:/anaconda/Lib/site-packages/cv2/data/haarcascade_frontalface_alt2.xml")color = (0,255,0) #边框颜色num = 0while cap.isOpened():ok, frame = cap.read() #cap.read()返回两个值,是否正确读取帧和当前帧的图片#print(ok)#print(frame)if not ok:break#将图片转成灰度图gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)#img_name = 'D:/desktop/face/gray/%d.jpg'%(num)#cv2.imwrite(img_name,gray)faceRects = classfier.detectMultiScale(gray, scaleFactor = 1.1, minNeighbors = 3, minSize = (32,32))#print(faceRects)if len(faceRects) > 0:for faceRect in faceRects:x,y,w,h = faceRect img_name = 'D:/desktop/face/huangzhaohong/%d.jpg'%(num) image = frame[y - 10: y + h + 10, x - 10: x + w + 10] #将人脸框相应的扩大cv2.imwrite(img_name, image)num += 1if num > (picture_num):breakcv2.rectangle(frame,(x - 10, y - 10), (x + w + 10, y + h + 10),color,2)font = cv2.FONT_HERSHEY_SIMPLEX#图片,标注文字,左上角坐标,字体形式,字体大小,颜色,字体粗细cv2.putText(frame,'num:%d' %(num),(x + 30, y + 30), font, 1, (255,0,0),4)if num > picture_num:breakcv2.imshow(window_name,frame)c = cv2.waitKey(10) #这里参数为0等于视频暂停if c & 0xFF == ord(' '):break #手动结束视频按空格#关闭摄像头,释放窗口cap.release()cv2.destroyAllWindows()if __name__ == '__main__':if len(sys.argv) != 1:print("!!!")else:Capture_picture("face",0,1000,"D:/desktop/face/huangzhaohong")
3.人脸数据的处理
机器学习中有一个很重要的概念叫有监督学习,而有监督学习中有细分两类问题:分类问题和聚类问题。**分类问题一般用于识别,回归问题一般用于检测。而在制作简单人脸识别的过程中,涉及到了两次的分类,一次的回归。**第一次的分类在上一步收集人脸数据时,识别视频中的人脸(知道是脸,但是不知道是谁的脸),第二次的分类在识别视频中的人脸是谁;回归用在上一步收集人脸时回归出人脸的位置。
在完成上一步骤后,我们已经解决了一个分类问题和一个回归问题,就剩下识别人脸是谁这个步骤,也是制作人脸识别最核心的步骤。
本步骤目的就是将收集到的图片进行一定的预处理,接下来介绍一下该步骤的几个函数。
def resize_image(image, height = ImageSize, width = ImageSize)
参数介绍:image指输入图片,height,width指最后输出图片的长宽,这里我们设置为64 x 64
函数目的:将图片填充为正方形,然后调整为64 x 64

def read_path(path_name)
参数介绍:path_name指文件路径(存放两个文件夹的文件夹)
函数目的:在做识别工作时,用的分类的方法,因此我们在制作数据时必须有图片和标签,而函数的目的就是将图片与标签一一对应,人脸识别问题中标签就是文件夹的名称(你自己的姓名和其他人),如下图

def load_dataset(path_name)
参数介绍:path_name指文件路径(存放两个文件夹的文件夹)
函数目的:在上面的函数中,我们让图片和标签一一对应了,但是对应的标签是一条路径对我们后续训练造成了不便,这里我们将路径为Hinton的标签设置为0,路径为other的标签设置为1,方便后面的代码编写。(其实就是二分类问题)

附上该步骤完整代码(命名为load_dataset)
import os
import random
import sys
import cv2
import numpy as np
from sklearn.model_selection import train_test_splitImageSize = 64def resize_image(image, height = ImageSize, width = ImageSize):top,bottom,left,right = (0,0,0,0)h, w, _ = image.shape#print(h, w, _)longest = max(h, w)#print(longest)if h < longest:dh = longest - htop = dh // 2bottom = dh - topelif w < longest:dw = longest - wleft = dw // 2right = dw - leftelse:passBlack = [0,0,0]#图片填充,top,botto,left,right指向上下左右补全长度,value指填充颜色constant = cv2.copyMakeBorder(image, top, bottom, left, right, cv2.BORDER_CONSTANT, value = [255,255,255])#cv2.imshow("img",constant)#cv2.waitKey(0)return cv2.resize(constant,(height,width))images = []
labels = []
def read_path(path_name):for dir_item in os.listdir(path_name):full_path = os.path.abspath(os.path.join(path_name, dir_item)) #返回绝对路径if os.path.isdir(full_path): #isdir()判断是否为文件夹,是文件夹的话继续递归调用read_path(full_path) #递归调用else:if dir_item.endswith('.jpg'):img = cv2.imread(full_path)img = resize_image(img,ImageSize,ImageSize)images.append(img) #文件夹下的所有图片labels.append(path_name) #标签为文件夹名字return images,labelsdef load_dataset(path_name):images, labels = read_path(path_name)images = np.array(images)#print(images.shape)labels = np.array([0 if label.endswith('Hinton') else 1 for label in labels])return images,labels#print(read_path("D:/desktop/face/"))
images,labels = load_dataset("D:/desktop/face/")
4.通过神经网络训练模型
在我们得到数据和标签后就可以训练模型了。这个步骤几个主要的操作就是划分数据集,搭建神经网络,然后进行训练,就可以得到一个模型了。
划分数据集

这里涉及到训练集,验证集,测试集的概念
| 类别 | 作用 |
|---|---|
| 训练集 | 用于拟合模型的数据样本 |
| 验证集 | 用于调整超参数 |
| 测试集 | 评估模型的泛化能力 |
一般来说,训练集和验证集划分为7比3,测试集为数据集的一半。这个步骤我们将上一步中收集到的人脸数据集进行了划分。并且对图片和标签进行了处理,图片就是用了归一化操作,使像素点间易比较;标记用了one-hot操作。
搭建神经网络

搭建一个很简单的神经网络,输入是数据集中的图片,输出是该图片对应的标签(即预测标签,用one-hot形式表示),该网络类似于LeNet5。

跑通效果:

训练结束后会出现一个模型

附上该步骤完整代码(文件名为face_train):
import randomimport numpy as np
from sklearn.model_selection import train_test_split
from keras.preprocessing.image import ImageDataGenerator
from keras.models import Sequential
from keras.layers import Dense, Dropout, Activation, Flatten
from keras.layers import Convolution2D, MaxPooling2D
from keras.optimizers import SGD
from keras.utils import np_utils
from keras.models import load_model
from keras import backend as Kfrom load_dataset import load_dataset, resize_image, ImageSizeclass Dataset:def __init__(self, path_name):#训练集self.train_images = Noneself.train_labels = None#验证集self.valid_images = Noneself.valid_labels = None#测试集self.test_images = None self.test_labels = None#数据集加载路径self.path_name = path_name#当前库采用的维度顺序self.input_shape = None#加载数据集并按照交叉验证的原则划分数据集并进行相关预处理工作def load(self, img_rows = ImageSize, img_cols = ImageSize, img_channels = 3, nb_classes = 2):#加载数据集到内存images, labels = load_dataset(self.path_name) #划分训练集和验证集,一般是7比3train_images, valid_images, train_labels, valid_labels = train_test_split(images, labels, test_size = 0.3, random_state = random.randint(0, 100))#划分测试集,一般是数据集的一半_, test_images, _, test_labels = train_test_split(images, labels, test_size = 0.5, random_state = random.randint(0, 100)) #当前的维度顺序如果为'channels_first',则输入图片数据时的顺序为:channels,rows,cols,否则:rows,cols,channels#这部分代码就是根据keras库要求的维度顺序重组训练数据集if K.image_data_format() == "channels_first":train_images = train_images.reshape(train_images.shape[0], img_channels, img_rows, img_cols)valid_images = valid_images.reshape(valid_images.shape[0], img_channels, img_rows, img_cols)test_images = test_images.reshape(test_images.shape[0], img_channels, img_rows, img_cols)self.input_shape = (img_channels, img_rows, img_cols) else:train_images = train_images.reshape(train_images.shape[0], img_rows, img_cols, img_channels)valid_images = valid_images.reshape(valid_images.shape[0], img_rows, img_cols, img_channels)test_images = test_images.reshape(test_images.shape[0], img_rows, img_cols, img_channels)self.input_shape = (img_rows, img_cols, img_channels) #输出训练集、验证集、测试集的数量print(train_images.shape[0], 'train samples')print(valid_images.shape[0], 'valid samples')print(test_images.shape[0], 'test samples')#我们的模型使用categorical_crossentropy作为损失函数,因此需要根据类别数量nb_classes将#类别标签进行one-hot编码使其向量化,在这里我们的类别只有两种,经过转化后标签数据变为二维train_labels = np_utils.to_categorical(train_labels, nb_classes) valid_labels = np_utils.to_categorical(valid_labels, nb_classes) test_labels = np_utils.to_categorical(test_labels, nb_classes) #像素数据浮点化以便归一化train_images = train_images.astype('float32') valid_images = valid_images.astype('float32')test_images = test_images.astype('float32')#将其归一化,图像的各像素值归一化到0~1区间train_images /= 255valid_images /= 255test_images /= 255 self.train_images = train_imagesself.valid_images = valid_imagesself.test_images = test_imagesself.train_labels = train_labelsself.valid_labels = valid_labelsself.test_labels = test_labels#CNN网络模型类
class Model:def __init__(self):self.model = None #建立模型def build_model(self, dataset, nb_classes = 2):#构建一个空的网络模型,它是一个线性堆叠模型,各神经网络层会被顺序添加self.model = Sequential() #以下代码将顺序添加CNN网络需要的各层,一个add就是一个网络层self.model.add(Convolution2D(32, 3, 3, border_mode='same', input_shape = dataset.input_shape)) #1 2维卷积层,输入dataset类的inputshapeself.model.add(Activation('relu')) #2 激活函数层self.model.add(Convolution2D(32, 3, 3)) #3 2维卷积层 self.model.add(Activation('relu')) #4 激活函数层self.model.add(MaxPooling2D(pool_size=(2, 2))) #5 池化层self.model.add(Dropout(0.25)) #6 Dropout层self.model.add(Convolution2D(64, 3, 3, border_mode='same')) #7 2维卷积层self.model.add(Activation('relu')) #8 激活函数层self.model.add(Convolution2D(64, 3, 3)) #9 2维卷积层self.model.add(Activation('relu')) #10 激活函数层self.model.add(MaxPooling2D(pool_size=(2, 2))) #11 池化层self.model.add(Dropout(0.25)) #12 Dropout层self.model.add(Flatten()) #13 Flatten层self.model.add(Dense(512)) #14 Dense层,又被称作全连接层self.model.add(Activation('relu')) #15 激活函数层 self.model.add(Dropout(0.5)) #16 Dropout层self.model.add(Dense(nb_classes)) #17 Dense层self.model.add(Activation('softmax')) #18 分类层,输出最终结果#输出模型概况self.model.summary()#训练模型def train(self, dataset, batch_size = 20, nb_epoch = 10, data_augmentation = True): sgd = SGD(lr = 0.01, decay = 1e-6, momentum = 0.9, nesterov = True) #采用SGD+momentum的优化器进行训练,首先生成一个优化器对象 self.model.compile(loss='categorical_crossentropy',optimizer=sgd,metrics=['accuracy']) #完成实际的模型配置工作#不使用数据提升,所谓的提升就是从我们提供的训练数据中利用旋转、翻转、加噪声等方法#训练数据,有意识的提升训练数据规模,增加模型训练量if not data_augmentation: self.model.fit(dataset.train_images,dataset.train_labels,batch_size = batch_size,nb_epoch = nb_epoch,validation_data = (dataset.valid_images, dataset.valid_labels),shuffle = True)#使用实时数据提升else: #定义数据生成器用于数据提升,其返回一个生成器对象datagen,datagen每被调用一#次其生成一组数据(顺序生成),节省内存,其实就是python的数据生成器datagen = ImageDataGenerator(featurewise_center = False, #是否使输入数据去中心化(均值为0),samplewise_center = False, #是否使输入数据的每个样本均值为0featurewise_std_normalization = False, #是否数据标准化(输入数据除以数据集的标准差)samplewise_std_normalization = False, #是否将每个样本数据除以自身的标准差zca_whitening = False, #是否对输入数据施以ZCA白化rotation_range = 20, #数据提升时图片随机转动的角度(范围为0~180)width_shift_range = 0.2, #数据提升时图片水平偏移的幅度(单位为图片宽度的占比,0~1之间的浮点数)height_shift_range = 0.2, #同上,只不过这里是垂直horizontal_flip = True, #是否进行随机水平翻转vertical_flip = False) #是否进行随机垂直翻转#计算整个训练样本集的数量以用于特征值归一化、ZCA白化等处理datagen.fit(dataset.train_images) #利用生成器开始训练模型self.model.fit_generator(datagen.flow(dataset.train_images, dataset.train_labels,batch_size = batch_size),samples_per_epoch = dataset.train_images.shape[0],nb_epoch = nb_epoch,validation_data = (dataset.valid_images, dataset.valid_labels)) MODEL_PATH = './Hinton.face.model.h5'def save_model(self, file_path = MODEL_PATH):self.model.save(file_path)def load_model(self, file_path = MODEL_PATH):self.model = load_model(file_path)def evaluate(self, dataset):score = self.model.evaluate(dataset.test_images, dataset.test_labels, verbose = 1)print("%s: %.2f%%" % (self.model.metrics_names[1], score[1] * 100))#识别人脸def face_predict(self, image): #依然是根据后端系统确定维度顺序if K.image_data_format() == "channels_first" and image.shape != (1, 3, ImageSize, ImageSize):image = resize_image(image) #尺寸必须与训练集一致都应该是IMAGE_SIZE x IMAGE_SIZEimage = image.reshape((1, 3, ImageSize, ImageSize)) #与模型训练不同,这次只是针对1张图片进行预测 elif K.image_data_format() == "channels_last" and image.shape != (1, ImageSize, ImageSize, 3):image = resize_image(image)image = image.reshape((1, ImageSize, ImageSize, 3)) #浮点并归一化image = image.astype('float32')image /= 255#给出输入属于各个类别的概率,我们是二值类别,则该函数会给出输入图像属于0和1的概率各为多少result = self.model.predict_proba(image)print('result:', result)#给出类别预测:0或者1result = self.model.predict_classes(image) #返回类别预测结果return result[0]if __name__ == '__main__':dataset = Dataset('D:/desktop/face/') dataset.load()model = Model()model.build_model(dataset)#先前添加的测试build_model()函数的代码model.build_model(dataset)#测试训练函数的代码model.train(dataset)if __name__ == '__main__':dataset = Dataset('D:/desktop/face/') dataset.load()model = Model()model.build_model(dataset)model.train(dataset)model.save_model(file_path = './model/Hinton.face.model.h5')if __name__ == '__main__': dataset = Dataset('D:/desktop/face/') dataset.load()#评估模型model = Model()model.load_model(file_path = './model/Hinton.face.model.h5')model.evaluate(dataset)
5.进行人脸识别
该步骤与收集人脸数据代码很相似,只是加上了一个预测过程(模型预测)。摄像头截取每一帧的图片,扔到模型中进行预测,预测出值为0则就是识别出录入的人,预测值为1即预测为未知。我们在第一步录入的是Hinton的人脸,下图展示效果。


附上该步骤的代码(文件名为face_recognition)
#-*- coding: utf-8 -*-import cv2
import sys
import gc
from face_train import Modelif __name__ == '__main__':if len(sys.argv) != 1:print("Usage:%s camera_id\r\n" % (sys.argv[0]))sys.exit(0)#加载模型model = Model()model.load_model(file_path = './model/Hinton.face.model.h5') #框住人脸的矩形边框颜色 color = (0, 255, 0)#捕获指定摄像头的实时视频流cap = cv2.VideoCapture(0)#人脸识别分类器本地存储路径cascade_path = "D:/anaconda/Lib/site-packages/cv2/data/haarcascade_frontalface_alt2.xml" #循环检测识别人脸while True:ret, frame = cap.read() #读取一帧视频if ret is True:#图像灰化,降低计算复杂度frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)else:continue#使用人脸识别分类器,读入分类器cascade = cv2.CascadeClassifier(cascade_path) #利用分类器识别出哪个区域为人脸faceRects = cascade.detectMultiScale(frame_gray, scaleFactor = 1.2, minNeighbors = 3, minSize = (32, 32)) if len(faceRects) > 0: for faceRect in faceRects: x, y, w, h = faceRect#截取脸部图像提交给模型识别这是谁image = frame[y - 10: y + h + 10, x - 10: x + w + 10]faceID = model.face_predict(image) #如果是“我”if faceID == 0: cv2.rectangle(frame, (x - 10, y - 10), (x + w + 10, y + h + 10), color, thickness = 2)#文字提示是谁cv2.putText(frame,'Hinton', (x + 30, y + 30), #坐标cv2.FONT_HERSHEY_SIMPLEX, #字体1, #字号(255,0,255), #颜色2) #字的线宽else:cv2.rectangle(frame, (x - 10, y - 10), (x + w + 10, y + h + 10), color, thickness = 2)cv2.putText(frame,'unknown', (x + 30, y + 30), #坐标cv2.FONT_HERSHEY_SIMPLEX, #字体1, #字号(255,0,255), #颜色2) #字的线宽cv2.imshow("Face_recognition", frame)#等待10毫秒看是否有按键输入k = cv2.waitKey(10)if k & 0xFF == ord(' '):break#释放摄像头并销毁所有窗口cap.release()cv2.destroyAllWindows()
6.总结
实现简单的人脸识别用到了四个.py文件,我用了Hinton教授的图片,大家可以用自己的人脸进行尝试。之前说了这个代码只是简单的二分类问题,但是可视化效果很直观所以吸人眼球,可以尝试设计成三分类,四分类问题,仅仅需要微调代码,但是再往上加意义就不大了。
简单人脸识别的实现对了解opencv的基础操作,图像识别实现的流程,包括机器学习,深度学习基础理论的应用起到了一个很好的总结作用,可以尝试复现。
若有不足,请多指正!!!















