圆栗子 编译整理 量子位 出品 | 公众号 QbitAI
△ 来自虾米妈咪
小朋友用妈妈的一寸照片通过了人脸识别,打击了小度音箱的家长监督机制。
活体检测没做好。
公交车身广告上的董明珠头像,被宁波交警系统拍了照,判定成“违法闯红灯”。
活体检测没做好。
所以,活体检测要怎么做?
名叫Adrian Rosebrock的程序猿,写了份事无巨细的教程,从构建数据集开始,一步步教大家用AI分辨真人和照片,精细到每行代码的用途。
△ 川川是假的,光头是真的
这个识别方法,用到了OpenCV和Keras,打开摄像头就可以实时检测。
重要的是,有源码提供,受到了推特用户的踊跃比心。
活体检测,可以检测些什么?
AI可以用哪些技巧,来区分真人和照片?
一是纹理分析 (Texture Analysis) 。皮肤的纹理特征是重要的依据,给2D照片拍照,比起给3D真人拍照,会损失一些纹理。 二是频率分析 (Frequency Analysis) 。照片脸部的频率组成,也不像真人那样丰富。 三是可变聚焦分析 (Variable focusing Analysis) 。连拍两张照片,聚焦在不同位置,查看像素值 (Pixel Value) 的变化。 四是启发式算法 (Heuristic-Based Algorithms) 。眼动、唇动、眨眼这些动作,照片是不会有的。 五是光流算法 (Optical Flow Algorithms) 。在相邻两帧之间,检测物体运动的方向和幅度,查出2D和3D物体之间的差别。 ……
不过这里,就把活体检测看成一个粗暴的二分类问题,这些复杂的分析先抛到一边。
自制数据集
程序猿把问题又简化了一下:这里说的“假脸”,只是“屏幕里的人脸”而已。
现在,可以开始造数据集了。
他用手机的前摄像头拍了一段25秒的视频;又举着手机、对着电脑摄像头,把视频播了一遍。
这样,一段真人视频,和一段“假脸”视频,就准备好了。
程序猿拍下的这两条视频,都提供下载。不过,他还是建议大家多收集一些数据,不同的人脸,甚至不同的人种,帮助算法茁壮成长。
下一步,要用OpenCV的人脸检测算法处理两段视频,把有用的区域 (ROI) ,就是人脸部分框出来。
这步用了80多行代码,每一行在做些什么,教程里都写清了。
最后,按照真假两个类别,把框好的脸部提取出来,就有了数据集。
温馨提示,需要平衡一下真图和假图的数量。比如,程序猿发现真实视频比假视频长,就用更大的间隔来提取。于是,他收获了真图161张,假图150张。
这数据集还是显得有些贫乏,所以后面需要扩增:旋转、翻转之类的操作,可以增加图片数量。
活体检测模型
数据集做好了,就要喂给做活体检测的神经网络。
程序猿给网络起名LivenessNet,大体长这样:
他说,这其实只是一个简单的CNN。而且,已经努力让网络保持在最浅、参数最少的状态。
这样做有两个原因:一是为避免模型在小数据集上发生过拟合,二是为保证模型快到可以实时推理,就算在树莓派上也能运行。
搭个网络
现在,就来实现一下这个网络。打开livenessnet.py,再写这一段代码:
代码语言:javascript复制 1# import the necessary packages
2from keras.models import Sequential
3from keras.layers.normalization import BatchNormalization
4from keras.layers.convolutional import Conv2D
5from keras.layers.convolutional import MaxPooling2D
6from keras.layers.core import Activation
7from keras.layers.core import Flatten
8from keras.layers.core import Dropout
9from keras.layers.core import Dense
10from keras import backend as K
11
12class LivenessNet:
13 @staticmethod
14 def build(width, height, depth, classes):
15 # initialize the model along with the input shape to be
16 # "channels last" and the channels dimension itself
17 model = Sequential()
18 inputShape = (height, width, depth)
19 chanDim = -1
20
21 # if we are using "channels first", update the input shape
22 # and channels dimension
23 if K.image_data_format() == "channels_first":
24 inputShape = (depth, height, width)
25 chanDim = 1
然后,一层一层加上去:
代码语言:javascript复制 1 # first CONV => RELU => CONV => RELU => POOL layer set
2 model.add(Conv2D(16, (3, 3), padding="same",
3 input_shape=inputShape))
4 model.add(Activation("relu"))
5 model.add(BatchNormalization(axis=chanDim))
6 model.add(Conv2D(16, (3, 3), padding="same"))
7 model.add(Activation("relu"))
8 model.add(BatchNormalization(axis=chanDim))
9 model.add(MaxPooling2D(pool_size=(2, 2)))
10 model.add(Dropout(0.25))
11
12 # second CONV => RELU => CONV => RELU => POOL layer set
13 model.add(Conv2D(32, (3, 3), padding="same"))
14 model.add(Activation("relu"))
15 model.add(BatchNormalization(axis=chanDim))
16 model.add(Conv2D(32, (3, 3), padding="same"))
17 model.add(Activation("relu"))
18 model.add(BatchNormalization(axis=chanDim))
19 model.add(MaxPooling2D(pool_size=(2, 2)))
20 model.add(Dropout(0.25))
程序猿说,这个网络有点像VGGNet,很浅,过滤器 (Filter) 很少。只是判断真假,不用深度网络。
最后,再加一个FC层→RELU层的组合。
代码语言:javascript复制 1 # first (and only) set of FC => RELU layers
2 model.add(Flatten())
3 model.add(Dense(64))
4 model.add(Activation("relu"))
5 model.add(BatchNormalization())
6 model.add(Dropout(0.5))
7
8 # softmax classifier
9 model.add(Dense(classes))
10 model.add(Activation("softmax"))
11
12 # return the constructed network architecture
13 return model
CNN搭好了,该训练了。
训练脚本长这样
大致的训练过程,就像这张图:
打开train_liveness.py,写下这段代码:
代码语言:javascript复制 1# set the matplotlib backend so figures can be saved in the background
2import matplotlib
3matplotlib.use("Agg")
4
5# import the necessary packages
6from pyimagesearch.livenessnet import LivenessNet
7from sklearn.preprocessing import LabelEncoder
8from sklearn.model_selection import train_test_split
9from sklearn.metrics import classification_report
10from keras.preprocessing.image import ImageDataGenerator
11from keras.optimizers import Adam
12from keras.utils import np_utils
13from imutils import paths
14import matplotlib.pyplot as plt
15import numpy as np
16import argparse
17import pickle
18import cv2
19import os
20
21# construct the argument parser and parse the arguments
22ap = argparse.ArgumentParser()
23ap.add_argument("-d", "--dataset", required=True,
24 help="path to input dataset")
25ap.add_argument("-m", "--model", type=str, required=True,
26 help="path to trained model")
27ap.add_argument("-l", "--le", type=str, required=True,
28 help="path to label encoder")
29ap.add_argument("-p", "--plot", type=str, default="plot.png",
30 help="path to output loss/accuracy plot")
31args = vars(ap.parse_args())
△ 导入,不停地导入
里面包含了许多的导入:
有matplotlib,是可视化工具; 有LivenessNet,就是刚才搭好的CNN; 有train_test_split,这是scikit-learn里的函数,把数据集拆成训练集和测试集; 有classification_report,也是scikit-learn里面的工具,用来生成简短统计报告的; 有ImageDataGenerator,做数据扩增用的; 有Adam,适合这个任务的优化器,当然也可以用SGD、RMSprop等等代替; 有paths,这个模块是用来收集图片路径的; 有pyplot,也是美丽的可视化工具; 有numpy,是Python数学库,也是OpenCV必需品; 有argparse,用来处理命令行参数; 有pickle,可以把标签编码器序列化到盘上; 有cv2,这是一组OpenCV Binding; 还有os,这个模块用处很多,但这里只用到了它的操作系统路径分隔符而已。
梳理好这些,再看余下的代码,就会清晰很多了。
后面,是一系列的初始化,以及训练前的各种准备活动。此处略去,详见教程原文。
训练正式启动
准备就绪,运行这段命令,就可以训练了:
代码语言:javascript复制 1$ python train.py --dataset dataset --model liveness.model --le le.pickle
2[INFO] loading images...
3[INFO] compiling model...
4[INFO] training network for 50 epochs...
5Epoch 1/50
629/29 [==============================] - 2s 58ms/step - loss: 1.0113 - acc: 0.5862 - val_loss: 0.4749 - val_acc: 0.7436
7Epoch 2/50
829/29 [==============================] - 1s 21ms/step - loss: 0.9418 - acc: 0.6127 - val_loss: 0.4436 - val_acc: 0.7949
9Epoch 3/50
1029/29 [==============================] - 1s 21ms/step - loss: 0.8926 - acc: 0.6472 - val_loss: 0.3837 - val_acc: 0.8077
11...
12Epoch 48/50
1329/29 [==============================] - 1s 21ms/step - loss: 0.2796 - acc: 0.9094 - val_loss: 0.0299 - val_acc: 1.0000
14Epoch 49/50
1529/29 [==============================] - 1s 21ms/step - loss: 0.3733 - acc: 0.8792 - val_loss: 0.0346 - val_acc: 0.9872
16Epoch 50/50
1729/29 [==============================] - 1s 21ms/step - loss: 0.2660 - acc: 0.9008 - val_loss: 0.0322 - val_acc: 0.9872
18[INFO] evaluating network...
19 precision recall f1-score support
20
21 fake 0.97 1.00 0.99 35
22 real 1.00 0.98 0.99 43
23
24 micro avg 0.99 0.99 0.99 78
25 macro avg 0.99 0.99 0.99 78
26weighted avg 0.99 0.99 0.99 78
27
28[INFO] serializing network to 'liveness.model'...
成果斐然
训练完结,LivenessNet在验证集上拿到了99%的准确度。
当然,验证集只是热身,后面还要打开摄像头,让AI去看更多没见过的人,和没见过的“假人”。
(这一部分代码,也有详尽的解读,参见教程原文。)
就像开头展示的那样,AI能清楚地判断,它眼前的川川不是真人,而程序猿是真人。
那么,你也可以训练一只这样的AI了。
不过,不用局限于简单的二分类,可以用上前面讲到的那些复杂的分析方法,比如频率分析,比如光流法,大有可为。
教程原文传送门: https://www.pyimagesearch.com/2019/03/11/liveness-detection-with-opencv/
源码下载入口,和无微不至的代码解析,都在里面了。
— 完 —