在本篇文章使用了Tensorflow来实现人脸识别,并在小数量数据集上表现级好,是入门Tensorflow图像识别分类问题的一个很棒的实践例子。
PS:这是我寒假的时候用来练手做的一个小项目,当时忘记写这个项目了。结果到学校之后就专心投入了服务外包的怀抱,有一个多月没有写过文章。说了这么多…我的意思是,咕咕咕。
PPS:因为某些不可知的原因,这个项目几乎所有使用到的存储路径都是绝对路径,所以在使用代码的时候一定要修改这些路径,换成你的路径。
这篇文章大概分为四大模块,分别是 “采集数据集”, “数据预处理” ,“训练数据并保存模型”,“使用模型进行预测”。
采集数据集
使用工具:
Python3.6
主要用到的模块:
cv2(Opencv的python版本)、PIL
首先第一步肯定是采集数据集了,网上有很多优秀的人脸数据集,但是有些不方便,不管是被墙无法下载还是数据集质量低等问题。所以,在这里我使用自己收集的数据集进行训练,一方面是可以稍微入门学一下Opencv怎么使用,另一方面,因为这本来就是一个神经网络中传统的分类问题,自己的数据集使用起来更加顺手。
因为在做这个项目的时候比较用心,所以就写了很多注释,不懂的东西也都上网查了,所以就不再分代码片介绍了,下面是采集数据集所使用的代码:
'''
created on February 2 13:34 2019
file name:get_face_img.py
@author:lhy
'''
import cv2
import sys
import os
from PIL import Image
def CatchFaceWithCamera(window_name, cemera_index, catch_pic_num, path_name):
'''
:param window_name:创建opencv界面的名字
:param cemera_index: 打开并使用哪个camera
:param catch_pic_num: 一共要拍多少张照片,超过这个限制程序会停止
:param path_name: 图片存储的路径
:return: None
'''
# 打开窗口
cv2.namedWindow(window_name)
# 输入视频,也可是摄像头的实时录像
captrue = cv2.VideoCapture(cemera_index)
# 加载人脸识别分类器,这些分类器可以到opencv2官网下载
classfier = cv2.CascadeClassifier("E:\\Tensorflow\\face_reco\\haarcascades\\haarcascade_frontalface_alt2.xml")
# 边框颜色,RGB格式
color = (255, 0, 0)
img_num = 0
#开始循环记录每一帧图像
while captrue.isOpened():
# 每一帧读取数据
have_content, frame = captrue.read()
if not have_content:
break
# 将当前帧转化成灰度图
grey_img = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 人脸检测,缩放比例是1.2,检测的有效点数是3
'''
classfier.detectMultiScale()函数参数
scaleFactor:图像缩放比例,可以理解为同一个物体与相机距离不同,其大小亦不同,必须将其缩放到一定大小才方便识别,该参数指定每次缩放的比例。每次缩小的比例
minNeighbors:匹配成功所需要的周围矩形框的数目,每一个特征匹配到的区域都是一个矩形框,只有多个矩形框同时存在的时候,才认为是匹配成功,比如人脸,这个默认值是3。
minSize:目标区域的最小值。
'''
faceRects = classfier.detectMultiScale(grey_img, scaleFactor=1.2, minNeighbors=4, minSize=(32, 32))
if len(faceRects) > 0: # 检测到有人脸
for faceRect in faceRects:
x, y, w, h = faceRect
# 保存当前帧到本地
img_name = path_name + '\\' + str(img_num) + '.jpg'
image = frame[y :y + h , x :x + w ]
cv2.imwrite(img_name, image)
img_num += 1
if img_num > catch_pic_num:
break
# 单独框出每一张人脸
cv2.rectangle(frame, (x , y ), (x + w , y + h ), color, 2)
# 显示已经捕捉到多少张人脸
# 照片/添加的文字/左上角坐标/字体/字体大小/颜色/字体粗细
cv2.putText(frame, 'num:%d' % (img_num), (x + 30, y + 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 4)
# 超过指定的最大保存数量跳出循环
if img_num > catch_pic_num:
break
# 显示图像
cv2.imshow(window_name, frame)
# waitKey()--这个函数是在一个给定的时间内(单位ms)等待用户按键触发;如果用户没有按下键,则接续等待(循环),将用户输入的键转化成int值
c = cv2.waitKey(10)
# ord()函数返回参数的ASCII数值,或者Unicode数值,一个字符与(&)上0xFF表示的是byte类型的数字转化成int类型
if c & 0xFF == ord('q'):
break
# 释放摄像头的使用,销毁所有窗口
captrue.release()
cv2.destroyAllWindows()
if __name__ == '__main__':
#当传入有命令行参数时,则是考虑到使用外接摄像头的因素,而且命令行参数应该是将使用摄像头的ID
#我的笔记本自带摄像头的ID是0,如果使用外接摄像头,则使用外接摄像头进行数据收集
if len(sys.argv) != 1:
print("You are using camera %s" % (sys.argv[1]))
CatchFaceWithCamera("get face images", sys.argv[1],1000,"E:\\Tensorflow\\face_reco\\data\\zgh")
else:
#记得每次收集不同人的数据时改变图片存储路径"E:\\Tensorflow\\face_reco\\data\\zgh"
CatchFaceWithCamera("get face images", 0, 1000, "E:\\Tensorflow\\face_reco\\data\\zgh")
可以多找几个人录人脸数据,这样一来,数据就收集完毕了。接下来进行数据预处理。
数据处理
在数据处理模块,因为本来就是入门级的项目,所以就将数据和对应的标签直接存在numpy数组中了,如果数据集过大,肯定会造成numpy数组溢出的情况,但是在这里数据集比较小(寒假收集不到那么多数据,只收集了两个人的),就暂时不担心这个问题了,主要思想是这样的:
1、遍历文件夹,读出每张图片的路径
2、对每张图片进行一定的变化,通过添加黑色像素的方式补全图片成为正方形,并resize成64x64的形状,存入数组。
3、通过不同人物的文件夹名字不同,设置相应的labels,存入数组。
完整数据处理代码如下:
'''
created on February 2 15:46 2019
filename:load_dataset.py
@author:lhy
'''
import os
import numpy as np
import cv2
import sys
IMAGE_SIZE = 64
#读取训练数据
images=[]
labels=[]
# 按照制定大小调整尺寸
def resize_image(image, height=IMAGE_SIZE, width=IMAGE_SIZE):
top, bottom, left, right = (0, 0, 0, 0)
# 获得图像尺寸
h, w, _ = image.shape
# 对于长宽不相等的图片找到最短的那一边
longest_edge = max(h, w)
# 计算短的那一边加上对应的像素宽度构成正方形
if h < longest_edge:
sh = longest_edge - h
top = sh // 2
bottom = sh - top
elif w < longest_edge:
dw=longest_edge-w
left=dw//2
right=dw-left
BLACK=[0,0,0]
#给图像增加边界
constant=cv2.copyMakeBorder(image,top,bottom,left,right,cv2.BORDER_CONSTANT,value=BLACK)
return cv2.resize(constant,(height,width))
def read_path(path_name):
#os.listdir() 方法用于返回指定的文件夹包含的文件或文件夹的名字的列表。
for dir_item in os.listdir(path_name):
#得到完整的操作路径,abspath()返回path规范化的绝对路径。
full_path=os.path.abspath(os.path.join(path_name,dir_item))
if os.path.isdir(full_path):#如果读取的是文件夹,则递归调用
read_path(full_path)
else:
if dir_item.endswith('.jpg'):
image=cv2.imread(full_path)
image=resize_image(image,IMAGE_SIZE,IMAGE_SIZE)
images.append(image)
labels.append(path_name)
return images,labels
#从指定路径读取训练数据
def load_dataset(path_name):
images,labels=read_path(path_name)
#将所有输入的图片转化成四维数组
images=np.array(images)
print(images.shape)
#数据标注,由于数据只有两个人的原因,则设置一个文件夹的脸部图像label=0,另外一个文件夹的label=1
#当数据量增多的时候,可以增加label的数量
labels=np.array([0 if label.endswith('lhy') else 1 for label in labels])
return images,labels
if __name__ == '__main__':
images,labels=load_dataset("E:\\Tensorflow\\face_reco\\data")
经过上面数据处理过程,所有图片和标签都已经确定,接下来开始训练数据。
训练数据并保存模型
在这里使用了Tensorflow的Keras的序贯模型来搭建神经网络,使得着整个过程更加方便快捷。其中,使用sklearn来按照一定比例划分出训练集、验证集和测试集。由于Keras可以在Tensorflow环境中使用,也可以在Theano环境中使用。但是这里后端使用的是Tensorflow,Tensorflow对应的数据格式是 ’tf‘ 格式,所以在对图片进行一定处理(比如说旋转,镜像、白化、归一化等操作)过程之前,我们要将图片reshape成[batch_size,x,w,channels](tf数据格式的特点:图像通道在最后面)的样子。代码中注释也很齐全,基本可以无障碍看:
'''
created on February 2 16:48 2019
filename:face_train.py
@author:lhy
'''
import numpy as np
import random
from sklearn.cross_validation 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 K
from load_dataset import load_dataset, resize_image, IMAGE_SIZE
class Dataset:
def __init__(self, path_name):
# 训练集
self.train_images = None
self.train_labels = None
# 验证集
self.valid_images = None
self.valid_labels = None
# 测试集
self.test_images = None
self.test_labels = None
# 数据集加载路径
self.path_name = path_name
# 当前库采用的维度顺序,用来区分tensoeflow和Theano
self.input_shape = None
# 加载数据集,并按照交叉验证的原则划分数据集等工作
def load(self, img_rows=IMAGE_SIZE, img_cols=IMAGE_SIZE, img_channels=3, nb_classes=2):
# 加载数据集
images, labels = load_dataset(self.path_name)
# 从images和labels中选取百分之30的作为验证集,其余百分之70作为训练集
train_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))
# 判断当前使用的后端是tensorflow还是Theano or CNTK
# 如果是tensorflow,则喂入的数据是channels_last(tf)类型,这种类型的shape一般是[batch_size,w,h,channels],通道数在最后一个维度
# 如果是Theano或者CNTK,则喂入的数据是channels_first(th)类型,这种类型的shape一般是[batch_size,channels,w,h],通道数在前面
if K.image_dim_ordering() == 'th':
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_cols, 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')
# 归一化
train_images /= 255
valid_images /= 255
test_images /= 255
self.train_images = train_images
self.valid_images = valid_images
self.test_images = test_images
self.train_labels = train_labels
self.valid_labels = valid_labels
self.test_labels = test_labels
# 搭建cnn网络
class Model:
def __int__(self):
self.model = None
# 建立模型
def build_model(self, dataset, nb_classes=2):
# 选择使用Sequntial序贯模型,是函数式模型的一种特殊情况
self.model = Sequential()
# 进行前向传播的网络层的搭建
# 二维卷积层,卷积核使用3*3,核个数32,使用全0填充
self.model.add(Convolution2D(32, 3, 3, border_mode='same', input_shape=dataset.input_shape))
# 激活函数relu
self.model.add(Activation('relu'))
self.model.add(Convolution2D(32, 3, 3))
self.model.add(Activation('relu'))
self.model.add(MaxPooling2D(pool_size=(2, 2)))
self.model.add(Dropout(0.25))
self.model.add(Convolution2D(64, 3, 3, border_mode='same'))
self.model.add(Activation('relu'))
self.model.add(Convolution2D(64, 3, 3))
self.model.add(Activation('relu'))
self.model.add(MaxPooling2D(pool_size=(2, 2)))
self.model.add(Dropout(0.25))
# 将数据降维
self.model.add(Flatten())
# 全连接
self.model.add(Dense(512))
self.model.add(Activation('relu'))
self.model.add(Dropout(0.5))
self.model.add(Dense(nb_classes))
self.model.add(Activation('softmax'))
# 输出模型概况
self.model.summary()
# 进行训练
def train(self, dataset, batch_size=40, nb_epoch=5, data_augmentation=True):
# 使用梯队下降和momentum优化器生成一个优化器对象
sgd = SGD(lr=0.01, decay=1e-6, momentum=0.9, nesterov=True)
# 完成模型配置工作
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每次被调用一次就生成一组数batch_size的数据(顺序生成),节省内存,就是python的数据生成器
datagen = ImageDataGenerator(featurewise_center=False,
samplewise_center=False,
featurewise_std_normalization=False,
samplewise_std_normalization=False,
zca_whitening=False,
rotation_range=20,
width_shift_range=0.1,
height_shift_range=0.1,
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 = 'E:\\Tensorflow\\face_reco\\model\\face.reco.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):
# 根据后端系统确定维度顺序(我自己用的tensoeflow我还判断个啥啊
if K.image_dim_ordering() == 'th' and image.shape != (1, 3, IMAGE_SIZE, IMAGE_SIZE):
image = resize_image(image)
image = image.reshape((1, 3, IMAGE_SIZE.IMAGE_SIZE))
elif K.image_dim_ordering() == 'tf' and image.shape != (1, IMAGE_SIZE, IMAGE_SIZE, 3):
image = resize_image(image)
image = image.reshape((1, IMAGE_SIZE, IMAGE_SIZE, 3))
# 浮点归一化
image = image.astype('float32')
image /= 255
# 给出输入属于各个类别的概率,我们是二值类别,则该函数会给出输入图像属于0和1的概率分别是多少
result = self.model.predict_proba(image)
print('result', result)
# 给出类别预测:0或者1
result = self.model.predict_classes(image)
return result[0]
if __name__=='__main__':
dataset=Dataset('E:\\Tensorflow\\face_reco\\data')
dataset.load()
model=Model()
model.build_model(dataset)
model.train(dataset,data_augmentation=False)
model.save_model(file_path='E:\\Tensorflow\\face_reco\\model\\face.reco.h5')
这里需要注意的一点是,如果你的数据集太小,少跑一些epoch,千万不要让模型达到过拟合。
使用模型进行预测
这个过程你可以直接输入一张人脸图片来辨别这张图片是谁,当然,也可以使用opencv对视频的每一帧进行人脸检测。以下代码就是第二种情况:
'''
created on February 4 1:42 2019
filename:face_recognition.py
@author:lhy
'''
import cv2
import sys
import gc
from face_train import Model
if __name__ == '__main__':
# 加载模型
model = Model()
model.load_model(file_path='E:\\Tensorflow\\face_reco\\model\\face.reco.h5')
# 人脸边框颜色
color = (255, 0, 0)
# 捕获摄像头实时数据流
cap = cv2.VideoCapture(0)
# 加载人脸识别分类器
cascade_path = 'E:\\Tensorflow\\face_reco\\haarcascades\\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)
# 如果是lhy,就在实时框出的人脸上方写上lhy的名字
if faceID == 0:
cv2.rectangle(frame, (x - 10, y - 10), (x + w + 10, y + h + 10), color, thickness=2)
# 写上文字
cv2.putText(frame, 'Lhy',
(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, 'cx',
(x + 30, y + 30), # 坐标
cv2.FONT_HERSHEY_SIMPLEX, # 字体
1, # 字号
(255, 0, 255), # 颜色
2) # 线宽
cv2.imshow("face_reco", frame)
# 等待10毫秒是否有按键键入
k = cv2.waitKey(10)
# 如果输入q退出循环
if k & 0xFF == ord('q'):
break
# 释放摄像头和窗口
cap.release()
cv2.destroyAllWindows()
到这里所有代码已经结束了,怎么样,有没有成功?虽然这只是一个入门Tensorflow的例子,但是却涉及了很多方面的知识,比如说图像数据处理,Keras的使用、opencv的使用等等,希望你能从中学到一些知识。
突然想起来,Github上有一个人脸识别项目做的蛮不错,叫做facenet,感兴趣的可以去Github上看看,大体思想是用神经网络提取特征,然后直接学习图像到欧式空间上点的映射,两张图像所对应的特征的欧式空间上的点的距离直接对应着两个图像是否相似。
上图一个简单的示例,其中图中的数字表示这图像特征之间的欧式距离,可以看到,图像的类内距离明显的小于类间距离,阈值大约为1.1左右。
具体地址:https://github.com/davidsandberg/facenet