基于OpenCV和Keras的人脸识别系列手记:
项目完整代码参见Github仓库。
本篇手记是上面这一系列的第三篇。原本发表在慕课网上,不知何故看不到了,现重新发在这里,给需要的人。
有了之前两篇手记:Opencv初接触,图片的基本操作和使用OpenCV通过摄像头捕获实时视频并探测人脸、准备人脸数据的基础,这篇手记我们要对采集的数据进行预处理。
之前已经准备好了训练模型需要用到的人脸图片,接下来的思路大致如下:
这篇手记讲解数据预处理部分。
前面我们是用OpenCV自带的人脸检测器提取的人脸,由于这个人脸检测器准确度有限,得到的人脸图片会有一些错误,我自己在检查的时候发现图片中有眼睛,门,手之类等等一些奇怪的图片也被保存了下来,如果这些图片不删掉就会导致训练出的模型有误差,所以在下一步处理数据之前就要手工检查数据,当然,这里的数据量不大(我一共收集了我和我女朋友的照片各一千张左右),所以手工检查数据的工作量也不大。
接着我们要用OpenCV统一图片尺寸大小以便输入到卷积神经网络中,我们在工作目录下建立一个load_face_dataset.py
文件,定义一个resize_image
函数来调整图片尺寸:
import os
import numpy as np
import cv2
IMAGE_SIZE = 64 # 指定图像大小
# 按指定图像大小调整尺寸
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)
# 计算短边需要增加多少像素宽度才能与长边等长(相当于padding,长边的padding为0,短边才会有padding)
if h < longest_edge:
dh = longest_edge - h
top = dh // 2
bottom = dh - top
elif w < longest_edge:
dw = longest_edge - w
left = dw // 2
right = dw - left
else:
pass # pass是空语句,是为了保持程序结构的完整性。pass不做任何事情,一般用做占位语句。
# RGB颜色
BLACK = [0,0,0]
# 给图片增加padding,使图片长、宽相等
# top, bottom, left, right分别是各个边界的宽度,cv2.BORDER_CONSTANT是一种border type,表示用相同的颜色填充
constant = cv2.copyMakeBorder(image, top, bottom, left, right, cv2.BORDER_CONSTANT, value = BLACK)
# 调整图像大小并返回图像,目的是减少计算量和内存占用,提升训练速度
return cv2.resize(constant, (height, width))
然后将图片读取到Python列表中以便用Python对其进一步处理,并对数据进行标注,“我”标注为0,其他人标注为“1”。在load_face_dataset.py
里接着定义一个read_path
函数从指定路径中读取图片到Python list,然后再定义一个load_dataset
函数调用read_path
函数读取图片并转化为更适用于机器学习的numpy多维数组,然后通过判断图片路径名的区别将图片数据标注为0和1:
# 读取训练数据到内存,这里数据结构是列表
images = []
labels = []
# path_name是当前工作目录,后面会由os.getcwd()获得
def read_path(path_name):
for dir_item in os.listdir(path_name): # os.listdir() 方法用于返回指定的文件夹包含的文件或文件夹的名字的列表
# 从当前工作目录寻找训练集图片的文件夹
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)
if image is None: # 遇到部分数据有点问题,报错'NoneType' object has no attribute 'shape'
pass
else:
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)
# 将lsit转换为numpy array
images = np.array(images, dtype='float') # 注意这里要将数据类型设为float,否则后面face_train_keras.py里图像归一化的时候会报错,TypeError: No loop matching the specified signature and casting was found for ufunc true_divide
# print(images.shape) # (1969, 64, 64, 3)
# 标注数据,me文件夹下是我,指定为0,其他指定为1,这里的0和1不是logistic regression二分类输出下的0和1,而是softmax下的多分类的类别
labels = np.array([0 if label.endswith('me') else 1 for label in labels])
return images, labels
上述代码中值得注意的一点是由于OpenCV自带的人脸探测算法有Bug,探测得到的人脸图片有的大小是0KB,这时图片就会报错,因此可以手动将得到的人脸数据图片中0KB的文件删掉,为了程序更鲁棒,这里加一个判断,只有当人脸文件不为空的时候才进行resize操作,类似的问题在后面进行人脸识别的时候也要注意。
标注好了图片后还需要对数据集进一步处理以用于深度学习算法。包括:
face_train_keras.py
文件,首先定义一个Dataset
类来完成上述五项数据处理操作:import random
import keras
import 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, Conv2D, MaxPooling2D
from keras.optimizers import SGD
from keras.models import load_model
from keras import backend as K
from load_face_dataset import load_dataset, resize_image, IMAGE_SIZE
import cv2
'''
对数据集的处理,包括:
1、加载数据集
2、将数据集分为训练集和测试集
3、根据Keras后端张量操作引擎的不同调整数据维度顺序
4、对数据集中的标签进行One-hot编码
5、数据归一化
'''
class Dataset:
# http://www.runoob.com/python3/python3-class.html
# 很多类都倾向于将对象创建为有初始状态的。
# 因此类可能会定义一个名为 __init__() 的特殊方法(构造方法),类定义了 __init__() 方法的话,类的实例化操作会自动调用 __init__() 方法。
# __init__() 方法可以有参数,参数通过 __init__() 传递到类的实例化操作上,比如下面的参数path_name。
# 类的方法与普通的函数只有一个特别的区别——它们必须有一个额外的第一个参数名称, 按照惯例它的名称是 self。
# self 代表的是类的实例,代表当前对象的地址,而 self.class 则指向类。
def __init__(self, path_name):
# 训练集
self.train_images = None
self.train_labels = None
# 测试集
self.test_images = None
self.test_labels = None
# 数据集加载路径
self.path_name = path_name
# 当前库采用的维度顺序,包括rows,cols,channels,用于后续卷积神经网络模型中第一层卷积层的input_shape参数
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)
# 注意下面数据集的划分是随机的,所以每次运行程序的训练结果会不一样
train_images, test_images, train_labels, test_labels = train_test_split(images, labels, test_size = 0.3, random_state = random.randint(0, 100))
# print(test_labels) # 确认了每次都不一样
# tensorflow 作为后端,数据格式约定是channel_last,与这里数据本身的格式相符,如果是channel_first,就要对数据维度顺序进行一下调整
if K.image_data_format == 'channel_first':
train_images = train_images.reshape(train_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)
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(test_images.shape[0], 'test samples')
# 后面模型中会使用categorical_crossentropy作为损失函数,这里要对类别标签进行One-hot编码
train_labels = keras.utils.to_categorical(train_labels, nb_classes)
test_labels = keras.utils.to_categorical(test_labels,nb_classes)
# 图像归一化,将图像的各像素值归一化到0~1区间。
train_images /= 255
test_images /= 255
self.train_images = train_images
self.test_images = test_images
self.train_labels = train_labels
self.test_labels = test_labels
在上面的一段代码里,加载完数据后我首先是用scikit-learn将数据集划分为训练集(用于训练模型)和测试集(用于评估训练好的模型的效果),scikit-learn是一个基于Python的传统机器学习库,能和Numpy交互使用,这里用其中的train_test_split
函数,把数据集按70%/30%的比例划分为训练集和测试集,需要注意的是这个函数默认会把数据集随机打散,这样训练集和测试集的数据分布就更加相近。
划分好数据集以后还要考虑一个Keras后端所遵循的图片数据格式的问题(关于Keras后端的概念可以参看我的另一篇手记Tensorflow池化操作中的padding细节探究),当Keras后端引擎是Tensorflow时,后端的image_data_format
属性是channel_last
,也就是说图片数据的维度格式是(rows, cols, channels)
,如果后端引擎是Theano的话,image_data_format
属性是channel_first
,图片数据的维度格式是(channels, rows, cols)
,因此,为了使程序更健壮,我们要通过判断image_data_format
属性的值来调整图片数据集的维度顺序。
接着,我们要对数据集的标签进行One-hot编码,又叫独热编码或者一位有效编码。独热编码是使用N位(有几个类别就有几位)状态寄存器对N个状态编码,每个状态都有它独立的寄存器位,并且在任意时候,其中只有一位有效。
举个例子:假如数据标签有四个类别,如果用数字表示是0, 1, 2, 3,那么用独热编码就应该表示为:[1 0 0 0], [0 1 0 0], [0 0 1 0]和[0 0 0 1]四个向量。在Keras中,对于表示为数字的标签,可以用to_categorical
方法进行独热编码。
最后一项处理是对图片数据进行归一化。在深度学习中,对数据进行归一化是为了将特征值尺度调整到相近的范围,如果不归一化,尺度大的特征值,梯度也比较大,尺度小的特征值,梯度也比较小,而梯度更新时的学习率是一样的,如果学习率小,梯度小的就更新慢,如果学习率大,梯度大的方向不稳定,不易收敛,通常需要使用最小的学习率迁就大尺度的维度才能保证损失函数有效下降,因此,通过归一化,把不同维度的特征值范围调整到相近的范围内,就能统一使用较大的学习率加速学习。因为图片像素值的范围都在0~255,图片数据的归一化可以简单地除以255。
图片数据集预处理操作:
本作品采用知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议进行许可。要查看该许可协议,可访问 http://creativecommons.org/licenses/by-nc-sa/4.0/ 或者写信到 Creative Commons, PO Box 1866, Mountain View, CA 94042, USA。