下述的大多数方法来自 FaceNet,也叫做DeepFace.
脸部识别
一般来说,脸部识别问题可以分为两类:
- 脸部验证(Face Verification):”这是某个人吗?” 通过提供的输入的数据来识别是否是某个确定的人。比如机场系统扫描你的护照来确认你是否是正确的持有人,比如移动手机通过识别你的脸部确认你是拥有者从而解锁。总的来说,这是一个1对1匹配的问题。
- 脸部识别(Face Recognition):”这是谁?” 通过提供的输入识别来识别对应的人是谁。比如公司的脸部识别打卡,通过识别脸部直接完成对应人员的打卡。总的来说,这是一个1对K的匹配问题。
FaceNet 通过一个神经网络先将输入的脸部照片解码为一个128维向量,通过比较2个这样的向量,从而判断这两张图片是否是一个人。将输入数据与数据库中所有人员的照片进行对比,从而找到”这是谁”。
我们将用TensorFlow来实现这个程序:(tensorflow 1.X)
- 1.实现三元损失函数
- 2.用一个预训练的模型来将脸部图片解码为128维的向量
- 3.使用这些代码来进行脸部验证和脸部识别
另外,我们使用通道优先(channels-first)。 也就是说对于输入数据的维度表示,我们使用(m,nC,nH,nW), 而不是(m,nH,nW,nC)。当然,channels-first 和 channels-last都有各自的理由,至今社区也没有一个统一的标准。
导入包
先导入包
from keras.models import Sequential
from keras.layers import Conv2D, ZeroPadding2D, Activation, Input, concatenate
from keras.models import Model
from keras.layers.normalization import BatchNormalization
from keras.layers.pooling import MaxPooling2D, AveragePooling2D
from keras.layers.merge import Concatenate
from keras.layers.core import Lambda, Flatten, Dense
from keras.initializers import glorot_uniform
from keras.engine.topology import Layer
from keras import backend as K
K.set_image_data_format('channels_first')
import cv2
import os
import numpy as np
from numpy import genfromtxt
import pandas as pd
import tensorflow as tf
from fr_utils import *
from inception_blocks_v2 import *
np.set_printoptions(threshold=np.nan)
0.简陋的脸部识别
在脸部验证中,你需要确定给到的两张图片是否是一个人。最简单的方法就是直接将两张图片一个像素一个像素的进行比较。如果两张图片间的间距小于某个阈值,他们可能就是一个人。
不难想到这个算法的表现会很差。因为光线的变化、人物脸部方向、甚至是头部位置的微小变化,都会导致像素值的改变。
与其使用原图片,我们可以使用编码后的图片数据。即f(img)。
将图片编码后的数据进行元素级的比较,我们可以得到一个更加准确的关于脸部验证的结果。
1.将脸部图片编码为128维的向量
1.1 使用卷积网络来进行编码
FaceNet模型需要使用非常多的数据和很长的时间来进行训练。这里我们跳过这个步骤,直接载入别人已经训练好的权重。 网络结构采用了 Szegedy等人文中的inception模型。 我们使用一个已经实现好的inception network的实现(在inception_blocks_v2.py中,略)。
几个需要知道的知识点:
- 这个网络采用96×96维度的RGB图像作为输入。特别地,输入一个脸部照片(或者多批次的m照片组)作为张量,形状为:(m,nC,nH,nW) = (m,3,96,96)
- 它将输出一个(m,128)的矩阵,即将每一张图片都编码为128维的向量。
FRmodel = faceRecoModel(input_shape=(3, 96, 96))
print("Total Params:", FRmodel.count_params())
输出:Total Params: 3743280
通过使用一个128元的全连接层作为它的最后一层,这就保证了模型将输出128维的向量。接着,使用这两个向量进行两张图片的比较:
如果编码符合以下判别标准,则是一个好的编码:
- 对同一个人的不同图片的编码较为相似
- 对不同人的图片的编码差异较大
三元损失函数将以上标准公式化了,并且试图将相同人的图片的编码缩小,将不同人图片的编码拉大。
1.2 三元损失函数对于一张图片x,我们声明它的编码为f(x), f是由神经网络计算的方法。
三元损失函数的训练需要用到三元组数据,每个三元组包含三张图片(A,P,N)
- A 是一张锚图片 - 某人的头部图像
- P 是一张”正”图片 - 与A图片中是同一个人物
- N 是一张”负”图片 - 与A图片中不是同一个人物
我们用(A(i),P(i),N(i))(都是上标)来声明第i个训练样本。
我们希望图A(i)与P(i)的距离至少比A(i)和N(i)的距离近至少一个α的值:
那么总的损失函数就是:
[z]+表示 max(z,0)。表示一旦A与P距离和A与N距离达到我们的要求,损失就为0,否则它的值就是损失值。
Notes:
- 公式中的第一个部分是锚图片A和正图片P的距离,你希望它尽可能的小
- 公式的第二个部分则是锚图片A和负图片N的距离,你希望它相对较大
- α叫做边距(margin),这是一个人为选择的超参数,我们使用 α = 0.2
大多数的实现里会将编码后的向量进行一次L2归一化,这里我们不用担心~
实现上面公式中的三元损失函数,需要4个步骤:
- 计算锚图片A和正图片P间的距离
- 计算锚图片A和负图片N间的距离
- 对每个三元组样本进行公式计算
- 将每组样本经步骤3得到的值与0取max并取和
PS:
def triplet_loss(y_true, y_pred, alpha = 0.2):
"""
三元损失函数的实现
Arguments:
y_true -- true 标签, 当你在keras中定义loss时需要, 在这个方法中你不需要它.
y_pred -- python list 包含三个对象:
anchor -- 锚图片编码后的结果, 形状为 (None, 128)
positive -- 正图片编码后的结果, 形状为(None, 128)
negative -- 负图片编码后的结果, 形状为 (None, 128)
Returns:
loss -- 数字, 损失值
"""
anchor, positive, negative = y_pred[0], y_pred[1], y_pred[2]
# Step 1
pos_dist = tf.reduce_sum(tf.square(tf.subtract(anchor,positive)),-1)
# Step 2
neg_dist = tf.reduce_sum(tf.square(tf.subtract(anchor,negative)),-1)
# Step 3
basic_loss = tf.maximum(tf.add(tf.subtract(pos_dist,neg_dist),alpha),0)
# Step 4
loss = tf.reduce_sum(basic_loss)
return loss
2.载入预训练模型
FaceNet通过最小化三元损失函数来进行训练。但训练需要大量的数据和计算时间,这里我们就不从头训练了。我们直接读取一个预训练的模型。用下面的代码读取来读取一个模型:
FRmodel.compile(optimizer = 'adam', loss = triplet_loss, metrics = ['accuracy'])
load_weights_from_FaceNet(FRmodel)
3.应用模型
假定我们构建的这个系统是一个门禁系统,用于给某公司利用脸部识别来确定是否允许某人进入公司建筑。
要通过门禁,每个人要先在入口处刷门禁卡,脸部识别系统会识别他们是否是他们所声明的人。
3.1 脸部识别
我们先建立一个数据库,它存放了所有被允许进入建筑人员的编码后向量数据。它将用到img_to_encoding(image_path,model)方法,这个方法在输入图片数据上通过模型的前向传播来获得结果。
因为是教程,简便起见,我们直接用一个dict来充当数据库:
database = {}
database["danielle"] = img_to_encoding("images/danielle.png", FRmodel)
database["younes"] = img_to_encoding("images/younes.jpg", FRmodel)
database["tian"] = img_to_encoding("images/tian.jpg", FRmodel)
database["andrew"] = img_to_encoding("images/andrew.jpg", FRmodel)
database["kian"] = img_to_encoding("images/kian.jpg", FRmodel)
database["dan"] = img_to_encoding("images/dan.jpg", FRmodel)
database["sebastiano"] = img_to_encoding("images/sebastiano.jpg", FRmodel)
database["bertrand"] = img_to_encoding("images/bertrand.jpg", FRmodel)
database["kevin"] = img_to_encoding("images/kevin.jpg", FRmodel)
database["felix"] = img_to_encoding("images/felix.jpg", FRmodel)
database["benoit"] = img_to_encoding("images/benoit.jpg", FRmodel)
database["arnaud"] = img_to_encoding("images/arnaud.jpg", FRmodel)
接下来,当一个人走到前门处并刷卡,你就可以从数据库中查找他的编码,然后再进行脸部匹配,主要以下几个步骤:
- 将前门摄像机捕捉的图片进行编码
- 计算上一步的编码与数据库中找到的对应id人员的编码间的间距
- 如果间距小于0.7,开门
def verify(image_path, identity, database, model):
"""
验证存放在image_path中的人是否和数据库identity对应的人是同一个
参数:
image_path -- 图片地址
identity -- string, 要识别者的名字(来自于刷卡id). 必须是建筑进入允许的人员.
database -- python dictionary 数据字典 人名:头像编码 (向量).
model -- keras的 inception 模型
Returns:
dist -- image_path存储的图像和identity对应的图像的间距
door_open -- True代表开门,False代表不开门
"""
# Step 1:
encoding = img_to_encoding(image_path,model)
# Step 2:
dist = np.linalg.norm(encoding - database[identity])
# Step 3:
if dist < 0.7:
print("It's " + str(identity) + ", welcome in!")
door_open = True
else:
print("It's not " + str(identity) + ", please go away")
door_open = False
return dist, door_open
用了 np.linalg.norm来计算间距,不传递第二个参数即计算F-范数。
我们传入一张正确的图片试一试:
verify("images/camera_0.jpg", "younes", database, FRmodel)
输出:It’s younes, welcome in!
再来一张错误的呢:
verify("images/camera_2.jpg", "kian", database, FRmodel)
输出:It’s not kian, please go away
3.2 脸部识别
脸部认证系统基本完成了,但是如果系统内某人丢失了ID卡,他再次回到办公室就不能再进去了!(需要刷卡)
要解决这个问题,你就需要将系统改造成一个脸部识别系统。这样大家就都不需要带id卡了。一个被授权的人只要走到前门,门就会自动打开!
很简单,只需要一个遍历,直接上代码即可:
def who_is_it(image_path, database, model):
"""
实现脸部识别系统,识别image_path图片人的身份
Arguments:
略
Returns:
min_dist -- image_path图片与数据库中图片的最小间距
identity -- 最小间距对应的人
"""
encoding = img_to_encoding(image_path,model)
# 初始化最小值,整大点
min_dist = 100
for (name, db_enc) in database.items():
dist = np.linalg.norm(encoding - db_enc)
if dist < min_dist:
min_dist = dist
identity = name
if min_dist > 0.7:
print("Not in the database.")
else:
print ("it's " + str(identity) + ", the distance is " + str(min_dist))
return min_dist, identity
试一下:
who_is_it("images/camera_0.jpg", database, FRmodel)
输出:it’s younes, the distance is 0.659393
激动人心的时候来了,我们把Lisa的图片裁剪成96×96再放入:
{% asset_img Lisa.png Lisa %} {% asset_img camera_Lisa.png LisaInCamera %}database["lisa"] = img_to_encoding("images/Lisa.png", FRmodel)
who_is_it("images/camera_Lisa.png", database, FRmodel)
输出: it’s lisa, the distance is 0.597898
成功!!!
这样,一个简陋版本的脸部识别系统就完成啦!
一些提升的方法
这里就不一一实现了,还有一些可以提升算法效果的方法:
- 对于每个人,多在数据库中放几张照片,比如不同角度的,不同光线的,不同时间的。在刷脸时,将之与数据库中每个人的多张图片进行比较,这样可以提高模型准确度。
- 运用一个裁剪算法,将图片尽量剪到只剩下脸部。这样可以尽量排除不相关因素的干扰,也能提高准确度。
引用:
- Florian Schroff, Dmitry Kalenichenko, James Philbin (2015). FaceNet: A Unified Embedding for Face Recognition and Clustering
- Yaniv Taigman, Ming Yang, Marc’Aurelio Ranzato, Lior Wolf (2014). DeepFace: Closing the gap to human-level performance in face verification
- The pretrained model we use is inspired by Victor Sy Wang’s implementation and was loaded using his code: https://github.com/iwantooxxoox/Keras-OpenFace.
- Our implementation also took a lot of inspiration from the official FaceNet github repository: https://github.com/davidsandberg/facenet
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!