原文:
annas-archive.org/md5/61756cde4c66978a12599ccffeb53dae译者:飞龙
第六章:使用 dlib 进行面部特征追踪和分类
在本章中,我们将学习 dlib 及其如何通过一些示例从图像和视频中定位人脸,同时也会学习使用 dlib 进行面部识别。
我们将涵盖以下主题:
-
介绍 dlib
-
面部特征点
-
在图像中找到 68 个面部特征点
-
视频中的面部
-
面部识别
介绍 dlib
dlib 是一个通用、跨平台的软件库,使用 C++编程语言编写。我们将学习 dlib,并理解如何从图像和视频中找到和使用人类面部特征。根据其官方网站dlib.net,dlib 是一个现代的 C++工具,包含机器学习算法和用于在 C++中创建复杂软件的工具,以解决现实世界的问题。它是一个 C++工具包,就像 OpenCV 一样,它包含了一套非常优秀的 Python 绑定,这将非常适合我们的应用。
dlib 是一个非常丰富的库,包含大量算法和功能,这些内容在他们的网站上都有很好的文档说明。这使得学习起来变得容易,并且它提供了许多与我们在本章将要做的以及为您的定制项目相似的示例。如果您对 dlib 感兴趣并想了解如何将其用于您的应用程序,建议您查看他们的网站。在dlib.net/网站的高质量便携代码部分,有针对 Microsoft Windows、Linux 和 macOS 的高效代码,就像 Python 一样,包含了一个非常丰富的机器学习算法集,包括我们在本章使用的最先进的深度学习,尽管我们将使用 TensorFlow 来完成我们的目的。它还包括支持向量机(SVMs),我们在第五章“使用 scikit-learn 和 TensorFlow 进行手写数字识别”中看到过,以及用于目标检测和聚类的广泛其他功能,如 K-means 等。它还包含丰富的数值算法、线性代数、奇异值分解(SVD)以及大量的优化算法,以及图形模型推理算法和图像处理(这对我们非常有用)。它有读取和写入常见图像格式的例程(尽管我们不会使用它们,因为我们将使用我们之前看到的工具来读取和写入图像)以及加速鲁棒特征(SURF)、方向梯度直方图(HOG)和 FHOG,这些对图像检测和识别很有用。目前有趣的是检测对象的各种工具,包括正面人脸检测、姿态估计和面部特征识别。因此,我们将在本章中讨论这些内容。dlib 还有一些其他功能,如多线程、网络、图形用户界面(GUI)开发、数据压缩以及许多其他实用工具。dlib.net/提供了 C++和 Python 的示例。我们将对人脸检测、面部特征点检测和识别感兴趣。因此,我们将通过类似的示例来查看我们这里有什么。
面部特征点
我们将学习 dlib 中关于面部特征点的所有内容。在我们能够运行任何代码之前,我们需要获取一些用于面部特征本身的数据。我们将了解这些面部特征是什么以及我们具体在寻找哪些细节。这些内容不包括在 Python dlib 发行版中,因此您将需要下载这些内容。我们将访问dlib.net/files/网站,在那里您可以查看所有源代码文件;滚动到页面底部,您可以看到shape_predictor_68_face_landmarks.dat.bz2文件。点击它,然后将其保存到您为这本书的 Jupyter Notebooks 保留的位置。
好的,那么,这究竟是什么?这 68 个标记点是什么?嗯,这些标记点是通过对称为 iBUG([ibug.doc.ic.ac.uk/resources/facial-point-annotations/](https://ibug.doc.ic.ac.uk/resources/facial-point-annotations/))的智能行为理解小组从 alpha 数据集进行训练生成的常见特征集。所以,这是一个预训练模型,一个包含来自世界各地、各种年龄、男性和女性等人群的大量人脸数据库。
因此,我们将处理各种情况,我们寻找的是围绕面部轮廓的一组点,正如您可以在以下图中看到:
点1至17是面部轮廓,点18至22是右眉毛,23至27是左眉毛,28至31是鼻梁,30至36是鼻底,37至42形成右眼,43至48勾勒出左眼,然后还有许多关于嘴巴的点,包括上唇两侧和下唇两侧。
因此,这些是所有人类面孔都会有的常见特征,这将使我们能够做很多事情,如人脸识别和身份验证、姿态估计、可能年龄估计、性别估计,甚至像面部缝合和面部混合这样的有趣事情。仅凭这些信息就可以做很多非常有趣的事情,这些都是基于面部纯强度值。所以,这里没有 SURF 特征、尺度不变特征变换(SIFT)特征、HOG 特征或任何类似的东西。这些只是从像素值中可检测到的。所以,实际上您可以将 RGB 转换为黑白到单色,并且如果这是一个回归树的集成,您可以运行这个模型。
您可以下载 iBUG 数据集并训练您自己的模型,并且您实际上可以调整特征的数量。有比这更多特征的数据集,但这对我们的目的来说已经足够了。如果您想对各种面孔或特定面孔运行它,您可以训练它,但您会发现这个预训练数据集在许多情况下都会有效。因此,iBUG 本身就很强大。我们将在这里使用它,并展示如何运行代码来为一些图像和视频找到所有这些特征。然后,我们将将其应用于人脸识别问题,其中我们在给定集合中区分面孔。在您下载了shape_predictor_68_face_landmarks.dat.bz2文件后,您可以将该文件放入您拥有 Jupyter Notebook 的目录中,然后我们可以开始编写代码。
在图像中寻找 68 个面部标记点
在本节中,我们将看到我们的第一个示例,其中找到 68 个面部地标和单人图像以及多个人图像。所以,让我们打开本节的 Jupyter Notebook。看看这个第一个单元格:
%pylab notebook
import dlib
import cv2
import os
import tkinter
from tkinter import filedialog
from IPython import display
root = tkinter.Tk()
root.withdraw()
#Go to your working directory (will be different for you)
%cd /home/test/13293
我们需要做一些基本的设置,就像我们在前面的章节中所做的那样。我们将初始化%pylab notebook。再次强调,这将加载 NumPy 和 PyPlot 以及其他一些东西,我们现在将执行notebook,这对于图像的近距离观察很有用,尽管我们将它切换到inline用于第二个示例,因为我们需要它来查看视频。然后,我们必须导入我们的其他库。dlib 当然是本节的重点。
我们将使用 OpenCV 的一些实用工具,但这只是额外的注释和视频处理。我们将使用tkinter来有一个漂亮的文件对话框显示。所以,而不是将文件名硬编码到我们的代码中,我们将提示用户输入我们想要分析的文件。我们将从IPython导入display以便在第二个示例中观看电影,我们必须设置tkinter;我们想要确保我们处于包含所有文件的工作目录中。你可能不需要这样做,但你可以这样做以确保。
因此,我们将选择单元格,按Ctrl + Enter,然后,如果一切正常,你应该看到以下输出:
你可以看到Populating the interactive namespace和你的当前工作目录。
好的,现在我们已经设置好了,让我们看看第一个示例,我们将实际使用我们下载的文件中的 68 个特征;我们将看到在 dlib 中这样做是多么简单。现在,我们将看到这只是一点点的代码,但它确实做了些非常酷的事情:
imgname = filedialog.askopenfilename(parent = root,initialdir = os.getcwd(), title = 'Select image file...')
img = imread(imgname)
img.flags['WRITEABLE']=True
annotated = img.copy()
predictor_path = "./shape_predictor_68_face_landmarks.dat"
detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor(predictor_path)
font = cv2.FONT_HERSHEY_SIMPLEX
dets = detector(img, 1)
print("Number of faces detected: {}".format(len(dets)))
for k, d in enumerate(dets):
print("Detection {}: Left: {} Top: {} Right: {} Bottom: {}".format(k, d.left(), d.top(), d.right(), d.bottom()))
shape = predictor(img,d)
print("Part 0: {}, Part 1:{} ...".format(shape.part(0),shape.part(1)))
head_width = shape.part(16).x-shape.part(0).y
fontsize = head_width/650
for pt in range(68):
x,y = shape.part(pt).x, shape.part(pt).y
annotated = cv2.putText(annotated, str(pt), (x,y), font, fontsize, (255,255,255),2, cv2.LINE_AA)
figure(figsize = (8,6))
imshow(annotated)
首先,我们将要求用户输入文件名。所以,这是使用tkinter,我们将打开一个文件名;它将使用initialdir=os.getcwd()函数在当前工作目录中开始搜索:
imgname = filedialog.askopenfilename(parent = root,initialdir = os.getcwd(), title = 'Select image file...')
我们将使用以下行来读取:
img = imread(imgname)
img.flags['WRITEABLE']=True
img.flags['WRITEABLE']=True这一行是 dlib 的一个小特性,并不是什么大问题,但是,根据你如何加载文件,WRITEABLE的flags可能被设置为False。这种情况发生在使用imread时。这取决于你如何加载它,但为了确保,WRITEABLE需要被设置为True。否则,dlib 会抛出一个错误。根据你的加载方式,这可能不是必要的。
我们想要创建一个可以写入的图像,实际上可以显示地标的位置,所以我们将创建我们之前加载的图像的副本,即包含人脸的图像,这样我们就可以写入它而不会覆盖原始图像:
annotated = img.copy()
现在,我们将从我们下载的文件中加载数据。shape_predictor_68_face_landmarks.dat.bz2以.bz2格式提供;如果你还没有解压它,你可以将其解压为.dat格式。如果你在 Windows 上,建议使用 7-zip。如果你在 Linux 或 macOS 上,应该有一个内置的实用程序,你可以双击它,提取应该相当直接。
因此,我们将设置路径并将其保持在当前目录中,并且我们需要初始化我们的对象:
predictor_path = "./shape_predictor_68_face_landmarks.dat"
现在,这里有两个阶段。首先,你需要检测人脸的位置。这类似于如果你之前使用过 OpenCV 和那些示例,Haar 级联会做什么,但我们使用dlib.get_frontal_face_detector,它只是内置的:
detector = dlib.get_frontal_face_detector()
因此,我们创建detector对象,从dlib.get_frontal_face_detector获取它,初始化它,然后是predictor:
predictor = dlib.shape_predictor(predictor_path)
一旦我们检测到人脸的位置,我们就知道有多少张人脸,可能会有多张。dlib 对于多张人脸也能很好地工作,我们将会看到。一旦你知道人脸在哪里,然后你可以运行predictor,它实际上会找到之前提到的 68 个地标的位置。所以,我们再次创建我们的detector对象和predictor对象,同时确保predictor_path设置正确。
然后,我们将设置我们的font:
font = cv2.FONT_HERSHEY_SIMPLEX
font只是将地标数据显示在注释图像上。所以,如果你想改变它,可以。好的,现在我们来到了代码的有趣部分。首先,进行检测,并找到人脸的确切位置。这里有一行非常简单的代码:
dets = detector(img,1)
我们将只打印出检测到的人脸数量:
print("Number of faces detected: {}".format(len(dets)))
这对于调试目的可能很有用,尽管我们将在实际检测到人脸的地方看到输出图像。
现在,我们将在这里进行一个for循环,这将处理可能有多张人脸的情况:
#1 detection = 1 face; iterate over them and display data
for k, d in enumerate(dets):
因此,我们将遍历每一个。dets的长度可能是一个,多个,或者零,但在这个例子中我们不会这么做。如果你不确定,你可能想把它放在try...catch块中,但在这里我们只处理有可见人脸的图像。
因此,我们将遍历人脸,并在Left、Top、Right和Bottom上显示每个脸的确切边界框;它们确切地在哪里?注意以下代码:
print("Detection {}: Left: {} Top:{} Right: {} Bottom: {}".format(
k, d.left(), d.top(), d.right(), d.bottom()))
这就是魔法发生的地方:
shape = predictor(img, d)
我们将找到形状,然后找到那 68 个地标,并通过打印出前几个地标来做一个简单的检查,以确保它正在工作:
print("Part 0: {}, Part 1: {} ...".format(shape.part(0), shape.part(1)))
好吧,所以我们有了脸部地标,现在我们实际上想要显示它,以便了解我们到底有什么。我们想要调整font的大小,以确保它适合图像,因为,根据图像的大小,你可能有一个高分辨率的图像,比如 4,000×2,000 像素,或者你可能有一个低分辨率的图像,比如 300×200 像素(或者类似的大小),图像中的头部可能非常大,就像主题靠近相机一样,或者相反,如果它远离相机,则可能很小。
所以,我们想要将font缩放到图像中头部的大小:
#We want to scale the font to be in proportion to the head
#pts 16 and 0 correspond to the extreme points on the right/left side of head
head_width = shape.part(16).x-shape.part(0).x
fontsize = head_width/650
所以,这里我们只是在计算head_width。shape是一个预测对象,它有一个part方法,你传入你想要找到的地标点的索引,每个地标都将有一个x和y部分。所以,head_width在这里是16,这取决于你的视角。head_width只是头部在像素意义上的宽度。然后,我们将根据head_width调整字体大小,650是一个很好的因子,效果很好。
现在,我们有了所有数据,我们将遍历每个点:
for pt in range(68);
x,y = shape.part(pt).x, shape.part(pt).y
annotated=cv2.putText(annotated, str(pt), (x,y), font, fontsize, (255,255,255),2, cv2.LINE_AA)
因此,我们将硬编码68,因为我们知道我们有68个点,但如果你使用另一种形状检测器,比如预训练的形状检测器,你可能想改变这个数字。我们遍历这些点,然后我们得到之前显示的每个地标点的x和y坐标。我们使用shape.part提取x和y坐标并更新注释图像。我们需要cv2将文本放入图像。dlib 确实有类似的功能,但cv2更好,我们无论如何都可以有一个统一的接口。所以,我们将在这里使用 OpenCV,然后我们将创建一个图形并显示它:
figure(figsize=(8,6))
imshow(annotated)
所以,这就是关于代码的所有内容,希望这对你来说看起来相当简单。随意阅读。当我们执行代码时,我们可以看到一个股票照片的对话框。我们可以从这些照片中选择任何一张;例如,这里是一位戴帽子的男士的照片。所以,计算这个只需要一点时间,看这里:
我们看到这个人有所有的 68 个点。我们将其标记为从 0 到67,因为这是 Python 从 0 开始的索引惯例,但我们可以看到,就像之前一样,我们有了所有的点;所以,你可以看到左侧的点 0,右侧的点 16,这取决于你的视角,然后它继续围绕整个头部。这里有一个放大的视图以增加清晰度:
如我们所见,有些点彼此很近,但你可以在这里了解每个点代表什么。看起来相当清晰。所以,这相当酷,正如之前提到的,你可以用这个做很多事情。这个人正直视镜头,所以你可能想知道如果有人头部倾斜会发生什么?好吧,我们将再次运行这个程序。
让我们在这里选择一位股票照片中的女士:
你可以看到她的头转过去了,但这仍然可以正常工作。在极端情况下,这并不总是可行的;如果某人的头转得太多以至于地标不见了,那么在合理的情况下,这可能会失败,你可以看到这实际上工作得非常好。
好的,那么关于多个人脸呢?这对那个有效吗?让我们看看另一张团体照片:
我们可以看到这里有六个人,他们以不同的姿势站立。鉴于这里的分辨率,你无法阅读这些注释,但这完全没问题,因为你已经看到了它们的位置,我们可以看到我们实际上非常准确地检测到了所有六个面部。所以,希望你能在这里得到一些想法,了解你如何在自己的代码中使用它,以及 dlib 在检测阶段为你提供了多么简单的操作。
视频中的人脸
我们将看到上一节关于照片中人脸的第二个示例。静态图像示例很简洁,但你可能想知道关于视频的情况。好的,让我们看看下一个示例:
%pylab inline
%cd /home/test/13293
import dlib
import cv2
import os
import tkinter
from tkinter import filedialog
from IPython import display
root = tkinter.Tk()
root.withdraw()
我们将代码更改为%pylab inline,因为所有这些小部件实际上可能会在你想显示视频序列时与 Jupyter 发生问题。我们将需要与之前示例中相同的代码来开始,只需将notebook替换为inline。然后,我们再次运行相同的代码。
执行完毕后,我们继续下一部分。这实际上非常相似,因为你只需要遍历每一帧,它就会以同样的方式工作:
predictor_path = "./shape_predictor_68_face_landmarks.dat"
detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor(predictor_path)
所以,你看,这段代码基本上与之前的示例相同。如果你想,你可以用你的摄像头来做这个。实际上,这非常有趣。我们这里不会使用摄像头,但如果你想在自定义项目中使用摄像头,你可以添加以下行:
cap = cv2.VideoCapture(0)
#0 is the first camera on your computer, change if you have more #than one camera
我们假设你只有一个摄像头。如果你有多个摄像头并且不想使用第一个,那么你可能需要将那个0改为其他值。如果你不想使用你的摄像头,请添加以下行:
cap = cv2.Videocapture('./rollerc.mp4')
在这里,我们不会使用摄像头。我们想要创建一个我们将要显示的图形,我们将它命名为100以确保它有一个唯一的 ID。我们将使用与之前示例中相同的font:
font = cv2.FONT_HERSHEY_SIMPLEX
这听起来很复杂,但实际上它只是一个普通的字体。我们将创建一个while循环,它将遍历每一帧:
while(True):
#capture frame-by-frame
ret, img = cap.read
img.flags['WRITEABLE']=True #just in case
因此,我们使用cap作为 OpenCV 的视频捕获对象,然后我们只需要执行cap.read()来读取帧。ret只是确保我们实际上读取了一个帧的代码。然后,img是返回的实际图像,再次确保设置了WRITEABLE标志,否则 dlib 可能会产生错误。
我们将尝试找到一个人脸,如果找不到人脸,那么我们将释放并跳出我们的循环:
try:
dets = detector(img, 1)
shape = predictor(img, dets[0])
except:
print('no face detected', end='\r')
cap.release()
break
你可能不希望你的应用程序有这个功能,但这里的一个很酷的事情是,如果你使用的是摄像头,一个简单的方法来停止这个循环无限期地运行就是将你的手放在脸部前面。你将手放在摄像头前,或者转动你的头,或者随便什么,这会自动停止它,无需手动操作。否则,你可以发送内核中断,并确保你执行 cap.release(),否则视频源将保持打开状态,你可能会稍后遇到错误。
根据前面的代码块,我们抓取图像,检测人脸,并获取形状。对于这段代码,我们假设只有一个脸,但你可以从之前的例子中看到如何处理多个脸。
然后,我们创建一个空白图像或一个与原始图像相同的图像,我们可以写入它而不会扭曲原始图像。设置 head_width 和 fontsize,然后做与我们之前完全一样的事情。找到 x 和 y 点,然后写入它们:
annotated=img.copy()
head_width = shape.part(16).x-shape.part(0).x
fontsize = head_width/650
for pt in range(68):
x,y = shape.part(pt).x, shape.part(pt).y
annotated=cv2.putText(annotated, str(pt), (x,y), font, fontsize, (255,255,255),2, cv2.LINE_AA)
我们将展示我们的结果,如下面的代码所示:
fig=imshow(cv2.cvtColor(annotated,cv2.COLOR_BGR2RGB
注意颜色,BGR2RGB。这是因为 OpenCV 默认使用 蓝绿红 (BGR),如果你不改变这个设置来显示颜色,颜色看起来会很奇怪。然后,这里有一些东西可以确保在脚本仍在运行时我们的窗口正在更新。否则,它实际上会运行整个脚本,你将看不到实时发生的事情。
我们然后按下 Shift + Enter。可能需要一秒钟来加载,然后它会运行得相当慢,主要是因为它是 Jupyter Notebook 的一部分。你可以将代码提取出来作为一个独立的程序运行,你可能还想创建一个名为 cv2 的窗口,但这对我们的目的来说已经足够了。当你执行单元格时,你会看到两位女士:
一张脸有点模糊,所以它不会检测到她,但对于前景中的那位女士,正如你所看到的,她的脸被很好地追踪,并且找到了地标。这可以根据你的硬件实时工作,这不是你希望在 Jupyter Notebook 中运行的那种类型的东西。你可以看多久就多久,但你会明白这个意思。
所以,这就是与视频一起工作的简单方法。切换到背景中的另一位女士,第一位女士的脸转过去了:
这就是与视频一起工作的简单方法,你可以检测多个脸,并使用这些信息做任何你想做的事情。
面部识别
我们将看看如何使用 dlib 和相对较少的代码执行面部识别。在这里,面部识别意味着我们将查看一张图片,看看这个人是否与另一张图片中的人相同。我们将保持简单,只比较两张脸以查看它们是否相同,但这一点可以很容易地推广,就像我们稍后看到的。
在这里,我们将进行与第一个示例类似的操作,我们将提示用户打开两个文件,每个文件中都有一个面部图像将被与另一个进行比较。为此,我们将使用来自 Labeled Faces in the Wild (LFW) 的某些面部图像。这是一个很好的数据库,包含来自各种名人的数千张面部图像。您可以从 vis-www.cs.umass.edu/lfw/ 下载整个集合,并获得大量可以使用的示例。因此,我们只是将从数据集的一个小子集中使用一些示例来进行我们的示例。
我们提示用户选择两个不同的面部图像。我们将从项目文件夹的 faces 子目录开始初始目录:
#Prompt the user for two images with one face each
imgname = filedialog.askopenfilename(parent=root, initialdir='faces', title='First face...')
face1 = imread(imgname)
face1.flags['WRITEABLE']=True
#second face
imgname = filedialog.askopenfilename(parent=root, initialdir='faces', title='Second face...')
face2 = imread(imgname)
face2.flags['WRITEABLE']=True
您需要从 dlib.net/files 下载两个额外的文件,它们是 shape_predictor_5_face_landmarks.dat 文件和 dlib_face_recognition_resnet_model_v1.dat 文件。再次强调,这些文件将以 bz2 格式存在。有趣的是,我们只使用了五个面部特征点,但结合描述符,实际上非常适用于描述人脸。因此,我们没有使用 68 个面部特征点,而是只用了 5 个。我们将看到这会多么顺利。下载这些文件,并像第一个示例中那样解压 bz2 文件。
现在,我们设置正确的文件路径:
predictor_path = './shape_predictor_5_face_landmarks.dat
face_rec_model_path= './ dlib_face_recognition_resnet_model_v1.dat
predictor 的工作方式与 68 个面部特征点相似,但同样会提供五个结果,我们将使用一个预训练的识别模型。它适用于各种面部;您现在不需要重新训练它。在这里,我们不需要进行任何复杂的深度学习建模。有方法可以训练自己的模型,但您会看到这实际上非常适合广泛的多种应用。
因此,我们创建我们的 detector,就像之前一样。这不需要任何额外的数据:
detector = dlib.get_frontal_face_detector()
我们将创建我们的形状查找器,类似于之前的示例,并且再次使用五个面部特征点检测器。我们将创建一个新的 facerec 对象,来自 dlib.face_recognition_model_v1,将路径作为 face_rec_model_path 传入:
sp = dlib.shape_predictor(predictor_path)
facerec = dlib.face_recognition_model_v1(face_rec_model_path)
现在,facerec所做的是,它接受一个映射,给定我们检测到的面部以及那些地标的位置和形状,然后它将创建一个 128 长度的浮点向量,称为描述符,用来描述面部。因此,它实际上创建了一个将描述面部特征的东西,并且能够捕捉到面部的本质。如果你有同一个人在两张不同的照片中,其中一张照片中的人离相机较远,而在另一张照片中他们的脸可能转向,可能有更多张照片,并且可能有不同的光照条件等等。描述符应该对那些条件相当不变。描述符永远不会完全相同,但同一个人应该得到足够相似的面部描述符,无论他们的方向、光照条件等等。即使他们改变发型或戴帽子,你也应该得到一个相似的描述符,而facerec实际上在这方面做得很好。
以下代码仅执行检测和形状查找:
dets1 = detector(face1, 1)
shape1 = sp(face1, dets1[0])
dets2 = detector(face2, 1)
shape2 = sp(face2, dets2[0])
然后,我们将执行之前描述的操作:给定检测、空间特征和地标,我们将计算 128 点的向量,我们可以稍作检查。然后,我们将并排查看面部:
figure(200)
subplot(1,2,1)
imshow(face1)
subplot(1,2,2)
imshow(face2)
现在,我们想知道面部有多相似,所以我们将计算欧几里得距离:
euclidean_distance = np.linalg.norm(np.array(face_descriptor1)-np.array(face_descriptor2))
这意味着你取每个点,从 1 到 128,从第二个点减去第一个点,对每个点进行平方,将它们相加,然后开平方,这将给出一个单一的数字。这个数字将用来确定这两张图像是否是同一个人的面部。
这里有一个神奇的数字0.6,我们将在这里使用它,并且它已经被经验证明非常有效。如果 128 维的距离小于0.6,我们说这两张图像是同一个人的。如果它大于0.6,或者等于0.6,就像这个例子一样,我们将说这些是不同的人。因此,我们查看这两张图像,计算所有这些指标,然后我们将说如果它是<0.6,面部匹配,如果是>0.6,面部不同:
if euclidean_distance<0.6:
print('Faces match')
else:
print('Faces are different')
现在,让我们运行代码。你会看到一个来自 LFW 的名人照片对话框。我们将选择亚历克·鲍德温和西尔维斯特·史泰龙中的一张:
巴德温和西尔维斯特·史泰龙被归类为两个不同的人。这正是我们所预期的,因为他们的脸是不同的。现在,让我们为另一对进行比较。让我们比较亚历克·鲍德温与亚历克·鲍德温:
在这里,你可以看到他们的面部匹配。让我们为了乐趣再进行一些比较。所以,姚明和温莎·瑞德看起来彼此不同:
然后,我们取温莎·瑞德的两个不同照片,面部匹配:
你可以做各种各样的组合。好吧,所以这很简单。看看面部描述符可能很有用;你只需按Shift + Tab,你就可以看到向量看起来像这样:
这并不非常易于人类理解,但如果你对此好奇,它仍然可用。这足以捕捉到人脸的本质,仅仅通过简单的比较,我们实际上可以相当好地判断两张图片是否为同一张人脸。这在 LFW 数据集上实际上有超过 99%的准确率。所以,你实际上很难找到两张人脸结果不佳的情况,无论是同一人的两张人脸声称不匹配,还是不同人的两张人脸声称匹配。
因此,如果你想根据自己的需求进行适配,你可以做的是获取自己的数据库,仅限于你想要识别的人的面部图像目录,然后当你有新的人脸时,只需遍历数据库中的每一张人脸。只需进行一个for循环,并将新的人脸与每一张进行比较。对于这里通过使用 NumPy 线性代数范数(np.linalg.norm)计算出的欧几里得距离,如果这个距离小于 0.6,那么你可以说你找到了一个匹配。如果你担心误判,你可以有一个人多张人脸,并与每一张进行比较,然后执行多数规则。
否则,假设你有十张人脸,你想要确保这十张人脸都匹配。如果你真的想确保没有出现误判,你可以获取十张非常好的图像,然后将你的新测试图像与这十张图像进行比较。但无论如何,从这个例子中你可以看出,这并不需要很多代码,并且这种方法可以适应各种不同的应用。
摘要
在本章中,我们简要介绍了 dlib 库,并学习了如何使用它进行人脸识别。然后,我们学习了如何使用预训练的 68 个面部特征点模型生成人脸轮廓。之后,我们学习了如何为单个人、多个人以及视频中的人找到面部特征点。
在下一章,第七章,使用 TensorFlow 进行深度学习图像分类,我们将学习如何使用预训练模型通过 TensorFlow 对图像进行分类,然后我们将使用我们自己的自定义图像。
第七章:使用 TensorFlow 进行深度学习图像分类
在本章中,我们将学习如何使用 TensorFlow 进行图像分类。首先,我们将使用预训练模型,然后我们将使用自定义图像进行模型训练。
在本章的结尾,我们将利用 GPU 来帮助我们加速计算。
在本章中,我们将涵盖以下内容:
-
TensorFlow 的深度介绍
-
使用预训练模型(Inception)进行图像分类
-
使用我们的自定义图像进行再训练
-
使用 GPU 加速计算
技术要求
除了 Python 知识和图像处理及计算机视觉的基础知识外,你还需要以下库:
-
TensorFlow
-
NVIDIA CUDA®深度神经网络
本章中使用的代码已添加到以下 GitHub 仓库中:
github.com/PacktPublishing/Computer-Vision-Projects-with-OpenCV-and-Python-3
TensorFlow 简介
在本章中,我们将更深入地了解 TensorFlow,并看看我们如何可以使用其深度学习方法构建一个通用的图像分类器。
这将是我们在第二章“使用 scikit-learn 和 TensorFlow 进行手写数字识别”中学到的内容的扩展,在那里我们学习了如何对手写数字进行分类。然而,这种方法要强大得多,因为它将适用于人们的通用图像、动物、食物、日常物品等等。
首先,让我们简单谈谈 TensorFlow 做什么,以及 TensorFlow 的一般工作流程。
首先,什么是张量?维基百科这样描述:
“在数学中,张量是描述几何向量、标量和其他张量之间线性关系的几何对象…给定一个参考向量基,张量可以表示为一个有组织的多维数值数组。”
然而,根据 TensorFlow 的制作者谷歌的说法,张量是任何多维数组,无论数据类型如何。本质上,根据谷歌的说法,张量基本上可以指任何东西。
谷歌将这个词泛化得如此之广,以至于它实际上并没有太多意义,我个人也不喜欢这样(来自工程和物理背景)。然而,TensorFlow 如此强大且有用,我打算克服这一点。只是要注意,如果你对误用单词tensor感到担忧,请不要担心,因为谷歌无论如何都在滥用这个词。
目前,我们只需要知道在 TensorFlow 中,张量是一种数据;通常是多维数组,但它可以是基本上任何东西,例如图像或文本。考虑到这一点,TensorFlow 通常是一个高性能数值库。它主要面向机器学习,但这并不意味着它仅用于机器学习。
TensorFlow 也可以用于模拟、解决复杂的偏微分方程以及几乎所有数值问题。我们只关注机器学习,特别是本章中的深度学习。我们将用它来实现其主要目的,但请注意,它通常用于构建和分析复杂的数值模型。
在我们开始构建分类器之前,我想分享一下我们通常如何使用 TensorFlow 进行非常基础的用法。如下开始:
- 我们将要更改目录,并确保我们可以使用以下代码加载关键库和显示图像等:
#Get started with needed libraries/settings/directory
%pylab inline
%cd C:\Users\mrever\Documents\packt_CV\tensclass
- 接下来,我们使用标准惯例导入
tensorflow和numpy:
import tensorflow as tf
import numpy as np
由于我们执行了pylab inline,所以我们不需要显式地导入numpy,但通常来说这是一个好习惯。如果我们想将一些代码复制到其他脚本中,我们需要确保已经导入了numpy。
- 让我们从简单的 TensorFlow 示例开始。我们只是将要执行一些非常基础的算术。在 TensorFlow 中定义一些常量,如下所示:
#Arithmetic with TensorFlow
a = tf.constant(2)
b = tf.constant(3)
这些常量可以是标量,就像我们定义的那样,也可以是向量或矩阵。我们只是将它们相加。当我们这样做时,我们可以定义我们的常量。
- 我们定义了常量,然后我们使用
with子句创建了一个 TensorFlow 会话。当它离开with子句时,我们会关闭 TensorFlow 会话,如下所示:
with tf.Session() as sess:
print("a=2, b=3")
print("a+b=" + str(sess.run(a+b)))
print("a*b=" + str(sess.run(a*b)))
Session的重要性取决于我们使用的资源,例如,如果我们使用 GPU 并希望释放它,但在这个部分,我们只是将讨论使用 CPU 的Session。
在我们的Session中,TensorFlow 在合理的地方进行了操作符重载。它理解a+b的含义,其中a和b都是 TensorFlow 常量。它还理解乘法(*)、减法(-)、除法(/)等算术运算。
- 现在,我们将使用不同的方法做同样的事情,通过创建
placeholder变量,如下所示:
a = tf.placeholder(tf.int16)
b = tf.placeholder(tf.int16)
通常,我们需要构建我们的模型。这就是 TensorFlow 的基础,它基本上是一个输入-输出模型。因此,我们有输入,这可能是一组数字、图像、单词或任何东西。我们通常需要在输入数据之前找到占位符,然后定义和构建我们的模型。
- 在我们的例子中,我们只是定义了加法,就像我们通常定义的那样,如下所示:
add = tf.add(a, b)
mul = tf.multiply(a, b)
这可能是一些更复杂的事情,比如构建一个神经网络,一个卷积神经网络(CNN)等等。
我们将稍后看到一些例子,但现在我们定义我们的输入、我们的模型、我们的操作等,并创建一个所谓的图,它将我们的输入映射到所需的输出。
- 同样,我们将创建一个
session,然后我们将运行我们的操作:
with tf.Session() as sess:
print("a+b=" + str(sess.run(add, feed_dict={a: 2, b: 3})))
print("a*b=" + str(sess.run(mul, feed_dict={a: 2, b: 3})))
在这种情况下,我们必须告诉它值是什么,然后它就会按照我们预期的那样执行,如下所示:
没有什么特别激动人心的——这只是让我们对 TensorFlow 正在做什么有一个基本的了解。我们将利用一些高级库来完成本章,但如果我们想要在未来更进一步,这是很重要的。
同样,我们将进行矩阵乘法。如前所述,常数可以不仅仅是标量。在这种情况下,我们定义矩阵,一个 2x2 的矩阵和一个 2x1 的矩阵,按照以下步骤:
- 我们定义我们的矩阵如下:
#Matrix multiplication
matrix1 = tf.constant([[1., 2.],[9.0,3.14159]])
matrix2 = tf.constant([[3.],[4.]])
- 然后,我们告诉它进行矩阵乘法,如下所示:
product = tf.matmul(matrix1, matrix2)
- 我们创建我们的会话:
with tf.Session() as sess:
result = sess.run(product)
print(result)
现在我们运行它,然后打印结果。输出如下:
再次强调,这非常基础,但在未来非常重要。我们不会在本课中定义我们的完整网络,因为这非常复杂,执行起来也非常耗时,但只是简要提及创建我们自己的 CNN 的一般步骤。
我们将创建所谓的层,定义我们的输入,然后创建一系列层并将它们堆叠起来,定义它们是如何连接的。然后我们找到输出层,然后我们必须定义一些其他事情,比如我们如何训练以及我们如何评估它。
这个代码如下:
#creating a convolutional neural network (skeleton--not complete code!)
# create a convolutional (not fully connected) layer...
conv1 = tf.layers.conv2d(x, 32, 5, activation=tf.nn.relu)
# and down-sample
conv1 = tf.layers.max_pooling2d(conv1, 2, 2)
# create second layer
conv2 = tf.layers.conv2d(conv1, 64, 3, activation=tf.nn.relu)
conv2 = tf.layers.max_pooling2d(conv2, 2, 2)
# flatten to 1D
fc1 = tf.contrib.layers.flatten(conv2)
# create fully-connected layer
fc1 = tf.layers.dense(fc1, 1024)
# final (output/prediction) layer
out = tf.layers.dense(fc1, n_classes)
#...training code etc.
再次强调,这只是为了我们的知识。深度学习是一个困难的课题,确定必要的架构以及如何精确训练,这超出了本章的范围(尽管我会邀请你了解更多关于它的内容)。在这里,我们只是看看我们如何利用已经完成的工作——但如果你想要更进一步,这就是你开始的地方。
在下一节中,我们将看到如何使用预训练的模型 Inception 来执行图像分类。
使用 Inception 进行图像分类
在本节中,我们将使用来自 Google 的预训练模型 Inception 来执行图像分类。然后我们将继续构建我们自己的模型——或者至少对模型进行一些再训练,以便在我们的图像上进行训练并对我们的物体进行分类。
现在,我们想看看我们如何使用已经训练好的模型,从头开始重新生成将花费很多时间。让我们从代码开始。
让我们回到 Jupyter Notebook。Notebook 文件可以在以下链接找到:github.com/PacktPublishing/Computer-Vision-Projects-with-OpenCV-and-Python-3/Chapter04。
为了运行代码,我们需要从 TensorFlow 的网站上下载一个文件,如下链接所示:download.tensorflow.org/models/image/imagenet/inception-2015-12-05.tgz。这是 Inception 模型。
该模型是在 2015 年训练的。它包含几个定义模型的文件,称为 graph,它定义了输入图像和输出分类之间的输入输出关系。
它还包含一些标签数据,因为输出不是类别名称;它是数字。这是从谷歌自己的 TensorFlow 示例中修改的,以便更容易理解和在 Jupyter Notebook 中运行,并减少代码量。然而,我们需要进行更改。
下载文件并完全解压。在 Windows 上,读者可能会使用 7-Zip,这将生成一个 TGZ 文件。确保然后解压缩 TGZ 文件以获取 TXT、PBTXT 和 PB 文件,特别是 PB 文件,因为它是实际包含训练模型的文件。
我们创建了一个名为 inceptiondict 的文件,而不是使用谷歌自己复杂的文件来映射类别数字到类别名称。
让我们看看 inceptiondict 文件:
这个文件有千个类别。自己训练这个模型将花费非常长的时间,但我们不必这样做;我们可以利用这一点,并在后面构建在此基础上。
如果我们想知道在这个预构建模型中我们能够识别哪些类型的图像,这个文件很有趣。文件中有很多动物,一些常见物品,水果,乐器,不同种类的鱼;它甚至能够识别日本的游戏 shoji。
我们将这个文件导入为一个名为 inceptiondict 的字典,它将数字映射到相应的类别描述;例如,类别 1 映射到描述 "goldfish, Carassius auratus"。
让我们探索主要代码。首先,我们将文件导入为 inceptiondict:
#The main code:
#"image" is a filename for the image we want to classify
#load our inception-id to English description dictionary
from inceptiondict import inceptiondict
现在,我们有了 run_inference_on_image 函数,其中 image 是一个文件名。它不是文件数据——我们还没有加载它——只是我们想要分类的图像的文件名。
然后,我们检查文件名是否存在,如果不存在则创建一个错误。如果存在,我们将使用 TensorFlow 自身的加载机制来读取该文件名,如下所示:
def run_inference_on_image(image):
#load image (making sure it exists)
if not tf.gfile.Exists(image):
tf.logging.fatal('File does not exist %s', image)
image_data = tf.gfile.FastGFile(image, 'rb').read()
我们之前讨论过图文件。将 classify_image_graph_def.pb 这个关键的文件从 TGZ 文件解压到当前目录。使用 TensorFlow 自身的文件加载机制以二进制方式打开它,然后我们将从这个文件创建我们的图定义,如下所示:
# Load our "graph" file--
# This graph is a pretrained model that maps an input image
# to one (or more) of a thousand classes.
# Note: generating such a model from scratch is VERY computationally
# expensive
with tf.gfile.FastGFile('classify_image_graph_def.pb', 'rb') as f:
graph_def = tf.GraphDef()
graph_def.ParseFromString(f.read())
_ = tf.import_graph_def(graph_def, name='')
在这里,我们只是加载预训练模型。谷歌已经为我们完成了艰苦的工作,我们将从那里读取。
然后,就像我们之前做的那样,我们需要创建我们的 TensorFlow 会话。我们通过以下 with 语句来完成:
#create a TF session to actually apply our model
with tf.Session() as sess:
# Some useful tensors:
# 'softmax:0': A tensor containing the normalized prediction across
# 1000 labels.
# 'pool_3:0': A tensor containing the next-to-last layer containing 2048
# float description of the image.
# 'DecodeJpeg/contents:0': A tensor containing a string providing JPEG
# encoding of the image.
# Runs the softmax tensor by feeding the image_data as input to the graph.
softmax_tensor = sess.graph.get_tensor_by_name('softmax:0')
predictions = sess.run(softmax_tensor,
{'DecodeJpeg/contents:0': image_data})
predictions = np.squeeze(predictions)
#The output here is a 1000 length vector, each element between 0 and 1,
#that sums to 1\. Each element may be thought of as a probability
#that the image corresponds to a given class (object type, e.g. bird,
#plane, and so on).
这个模型已经具有多个称为张量的层。我们需要提取 softmax 层。
我们模型的输出不仅仅是检测到 100% 的东西;它为每一个东西都给出一个概率。例如,我们可能会有 90% 的概率认为我们的图像是某种猫,20% 的概率认为它是一只松鼠,0.01% 的概率是椅子或其他东西。是的,有时你确实会得到一些非常离谱的分类,尽管通常这些概率都非常小。
为每一千个类别中的每一个都计算了一部分概率。当然,其中绝大多数都是零或非常非常接近零。
我们想要提取倒数第二层,其中包含 2048 个对图像和输入图像的 JPEG 编码的详细描述。请注意,我们没有以二维或三维向量(或他们称之为张量)的形式加载原始图像数据——我们仍然以 JPEG 编码的形式拥有它。我们只是在定义变量以提取输出和找到输入。
NumPy 的 squeeze 函数可以去除所有单维。所以,如果我们有一个 1 行 1000 列的矩阵,这将把它转换成 1000 行 1 列。
好的,所以,我们理解了会话中的输入和输出。仅仅为了理解,我们只想提取前五个预测,并且我们将过滤掉概率小于 10% 的预测。最多我们只能得到五个预测,但通常会更少,因为我们忽略了低于 10% 的任何东西,如下所示:
#We only care about the top 5 (at most) predictions, and ones that have
#at least a 10% probability of a match
num_top_predictions= 5
top_k = predictions.argsort()[-num_top_predictions:][::-1]
for node_id in top_k:
human_string = inceptiondict[node_id]
score = predictions[node_id]
if score > 0.1:
print('%s (score = %.5f)' % (human_string, score))
我们运行模型并得到图像的输出,然后按我们的前五个排序。然后我们遍历这些顶级预测,通过运行输出的 node_id 通过我们的 inceptiondict 字典将其转换为人类字符串。我们读取 score,然后只有当 score 大于 10% 时才打印输出。
我们只是在定义函数,我们并没有运行它,所以这应该运行得非常快。
现在,我们将对这个图像运行这个程序。在 sample_imgs 子目录中有一些样本图像。我们想要测试这个,所以只需取消注释以下这些行中的一行来定义我们的 image 变量:
#uncomment out one of these lines to test
image='sample_imgs/cropped_panda.jpg'
# image='sample_imgs/dog.jpg'
# image='sample_imgs/bicycle2.jpg'
# image='sample_imgs/garbagecan.jpg'
# image='sample_imgs/bunny.jpg'
# image='sample_imgs/trombone.jpg'
# image='sample_imgs/treasurechest.jpg'
# image='sample_imgs/hotdog.jpg'
figure()
imshow(imread(image))
run_inference_on_image(image)
然后,我们将创建一个图形,使用 imshow 函数查看我们看到的内容,然后使用 run_inference_on_image 函数,该函数将输出结果。
要运行前面的代码块并使用 cropped_panda.jpg 图片,取消注释熊猫图片行。我们可以在以下输出中看到图片。它以大约 90% 的概率将其分类为 panda、giant panda 或其他同义词,如下所示:
让我们在其他东西上试一试。比如我们的 bicycle2.jpg 文件?在取消注释 bicycle2.jpg 行的同时,注释掉 cropped_panda.jpg 行,我们得到以下输出:
它以 91% 的概率将图片分类为 mountain bike。
我们在这里变得有点具体了。现在让我们用 garbagecan.jpg 文件来试一试:
在这里它的置信度并不高,只有大约 67%的概率被分类。有时这就是我们能做的最好了,但这并不太糟糕。这是最可能的结果。
让我们尝试bunny.jpg文件:
好吧,我们有 87%的把握认为这是一只兔子。看起来相当不错。
现在,让我们尝试trombone.jpg文件:
哇,非常确定。这张图片是长号的可能性超过 99%——非常好。
如果你是一个热门电视剧的粉丝,你可能想知道分类器是否能识别出热狗。答案是肯定的:
它确实识别出了一个热狗,置信度为 97%。
最后,我们将我们的分类器运行在dog.jpg图片上,如下所示:
显然,训练这个模型的人是一个狗爱好者,所以他们定义了多个不同的狗类。我们得到了爱尔兰猎狼犬、俄罗斯猎狼犬、瞪羚猎犬和其他一些返回的结果。它似乎认为它属于那些类别之一!
这工作得相当好。如果我们需要的恰好是那些 1000 个类别之一,那么我们在这里就做得很好。你应该能够将 Jupyter Notebook 中的代码适应你的需求。希望深度学习和图像分类不再像以前那样令人生畏。
因此,我们将继续到下一部分,我们将使用我们自己的图片进行一些重新训练,并分类那些尚未在谷歌训练数据库中的对象。
使用我们自己的图片重新训练
在本节中,我们将超越我们使用预构建分类器所做的工作,并使用我们自己的图片和标签。
我首先应该提到的是,这并不是真正从头开始用深度学习进行训练——训练整个系统需要多个层次和算法,这非常耗时——但我们可以利用一种叫做迁移学习的技术,其中我们使用与大量图像训练的前几层,如下面的图所示:
深度学习的一个注意事项是,拥有几百或几千张图片是不够的。你需要数十万甚至数百万个样本才能得到好的结果,而收集这么多数据是非常耗时的。此外,在个人电脑上运行它,我预计大多数人都在使用,在计算上是不切实际的。
但好消息是,我们可以从我们的现有模型中提取层,并在最后进行一些调整,从而得到非常好的结果。我们通过使用在数十万或数百万张图片上训练的输入特征来利用预训练,并将它们转移到模型以前从未见过的图像类型。
要做到这一点,我们从 TensorFlow Hub (www.tensorflow.org/hub/) 借用了一些代码。但是,我们必须做一些调整,以便它能够以更少的代码运行,并且可以轻松地将其放入我们的 Jupyter Notebook 中并运行。
为了开始,我们需要一些用于训练的图片,以及不同的训练方法。谷歌很友好地提供了一个名为 flower_photos 的样本,链接如下:download.tensorflow.org/example_images/flower_photos.tgz。再次强调,它是一个 TGZ 文件,所以请下载文件并彻底解压。
你将得到一个 flower_photos 目录,其中将包含不同种类花朵的子目录,如郁金香、蒲公英等,这些种类并未包含在最初的 1,000 个类别中。这些目录名将作为这些图片的标签。我们只需要解压它们,然后在我们的代码中输入花朵照片。
获取大量照片的一个便宜方法是使用 Chrome 的 Fatkun 批量下载插件 (chrome.google.com/webstore/detail/fatkun-batch-download-ima/nnjjahlikiabnchcpehcpkdeckfgnohf?hl=en)。使用这个插件,我们可以去像 Google 图片搜索这样的地方,搜索我们想要的任何类型的对象——动物、食物等等——并且可以快速地抓取数百张图片。
Firefox 或你使用的任何网络浏览器都有类似的插件。只要你不介意使用这类图片,如果它们能满足你的需求,那么这是一种很好的做法。
在你完成花朵照片的处理后,我建议你抓取自己的图片。想想你想要训练的内容,想想你认为会有用的内容。尽量获取每个类别的至少 100 张图片,并抓取多个类别。
为了说明目的,我决定对一些玩具进行分类。也许你正在经营一家玩具店,正在清点库存,或者你是一位收藏家,想要了解里面具体有什么——你只是有一堆照片,想要对它们进行分类。
我创建了四个子文件夹,分别命名为 barbie、gi joe、my little pony 和 transformers,如下所示:
每个文件夹都包含每种类型超过 100 张的图片。文件名并不重要——只是目录名将被用于标签。
因此,你可以测试它是否工作,你需要将一些图片分离出来。如果你在训练过的图片上进行测试,那么你就是在作弊——你实际上不知道你的模型是否已经泛化。所以,请确保从该目录中提取一些图片,并将它们暂时放入一个单独的目录中。
重新训练的代码在 Jupyter Notebook 文件中本身就有介绍,所以我们不会从头到尾讲解。我们创建了一个名为retrained.py的文件,它是基于 TensorFlow Hub 版本,但更容易集成到现有代码中,并且许多变量已经处理好了。
我们需要做的只是导入retrain函数,然后在我们toy_images文件夹上重新训练,如下所示:
#pull the function from our custom retrain.py file
from retrain import retrain
#Now we'll train our model and generate our model/graph file 'output_graph.pb'
retrain('toy_images')
这通常需要一段时间。如果你在flower_photos目录上运行代码,那可能需要半小时,尤其是在 CPU 上而不是 GPU 上。toy_images示例将花费更少的时间,因为图像数量较少。
在机器学习中进行训练通常是耗时最多的部分;这就是为什么你的电脑会长时间占用。将图像通过分类器运行是很快的,就像我们之前看到的,但训练可能需要几分钟、几小时、几天,甚至可能更长。在这种情况下,我们可能需要半小时,这取决于有多少图像。
几分钟后,我们的retrained函数成功运行,输出如下:
我已经降低了retrain函数的一些详细程度,否则它会输出很多没有太多意义的消息。如果你想检查代码是否成功运行,可以进入代码中将其调高,但只要一切设置正确,它应该会正常运行。
让我们确认它是否工作:
#Confirm that it worked
!ls *.pb
#should see file "output_graph.pb"
我们将寻找那个.pb(Python 二进制文件)文件,它将是我们所做工作的输出。所以,那就是模型,输入输出模型,或者通常在 TensorFlow 中称为图。
运行代码后,我们应该得到以下输出:
我们有一个名为output_graph.pb的文件。那就是我们刚刚创建的;你应该能在你的目录中看到这个文件。
运行你的图像的代码并不那么复杂。加载我们的output_graph.pb图文件与我们之前加载 Inception 模型时所做的类似,如下所示:
#Let's load some code that will run our model on a specified image
def load_graph(model_file):
graph = tf.Graph()
graph_def = tf.GraphDef()
with open(model_file, "rb") as f:
graph_def.ParseFromString(f.read())
with graph.as_default():
tf.import_graph_def(graph_def)
return graph
read_tensor_from_image_file函数有助于从图像文件中读取数据,如下所示:
def read_tensor_from_image_file(file_name,
input_height=299,
input_width=299,
input_mean=0,
input_std=255):
input_name = "file_reader"
output_name = "normalized"
file_reader = tf.read_file(file_name, input_name)
if file_name.endswith(".png"):
image_reader = tf.image.decode_png(
file_reader, channels=3, name="png_reader")
elif file_name.endswith(".gif"):
image_reader = tf.squeeze(
tf.image.decode_gif(file_reader, name="gif_reader"))
elif file_name.endswith(".bmp"):
image_reader = tf.image.decode_bmp(file_reader, name="bmp_reader")
else:
image_reader = tf.image.decode_jpeg(
file_reader, channels=3, name="jpeg_reader")
float_caster = tf.cast(image_reader, tf.float32)
dims_expander = tf.expand_dims(float_caster, 0)
resized = tf.image.resize_bilinear(dims_expander, [input_height, input_width])
normalized = tf.divide(tf.subtract(resized, [input_mean]), [input_std])
sess = tf.Session()
result = sess.run(normalized)
return result
这里有一些默认值,但它们并不重要。图像不一定需要是299乘以299。我们这里只处理 JPEG 文件,但如果我们有 PNG、GIF 或 BMP 格式的文件,模型也能处理。我们只需解码图像,将它们放入我们的变量中,并存储和返回它们。
如前所述,标签来自目录。以下代码将加载创建的output_labels.txt,它将从output_labels.txt中加载,这将是我们的一种字典,由我们的子目录名称定义:
def load_labels(label_file):
label = []
proto_as_ascii_lines = tf.gfile.GFile(label_file).readlines()
for l in proto_as_ascii_lines:
label.append(l.rstrip())
return label
以下代码显示了label_image函数。为了找到你已知的图像,给出正确的文件名,但有一个默认值以防万一:
def label_image(file_name=None):
if not file_name:
file_name = "test/mylittlepony2.jpg"
model_file = "./output_graph.pb"
label_file = "./output_labels.txt"
input_height = 299
input_width = 299
input_mean = 0
input_std = 255
input_layer = "Placeholder"
output_layer = "final_result"
我为了简单起见硬编码了这些。如果你想改变东西,你可以,但我认为把它写在那里会让事情更容易阅读和理解。
我们加载我们的图文件,从图像文件中读取数据,并从我们创建的新模型中读取层名称,如下所示:
graph = load_graph(model_file)
t = read_tensor_from_image_file(
file_name,
input_height=input_height,
input_width=input_width,
input_mean=input_mean,
input_std=input_std)
input_name = "import/" + input_layer
output_name = "import/" + output_layer
input_operation = graph.get_operation_by_name(input_name)
output_operation = graph.get_operation_by_name(output_name)
我们将只读取输入和输出层。
我们定义了我们的会话,并从output_operation获取结果。再次,我们将它排序到top_k变量中,并打印结果:
with tf.Session(graph=graph) as sess:
results = sess.run(output_operation.outputs[0], {
input_operation.outputs[0]: t
})
results = np.squeeze(results)
top_k = results.argsort()[-5:][::-1]
labels = load_labels(label_file)
for i in top_k:
print(labels[i], results[i])
课程种类繁多,但实际上我们将会看到这里始终只有一个结果。
让我们再次尝试我们的代码。正如讨论的那样,我们将一些图像分离到一个单独的目录中,因为我们不想在训练图像上测试,那样证明不了什么。
让我们在我们的第一个transformers1.jpg图像上测试重新训练的模型。模型将显示图像并告诉我们分类结果:
#label_image will load our test image and tell us what class/type it is
#uncomment one of these lines to test
#
test_image='test/transformers1.jpg'
# test_image='test/transformers2.jpg'
# test_image='test/transformers3.jpg'
# test_image='test/mylittlepony1.jpg'
# test_image='test/mylittlepony2.jpg'
# test_image='test/mylittlepony3.jpg'
# test_image='test/gijoe1.jpg'
# test_image='test/gijoe2.jpg'
# test_image='test/gijoe3.jpg'
# test_image='test/barbie1.jpg'
# test_image='test/barbie2.jpg'
# test_image='test/barbie3.jpg'
#display the image
figure()
imshow(imread(test_image))
#and tell us what the classification result is
label_image(test_image)
上述代码的输出如下:
模型以非常高的概率将图像分类为transformers。由于我们的图像足够独特,并且类别较少,它将工作得非常好。我们看到有 99.9%的概率这张照片是变形金刚,有很小概率是 G.I. Joe,而且肯定不是芭比或小马宝莉。
我们可以使用*Ctrl + /来在 Jupyter Notebook 中注释和取消注释代码行,并按Ctrl + Enter再次使用transformer2.jpg图片运行代码:
输出再次是transformers。这次模型认为它比 G.I. Joe 稍微更有可能是芭比,但概率微不足道。
让我们再次尝试使用mylittlepony1.jpg图片:
是的,它确实看起来像my little pony子文件夹中的其他图片。
让我们再拍一张照片,mylittlepony3.jpg:
再次,没有问题对图像进行分类。让我们也看看gijoe2.jpg:
有很高的概率是gi joe,transformers和barbie比my little pony更可能,但再次,所有这些概率都是微不足道的——它肯定是一个gi joe。
最后,让我们在barbie1.jpg上尝试:
再次,肯定被分类为barbie,my little pony是第二可能的选择,可能是因为颜色;芭比和小马宝莉玩具上通常有更多的粉色和紫色。
现在我们知道如何使用我们自己的图像来重新训练一个现有的模型。不需要太多的编码或 CPU 时间,我们可以为我们的目的创建一个定制的图像分类器。
在下一节中,我们将讨论如何在你的 GPU 的帮助下加速计算。
使用 GPU 加速计算
在本节中,我们将简要讨论如何使用 GPU 加速计算。好消息是 TensorFlow 实际上在利用 GPU 方面非常聪明,所以如果你已经设置好了一切,那么这相当简单。
让我们看看如果 GPU 设置正确,事物看起来会是什么样子。首先,按照以下方式导入 TensorFlow:
import tensorflow
接下来,我们打印tensorflow.Session()。这仅仅给我们提供了关于我们的 CPU 和 GPU(如果它已正确设置)的信息:
print(tensorflow.Session())
输出如下:
如我们从输出中可以看到,我们使用的是一块配备 GeForce GTX 970M 的笔记本电脑,它是 CUDA 兼容的。这是运行带有 GPU 的 TensorFlow 所必需的。如果一切设置正确,你将看到与前面输出非常相似的消息,包括你的 GPU,你的卡型号以及它的内存等详细信息。
TensorFlow 在这方面很聪明。我们可以自己覆盖它,但只有当我们知道自己在做什么,并且愿意投入额外的工作时,这才有好主意。除非我们知道自己在做什么,否则我们不会获得改进的性能,所以还是保留默认设置。
后续章节在 CPU 上运行良好,只是速度不是特别快。
关于 TensorFlow 使用 GPU 的坏消息是,设置它并不完全直接。例如,我们之前介绍了pip命令,比如pip install tensorflow和pip install tensorflow-gpu,这是一个起点,但我们仍然需要安装 CUDA。
我已安装版本 9.0。如果你有一块 Quadro GPU 或某种工作站,Tesla,或者那些专用卡,你应该使用 CUDA 版本 9.1。它是平台相关的,取决于你有什么样的 GPU,以及更具体地说,你有什么样的操作系统,所以我们不能在这里详细介绍。
需要知道的重要一点是,我们不仅需要安装tensorflow-gpu,我们还需要安装 CUDA。从 NVIDIA 网站下载并安装适用于您的操作系统的 CUDA(developer.nvidia.com/cuda-toolkit)。
此外,TensorFlow 还需要NVIDIA CUDA®深度神经网络(cuDNN)库,这是一个 Windows 的大 DLL 文件,或 Linux 的共享对象(.SO)文件。macOS 的情况也类似。它只是一个文件,需要放在你的路径中。我通常将其复制到我的CUDA目录中。
如果你确实有一块,尝试安装 CUDA,尝试安装 cuDNN,并尝试让 TensorFlow 运行起来。希望这能加速你的计算。
摘要
在本章中,我们学习了如何使用基于 TensorFlow 的预训练模型来分类图像。然后我们重新训练我们的模型以处理自定义图像。
最后,我们简要概述了如何通过在 GPU 上执行计算来加速分类过程。
通过本书中涵盖的示例,你将能够使用 Python、OpenCV 和 TensorFlow 来执行你的自定义项目。
1万+

被折叠的 条评论
为什么被折叠?



