机器学习的最新进展使人脸识别不再是一个难题。但在此之前,研究人员进行了各种尝试并开发了各种技能,以使计算机能够识别人。取得了一定成功的早期尝试之一是eigenface,它基于线性代数技术。
在本教程中,我们将了解如何使用一些简单的线性代数技术(例如主成分分析)构建原始人脸识别系统。
完成本教程后,您将了解:
- 特征脸技术的发展
- 如何使用主成分分析从图像数据集中提取特征图像
- 如何将任意图像表示为特征图像的加权和
- 如何从主成分权重比较图像的相似度

人脸识别
教程概述
本教程分为 3 部分;他们是:
- 图像和人脸识别
- 特征脸概述
- 实施特征脸
图像和人脸识别
在计算机中,图片被表示为像素矩阵,每个像素都以一些数值编码的特定颜色。人们很自然地会问计算机是否可以阅读图片并理解它是什么,如果可以,我们是否可以使用矩阵数学来描述逻辑。为了不那么雄心勃勃,人们试图将这个问题的范围限制在识别人脸上。人脸识别的早期尝试是将矩阵视为高维细节,并从中推断出较低维度的信息向量,然后尝试识别较低维度的人。在过去,这是必要的,因为计算机功能不强大,内存量也非常有限。然而,通过探索如何压缩由于图像尺寸要小得多,我们开发了一种技能来比较两个图像是否描绘相同的人脸,即使图片不相同。
1987 年,Sirovich 和 Kirby 发表的一篇论文考虑了这样的想法:所有人脸图片都是一些“关键图片”的加权和。Sirovich 和 Kirby 将这些关键图片称为“特征图片”,因为它们是人脸均值减去图片的协方差矩阵的特征向量。在论文中,他们确实以矩阵形式提供了人脸图片数据集的主成分分析算法。并且加权和中使用的权重确实对应于人脸图片到每个特征图片的投影。
1991年,Turk和Pentland的一篇论文创造了“特征脸”一词。他们建立在 Sirovich 和 Kirby 的想法之上,并使用权重和特征图作为识别面部的特征。Turk 和 Pentland 的论文提出了一种节省内存的方法来计算特征图。它还提出了一种关于人脸识别系统如何运行的算法,包括如何更新系统以包含新的人脸以及如何将其与视频捕捉系统相结合。同一篇论文还指出,特征脸的概念可以帮助重建部分被遮挡的图像。
特征脸概述
在我们进入代码之前,让我们概述一下使用特征脸进行人脸识别的步骤,并指出一些简单的线性代数技术如何帮助完成任务。
假设我们有一堆人脸图片,它们都具有相同的像素维度(例如,都是 r×c 灰度图像)。如果我们得到 M 张不同的图片并将每张图片矢量化为 L=r×c 像素,我们可以将整个数据集表示为 L×M 矩阵(我们称之为矩阵A),其中矩阵中的每个元素都是像素的灰度值。
回想一下,主成分分析 (PCA) 可以应用于任何矩阵,其结果是许多称为主成分的向量。每个主成分的长度与矩阵的列长度相同。同一矩阵的不同主成分彼此正交,这意味着它们中任意两个的向量点积为零。因此,各个主成分构建了一个向量空间,其中矩阵中的每一列都可以表示为主成分的线性组合(即加权和)。
完成的方法是首先采C=A-a在哪里a是矩阵的平均向量A。所以C是减去每列的矩阵A与平均向量a。那么协方差矩阵是
S=C.C
从中我们可以找到它的特征向量和特征值。主成分是按特征值降序排列的这些特征向量。因为矩阵�是一个L×L矩阵,我们可以考虑求一个M×M矩阵的特征向量�时间⋅�相反作为特征向量�为了�时间⋅�可以转化为特征向量你的�⋅�时间经过你=�⋅�,除了我们通常更喜欢写你作为归一化向量(即范数你是 1)。
主成分向量的物理意义�,或等效的特征向量�=�⋅�时间,是我们构建矩阵列的关键方向�。不同主成分向量的相对重要性可以从相应的特征值推断出来。特征值越大,越有用(即包含更多关于�) 主成分向量。因此我们可以只保留前 K 个主成分向量。如果矩阵�是人脸图片的数据集,前K个主成分向量是前K个最重要的“人脸图片”。我们称它们为特征脸图片。
对于任何给定的人脸图片,我们可以使用向量点积将其均值减去版本投影到特征脸图片上。结果就是这张人脸图片与特征脸的相关程度有多近。如果人脸图片与特征脸完全无关,我们预计其结果为零。对于 K 个特征脸,我们可以找到任何给定人脸图片的 K 个点积。我们可以将结果表示为该面部图片相对于特征脸的权重。权重通常表示为向量。
相反,如果我们有一个权重向量,我们可以将受权重影响的每个特征脸相加并重建一个新的脸部。让我们将特征脸表示为矩阵�,这是一个L×K矩阵,权重向量�是一个列向量。那么对于任意�我们可以将人脸的图片构造为
�=�⋅�
哪个�结果是长度为 L 的列向量。因为我们只使用前 K 个主成分向量,所以我们应该预期得到的面部图片会失真,但保留了一些面部特征。
由于特征脸矩阵对于数据集是恒定的,因此变化的权重向量�意思是变化的脸部图片。因此,我们可以预期同一个人的图片将提供相似的权重向量,即使图片不相同。因此,我们可以利用两个权重向量之间的距离(例如 L2 范数)作为两张图片相似程度的度量。
实施特征脸
现在我们尝试用 numpy 和 scikit-learn 来实现特征脸的想法。我们还将使用 OpenCV 来读取图片文件。您可能需要使用命令安装相关包pip
:
1
|
pip install opencv–python
|
我们使用的数据集是ORL Database of Faces,它已经很老旧了,但我们可以从 Kaggle 下载它:
该文件是一个大约 4MB 的 zip 文件。它有40个人的照片,每个人有10张照片。总共400张图片。下面我们假设文件被下载到本地目录并命名为attface.zip
.
我们可以解压zip文件来获取图片,也可以利用zipfile
Python中的包直接读取zip文件中的内容:
1
2
3
4
5
6
7
8
9
10
11
12
|
import cv2
import zipfile
import numpy as np
faces = {}
with zipfile.ZipFile(“attface.zip”) as facezip:
for filename in facezip.namelist():
if not filename.endswith(“.pgm”):
continue # not a face picture
with facezip.open(filename) as image:
# If we extracted files from zip, we can use cv2.imread(filename) instead
faces[filename] = cv2.imdecode(np.frombuffer(image.read(), np.uint8), cv2.IMREAD_GRAYSCALE)
|
以上是读取zip中的每个PGM文件。PGM 是一种灰度图像文件格式。我们将每个 PGM 文件提取为字节字符串,image.read()
并将其转换为 numpy 字节数组。然后我们使用 OpenCV 将字节字符串解码为像素数组cv2.imdecode()
。OpenCV 将自动检测文件格式。我们将每张图片保存到 Python 字典中faces
以供以后使用。
在这里,我们可以使用 matplotlib 查看这些人脸图片:
1
2
3
4
5
6
7
8
|
...
import matplotlib.pyplot as plt
fig, axes = plt.subplots(4,4,sharex=True,sharey=True,figsize=(8,10))
faceimages = list(faces.values())[–16:] # take last 16 images
for i in range(16):
axes[i%4][i//4].imshow(faceimages[i], cmap=”gray”)
plt.show()
|

我们还可以找到每张图片的像素大小:
1
2
3
|
...
faceshape = list(faces.values())[0].shape
print(“Face image shape:”, faceshape)
|
1
|
Face image shape: (112, 92)
|
人脸图片通过 Python 字典中的文件名进行识别。我们可以看一下文件名:
1
2
|
...
print(list(faces.keys())[:5])
|
1
|
[‘s1/1.pgm’, ‘s1/10.pgm’, ‘s1/2.pgm’, ‘s1/3.pgm’, ‘s1/4.pgm’]
|
因此我们可以将同一个人的面孔归为同一类。共有40个类,共400张图片:
1
2
3
4
|
...
classes = set(filename.split(“/”)[0] for filename in faces.keys())
print(“Number of classes:”, len(classes))
print(“Number of pictures:”, len(faces))
|
1
2
|
Number of classes: 40
Number of pictures: 400
|
为了说明使用特征脸进行识别的能力,我们希望在生成特征脸之前保留一些图片。我们提供一个人的所有照片以及另一个人的一张照片作为我们的测试集。剩余的图片被矢量化并转换为 2D numpy 数组:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
...
# Take classes 1-39 for eigenfaces, keep entire class 40 and
# image 10 of class 39 as out-of-sample test
facematrix = []
facelabel = []
for key,val in faces.items():
if key.startswith(“s40/”):
continue # this is our test set
if key == “s39/10.pgm”:
continue # this is our test set
facematrix.append(val.flatten())
facelabel.append(key.split(“/”)[0])
# Create facematrix as (n_samples,n_pixels) matrix
facematrix = np.array(facematrix)
|
现在我们可以对该数据集矩阵进行主成分分析。我们没有一步步计算 PCA,而是利用 scikit-learn 中的 PCA 函数,我们可以轻松检索我们需要的所有结果:
1
2
3
4
5
|
...
# Apply PCA to extract eigenfaces
from sklearn.decomposition import PCA
pca = PCA().fit(facematrix)
|
我们可以从解释的方差比中确定每个主成分的显着性:
1
2
|
...
print(pca.explained_variance_ratio_)
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
[1.77824822e-01 1.29057925e-01 6.67093882e-02 5.63561346e-02
5.13040312e-02 3.39156477e-02 2.47893586e-02 2.27967054e-02
1.95632067e-02 1.82678428e-02 1.45655853e-02 1.38626271e-02
1.13318896e-02 1.07267786e-02 9.68365599e-03 9.17860717e-03
8.60995215e-03 8.21053028e-03 7.36580634e-03 7.01112888e-03
6.69450840e-03 6.40327943e-03 5.98295099e-03 5.49298705e-03
5.36083980e-03 4.99408106e-03 4.84854321e-03 4.77687371e-03
…
1.12203331e-04 1.11102187e-04 1.08901471e-04 1.06750318e-04
1.05732991e-04 1.01913786e-04 9.98164783e-05 9.85530209e-05
9.51582720e-05 8.95603083e-05 8.71638147e-05 8.44340263e-05
7.95894118e-05 7.77912922e-05 7.06467912e-05 6.77447444e-05
2.21225931e-32]
|
或者我们可以简单地构造一个适中的数字,比如 50,并将这些主成分向量视为特征脸。为了方便起见,我们从 PCA 结果中提取特征脸并将其存储为 numpy 数组。请注意,特征脸存储为矩阵中的行。如果我们想显示它,我们可以将其转换回 2D。在下面,我们展示了一些特征脸,看看它们是什么样子的:
1
2
3
4
5
6
7
8
9
10
|
...
# Take the first K principal components as eigenfaces
n_components = 50
eigenfaces = pca.components_[:n_components]
# Show the first 16 eigenfaces
fig, axes = plt.subplots(4,4,sharex=True,sharey=True,figsize=(8,10))
for i in range(16):
axes[i%4][i//4].imshow(eigenfaces[i].reshape(faceshape), cmap=”gray”)
plt.show()
|

从这张图片中,我们可以看到特征脸是模糊的脸,但实际上每个特征脸都拥有一些可以用来构建图片的面部特征。
由于我们的目标是构建人脸识别系统,因此我们首先计算每张输入图片的权重向量:
1
2
3
|
...
# Generate weights as a KxN matrix where K is the number of eigenfaces and N the number of samples
weights = eigenfaces @ (facematrix – pca.mean_).T
|
上面的代码使用矩阵乘法来代替循环。它大致相当于以下内容:
1
2
3
4
5
6
7
8
|
...
weights = []
for i in range(facematrix.shape[0]):
weight = []
for j in range(n_components):
w = eigenfaces[j] @ (facematrix[i] – pca.mean_)
weight.append(w)
weights.append(weight)
|
到这里,我们的人脸识别系统就已经完成了。我们使用 39 个人的照片来构建我们的特征脸。我们使用属于这 39 个人之一的测试图片(从训练 PCA 模型的矩阵中提取出来的图片)来看看它是否能够成功识别人脸:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
...
# Test on out-of-sample image of existing class
query = faces[“s39/10.pgm”].reshape(1,–1)
query_weight = eigenfaces @ (query – pca.mean_).T
euclidean_distance = np.linalg.norm(weights – query_weight, axis=0)
best_match = np.argmin(euclidean_distance)
print(“Best match %s with Euclidean distance %f” % (facelabel[best_match], euclidean_distance[best_match]))
# Visualize
fig, axes = plt.subplots(1,2,sharex=True,sharey=True,figsize=(8,6))
axes[0].imshow(query.reshape(faceshape), cmap=“gray”)
axes[0].set_title(“Query”)
axes[1].imshow(facematrix[best_match].reshape(faceshape), cmap=“gray”)
axes[1].set_title(“Best match”)
plt.show()
|
上面,我们首先将矢量化图像减去从 PCA 结果中检索到的平均矢量。然后我们计算这个均值减去向量到每个特征脸的投影,并将其作为该图片的权重。然后,我们将相关图片的权重向量与每张现有图片的权重向量进行比较,并找到 L2 距离最小的图片作为最佳匹配。我们可以看到,它确实可以成功地找到同一类中最接近的匹配:
1
|
Best match s39 with Euclidean distance 1559.997137
|
我们可以通过并排比较最接近的匹配来可视化结果:

我们可以用我们从 PCA 拿出的第 40 个人的照片再试一次。我们永远不会得到正确的结果,因为它对我们的模型来说是一个新人。然而,我们想看看它有多么错误以及距离度量中的值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
...
# Test on out-of-sample image of new class
query = faces[“s40/1.pgm”].reshape(1,–1)
query_weight = eigenfaces @ (query – pca.mean_).T
euclidean_distance = np.linalg.norm(weights – query_weight, axis=0)
best_match = np.argmin(euclidean_distance)
print(“Best match %s with Euclidean distance %f” % (facelabel[best_match], euclidean_distance[best_match]))
# Visualize
fig, axes = plt.subplots(1,2,sharex=True,sharey=True,figsize=(8,6))
axes[0].imshow(query.reshape(faceshape), cmap=“gray”)
axes[0].set_title(“Query”)
axes[1].imshow(facematrix[best_match].reshape(faceshape), cmap=“gray”)
axes[1].set_title(“Best match”)
plt.show()
|
我们可以看到它的最佳匹配有更大的 L2 距离:
1
|
Best match s5 with Euclidean distance 2690.209330
|
但我们可以看到错误的结果与相关图片有一些相似之处:
Turk和Petland的论文中建议我们为L2距离设置一个阈值。如果最佳匹配的距离小于阈值,我们就会认为该人脸被识别为同一个人。如果距离高于阈值,即使可以在数字上找到最佳匹配,我们也会声称该图片是我们从未见过的人。在这种情况下,我们可以考虑通过记住这个新的权重向量,将其作为一个新人纳入我们的模型中。
实际上,我们可以更进一步,使用特征脸生成新的面孔,但结果不太现实。在下面,我们使用随机权重向量生成一个并将其与“平均脸”并排显示:
1
2
3
4
5
6
7
8
9
10
|
...
# Visualize the mean face and random face
fig, axes = plt.subplots(1,2,sharex=True,sharey=True,figsize=(8,6))
axes[0].imshow(pca.mean_.reshape(faceshape), cmap=“gray”)
axes[0].set_title(“Mean face”)
random_weights = np.random.randn(n_components) * weights.std()
newface = random_weights @ eigenfaces + pca.mean_
axes[1].imshow(newface.reshape(faceshape), cmap=“gray”)
axes[1].set_title(“Random face”)
plt.show()
|

特征脸有多好?由于模型的简单性,它的成绩令人惊讶。然而,特克和彭特兰在各种条件下对其进行了测试。研究发现,其准确度“在光线变化时平均为 96%,在方向变化时平均为 85%,在尺寸变化时平均为 64%”。因此,作为人脸识别系统,它可能不太实用。毕竟作为矩阵的图片在放大和缩小后在主成分域会产生很大的畸变。因此,现代的替代方案是使用卷积神经网络,它对各种变换更宽容。
将所有内容放在一起,以下是完整的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
|
import zipfile
import cv2
import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
# Read face image from zip file on the fly
faces = {}
with zipfile.ZipFile(“attface.zip”) as facezip:
for filename in facezip.namelist():
if not filename.endswith(“.pgm”):
continue # not a face picture
with facezip.open(filename) as image:
# If we extracted files from zip, we can use cv2.imread(filename) instead
faces[filename] = cv2.imdecode(np.frombuffer(image.read(), np.uint8), cv2.IMREAD_GRAYSCALE)
# Show sample faces using matplotlib
fig, axes = plt.subplots(4,4,sharex=True,sharey=True,figsize=(8,10))
faceimages = list(faces.values())[–16:] # take last 16 images
for i in range(16):
axes[i%4][i//4].imshow(faceimages[i], cmap=”gray”)
print(“Showing sample faces”)
plt.show()
# Print some details
faceshape = list(faces.values())[0].shape
print(“Face image shape:”, faceshape)
classes = set(filename.split(“/”)[0] for filename in faces.keys())
print(“Number of classes:”, len(classes))
print(“Number of images:”, len(faces))
# Take classes 1-39 for eigenfaces, keep entire class 40 and
# image 10 of class 39 as out-of-sample test
facematrix = []
facelabel = []
for key,val in faces.items():
if key.startswith(“s40/”):
continue # this is our test set
if key == “s39/10.pgm”:
continue # this is our test set
facematrix.append(val.flatten())
facelabel.append(key.split(“/”)[0])
# Create a NxM matrix with N images and M pixels per image
facematrix = np.array(facematrix)
# Apply PCA and take first K principal components as eigenfaces
pca = PCA().fit(facematrix)
n_components = 50
eigenfaces = pca.components_[:n_components]
# Show the first 16 eigenfaces
fig, axes = plt.subplots(4,4,sharex=True,sharey=True,figsize=(8,10))
for i in range(16):
axes[i%4][i//4].imshow(eigenfaces[i].reshape(faceshape), cmap=”gray”)
print(“Showing the eigenfaces”)
plt.show()
# Generate weights as a KxN matrix where K is the number of eigenfaces and N the number of samples
weights = eigenfaces @ (facematrix – pca.mean_).T
print(“Shape of the weight matrix:”, weights.shape)
# Test on out-of-sample image of existing class
query = faces[“s39/10.pgm”].reshape(1,–1)
query_weight = eigenfaces @ (query – pca.mean_).T
euclidean_distance = np.linalg.norm(weights – query_weight, axis=0)
best_match = np.argmin(euclidean_distance)
print(“Best match %s with Euclidean distance %f” % (facelabel[best_match], euclidean_distance[best_match]))
# Visualize
fig, axes = plt.subplots(1,2,sharex=True,sharey=True,figsize=(8,6))
axes[0].imshow(query.reshape(faceshape), cmap=“gray”)
axes[0].set_title(“Query”)
axes[1].imshow(facematrix[best_match].reshape(faceshape), cmap=“gray”)
axes[1].set_title(“Best match”)
plt.show()
# Test on out-of-sample image of new class
query = faces[“s40/1.pgm”].reshape(1,–1)
query_weight = eigenfaces @ (query – pca.mean_).T
euclidean_distance = np.linalg.norm(weights – query_weight, axis=0)
best_match = np.argmin(euclidean_distance)
print(“Best match %s with Euclidean distance %f” % (facelabel[best_match], euclidean_distance[best_match]))
# Visualize
fig, axes = plt.subplots(1,2,sharex=True,sharey=True,figsize=(8,6))
axes[0].imshow(query.reshape(faceshape), cmap=“gray”)
axes[0].set_title(“Query”)
axes[1].imshow(facematrix[best_match].reshape(faceshape), cmap=“gray”)
axes[1].set_title(“Best match”)
plt.show()
|