cv-proj-ocv-py-merge-0
Python OpenCV 计算机视觉项目(全)
原文:
annas-archive.org/md5/61756cde4c66978a12599ccffeb53dae译者:飞龙
前言
在这本书中,您将学习如何利用 Python、OpenCV 和 TensorFlow 的强大功能来解决计算机视觉中的问题。Python 是快速原型设计和开发图像处理和计算机视觉生产级代码的理想编程语言,它具有稳健的语法和丰富的强大库。
本书将是您设计和开发针对现实世界问题的生产级计算机视觉项目的实用指南。您将学习如何为主要的操作系统设置 Anaconda Python,并使用计算机视觉的尖端第三方库,您还将学习分类图像和视频中的检测和识别人类的最先进技术。通过本书的结尾,您将获得使用 Python 及其相关库构建自己的计算机视觉项目所需的技能。
本书面向对象
希望使用机器学习和 OpenCV 的强大功能构建令人兴奋的计算机视觉项目的 Python 程序员和机器学习开发者会发现这本书很有用。本书的唯一先决条件是您应该具备扎实的 Python 编程知识。
要充分利用这本书
在 Python 及其包(如 TensorFlow、OpenCV 和 dlib)中具备一些编程经验,将帮助您充分利用这本书。
需要一个支持 CUDA 的强大 GPU 来重新训练模型。
下载示例代码文件
您可以从 www.packt.com 的账户下载本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packt.com/support 并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
在 www.packt.com 登录或注册。
-
选择支持选项卡。
-
点击代码下载和勘误表。
-
在搜索框中输入书籍名称,并遵循屏幕上的说明。
下载文件后,请确保使用最新版本解压或提取文件夹:
-
WinRAR/7-Zip for Windows
-
Zipeg/iZip/UnRarX for Mac
-
7-Zip/PeaZip for Linux
本书代码包也托管在 GitHub 上,地址为 github.com/PacktPublishing/Computer-Vision-Projects-with-OpenCV-and-Python-3。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还有其他来自我们丰富的图书和视频目录的代码包可供选择,请访问 github.com/PacktPublishing/。查看它们!
下载彩色图像
我们还提供了一个包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/9781789954555_ColorImages.pdf。
使用的约定
本书使用了多种文本约定。
CodeInText: 表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“word_counts.txt文件包含一个词汇表,其中包含我们从训练模型中得到的计数,这是我们的图像标题生成器所需要的。”
代码块按照以下方式设置:
testfile = 'test_images/dog.jpeg'
figure()
imshow(imread(testfile))
任何命令行输入或输出都按照以下方式编写:
conda install -c menpo dlib
粗体: 表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“点击 下载 按钮。”
警告或重要注意事项看起来像这样。
小贴士和技巧看起来像这样。
联系我们
我们读者的反馈总是受欢迎的。
一般反馈: 如果你对此书的任何方面有疑问,请在邮件主题中提及书名,并通过customercare@packtpub.com发送邮件给我们。
勘误: 尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果你在这本书中发现了错误,我们将不胜感激,如果你能向我们报告这个错误。请访问www.packt.com/submit-errata,选择你的书籍,点击勘误提交表单链接,并输入详细信息。
盗版: 如果你在互联网上以任何形式遇到我们作品的非法副本,如果你能提供位置地址或网站名称,我们将不胜感激。请通过发送链接至copyright@packt.com与我们联系。
如果你有兴趣成为作者: 如果你有一个你擅长的主题,并且你对撰写或为书籍做出贡献感兴趣,请访问authors.packtpub.com。
评论
请留下评论。一旦你阅读并使用了这本书,为什么不在你购买它的网站上留下评论呢?潜在读者可以看到并使用你的客观意见来做出购买决定,我们 Packt 可以了解你对我们的产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!
关于 Packt 的更多信息,请访问packt.com。
第一章:设置 Anaconda 环境
欢迎来到使用 OpenCV 和 Python 3 的计算机视觉项目。如果你是 OpenCV 和计算机视觉的新手,这本书你可能想看看。
在本章中,我们将安装本书中将要使用到的所有必需工具。我们将处理 Python 3、OpenCV 和 TensorFlow。
你可能想知道:为什么我应该使用 Python 3,而不是 Python 2?你问题的答案可以在 Python 自己的网站上找到:
"Python 2 是遗留的,Python 3 是语言的现在和未来。"
我们在这里展望未来,如果我们想要使我们的代码具有前瞻性,最好使用 Python 3。如果你使用的是 Python 2,这里的一些代码示例可能无法运行,因此我们将安装 Python 3 并使用它来完成本书的所有项目。
在本章中,我们将涵盖以下主题:
-
介绍和安装 Python 和 Anaconda
-
安装额外的库
-
探索 Jupyter Notebook
介绍和安装 Python 和 Anaconda
我们首先需要 Python 3。安装它的最佳方式是下载 Continuum Analytics 和 Anaconda 发行版。
Anaconda 是一个功能齐全的 Python 发行版,附带大量包,包括数值分析、数据科学和计算机视觉。它将使我们的生活变得更加容易,因为它为我们提供了基础 Python 发行版中不存在的库。
Anaconda 最好的部分是它为我们提供了conda包管理器,以及pip,这使得为我们的 Python 发行版安装外部包变得非常容易。
让我们开始吧。
安装 Anaconda
我们将首先设置 Anaconda 和 Python 发行版,按照以下步骤进行:
- 访问 Anaconda 网站,使用以下链接www.anaconda.com/download。你应该会看到一个类似于以下截图的着陆页:

- 接下来,选择你的操作系统,下载包含 Python 3.7 的最新版本的 Anaconda 发行版。点击下载按钮,如图所示:

Windows 的安装程序是图形化的;然而,你可能需要为 macOS 或 Linux 使用命令行安装程序。
安装设置文件非常简单,所以我们不会在这里逐个步骤说明。
- 当你正确安装了所有软件并定义了路径变量后,前往命令提示符,通过输入
where python命令来确保一切正常。这会显示 Python 安装的所有目录。你应该会看到类似于以下截图的内容:

如前一个截图所示,我们看到 Python 的第一个实例在我们的 Anaconda 发行版中。这意味着我们可以继续我们的 Python 程序。
在 macOS 或 Linux 中,命令将是which python而不是where python。
- 现在,让我们确保我们拥有我们的其他工具。我们的第一个工具将是 IPython,它本质上是一种用于多种编程语言的交互式计算命令壳。我们将使用
where ipython命令来检查它,如图所示:

- 下一个我们将检查的包是
pip工具,它是 Python 安装程序包。我们使用where pip命令来完成此操作,如图所示:

- 下一个要检查的工具是
conda包,它是 Anaconda 内置的包管理器。这是通过where conda命令完成的,如图所示:

我们现在应该可以使用 Python 了。
如果你运行which python在 macOS 或 Linux 上,并且它显示类似user/bin/Python的内容,这意味着 Python 可能未安装或不是我们路径中的第一项,因此我们应该根据我们的系统进行修改。
在下一节中,我们将介绍安装额外的库,如 OpenCV、TensorFlow、dlib 和 Tesseract,这些库将用于本书中的项目。
安装额外的库
我们将在本节中安装的所有包对我们即将到来的项目至关重要。所以,让我们开始吧。
安装 OpenCV
要获取 OpenCV,请访问以下链接:anaconda.org/conda-forge/opencv。技术上,我们不需要访问网站来安装此包。该网站仅显示 OpenCV 的各种版本以及我们可以在其上安装的所有不同系统。
将网站上的安装命令复制并粘贴到命令提示符中,然后运行,如图所示:

上述命令是一个简单、平台无关的方法来获取 OpenCV。还有其他获取它的方法;然而,使用此命令可以确保我们安装的是最新版本。
安装 dlib
我们需要从 Anaconda 发行版安装 dlib,类似于 OpenCV。正如安装 OpenCV 一样,安装 dlib 是一个简单的过程。
运行以下命令:
conda install -c menpo dlib
你将得到以下输出:

这将花费大约 10 到 20 秒的时间运行。如果一切顺利,我们应该可以使用 dlib 了。
安装 Tesseract
Tesseract 是 Google 的光学字符识别库,并且不是原生的 Python 包。因此,有一个 Python 绑定,它调用可执行文件,然后可以手动安装。
访问 Tesseract 的 GitHub 仓库,该仓库位于以下链接:github.com/tesseract-ocr/tesseract。
滚动到 GitHub 自述文件中的安装 Tesseract部分。在这里,我们有两个选项:
-
通过预构建的二进制包安装
-
从源代码构建
我们想要通过预构建的二进制包来安装它,因此点击该链接。我们也可以从源代码构建,如果我们想的话,但这并不真正提供任何优势。Tesseract Wiki 解释了在各个不同的操作系统上安装它的步骤。
由于我们使用的是 Windows,并且我们想要安装一个预构建的版本,请点击 UB Mannheim 的 Tesseract 链接,在那里您可以找到所有最新的设置文件。从网站上下载最新的设置文件。
下载完成后,运行安装程序或执行命令。然而,这并不会将 Tesseract 添加到您的路径中。我们需要确保它在您的路径中;否则,当您在 Python 中调用 Tesseract 时,您将收到一个错误消息。
因此,我们需要找出 Tesseract 的位置并修改我们的路径变量。为此,在命令提示符中输入where tesseract命令,如下截图所示:

一旦您有了二进制包,使用pip命令将 Python 绑定应用到这些包上。使用以下命令:
$ pip install tesseract
$ pip install pytesseract
现在应该可以使用 Tesseract 了。
安装 TensorFlow
最后但同样重要的是,我们将安装 TensorFlow,这是一个用于跨各种任务的数据流编程的软件库。它通常用于机器学习应用,如神经网络。
要安装它,请访问以下链接的 TensorFlow 网站:tensorflow.org/install/。网站包含所有主要操作系统的说明。
由于我们使用的是 Windows,安装过程非常简单。我们只需在命令提示符中运行pip install tensorflow命令,如下截图所示:

如前一个截图所示,TensorFlow 已经安装在系统上,因此它表示要求已满足。现在我们应该可以使用 TensorFlow 了。
使用以下命令安装tensorflow-hub:
pip install tensorflow-hub
接下来,使用以下命令安装tflearn:
pip install tflearn
最后,Keras 是一个高级接口,可以使用以下命令安装:
pip install keras
我们已经安装了 OpenCV、TensorFlow、dlib 和 Tesseract,因此我们应该可以使用我们书籍的工具了。我们的下一步将是探索 Jupyter Notebook,这应该很有趣!
探索 Jupyter Notebook
现在我们已经安装了库,我们准备好开始使用 Jupyter Notebook 了。Jupyter Notebook 是一种创建交互式代码和小部件的好方法。它允许我们创建带有实时代码和实验的交互式演示,就像我们现在所做的那样。
如果使用 Anaconda 正确设置了一切,我们可能已经安装了 Jupyter。让我们现在看看 Jupyter Notebook。
在你的代码文件所在的目录中打开命令提示符,然后运行 jupyter notebook 命令。这将打开一个在命令执行目录中的网络浏览器。这应该会得到一个类似于以下截图的界面:

接下来,打开 .ipynb 文件,以便你可以探索 Jupyter Notebook 的基本功能。一旦打开,我们应该看到一个类似于以下截图的页面:

如所示,有一些块(称为 cells),我们可以在这里输入 Python 命令和 Python 代码。我们还可以输入其他命令,也称为 magic commands,这些命令本身不是 Python 的一部分,但允许我们在 Jupyter 或 IPython(Python 的交互式外壳)中做一些很酷的事情。开头 % 的意思是该命令是一个 magic command。
这里最大的优点是我们可以执行单个代码行,而不是一次输入整个代码块。
如果你刚开始使用 Jupyter Notebook,请访问以下链接:www.cheatography.com/weidadeyue/cheat-sheets/jupyter-notebook/。在这里,他们列出了 Jupyter Notebook 的键盘快捷键,这对于快速代码测试非常有用。
让我们回顾一下前面截图中的某些命令,并看看它们的作用,如下所示:
-
如第一个单元格所示,
%pylab notebook命令导入了许多非常有用且常见的库,特别是 NumPy 和 PyPlot,而无需我们显式调用导入命令。它还简化了 Notebook 的设置。 -
同样在第一个单元格中,我们指定我们将要工作的目录,如下所示:
%cd C:\Users\<user_name>\Documents\<Folder_name>\Section1-Getting_started
这导致了以下输出:

到目前为止,一切顺利!
- 下一个单元格显示了如何导入我们的库。我们将导入 OpenCV、TensorFlow、dlib 和 Tesseract,只是为了确保一切正常,没有出现任何令人不快的惊喜。这是通过以下代码块完成的:
import cv2
import tensorflow as tf
import dlib
import pytesseract as ptess
如果在这里收到错误信息,请按照说明仔细重新安装库。有时事情确实会出错,这取决于我们的系统。
- 图表中的第三个单元格包含导入 TensorFlow 图模块的命令。这可以在 Notebook 内部获取函数帮助时派上用场,如下所示:
tf.Graph?
我们将在 第七章 中讨论这个函数,使用 TensorFlow 进行深度学习图像分类。
- Jupyter Notebooks 的另一个优点是,我们可以在单元格中直接运行 shell 命令。如图表中的第四个单元格所示(此处重复),
ls命令显示了我们从该目录中工作的所有文件:

- 在这本书中,我们将处理很多图像,所以我们会希望在笔记本中直接查看图像。使用
imread()函数从你的目录中读取图像文件。之后,你的下一步是创建一个figure()小部件来显示图像。最后,使用imshow()函数实际显示图像。
整个过程总结在下面的截图里:

这很棒,因为我们有灵活的部件。
- 通过抓住右下角,我们可以将其缩小到一个合理的尺寸,以便查看带有像素轴的颜色图像。我们还有平移选项可用。点击它,我们可以平移图像并框选放大。按主页按钮将重置原始视图。
我们将想要查看我们的图像、处理后的图像等等——如前所述,这是一个非常方便且简单的方法。我们还可以使用 PyLab 内联,这在某些情况下很有用,我们将会看到。
- 正如我们所知,计算机视觉的一部分是处理视频。要在笔记本中播放视频,我们需要导入一些库并使用 IPython 的 HTML 功能,如下面的截图所示:

实际上,我们是在使用我们的网络浏览器的播放功能。所以,这并不是真正的 Python 在做这件事,而是我们的网络浏览器,它使得 Jupyter Notebook 和我们的浏览器之间能够实现交互性。
这里,我们定义了 playvideo() 函数,它接受视频文件名作为输入,并返回一个包含我们视频的 HTML 对象。
- 在 Jupyter 中执行以下命令来播放 Megamind 视频。这只是电影 Megamind 的一段剪辑,如果我们下载所有源代码,它就会随 OpenCV 一起提供:
playvideo(' ./Megamind.mp4')
- 你会看到一个黑色盒子,如果你向下滚动,你会找到一个播放按钮。点击这个按钮,电影就会播放,如下面的截图所示:

这可以用来播放我们自己的视频。你只需要将命令指向你想播放的视频。
一旦所有这些运行起来,你应该能够良好地运行我们在接下来的章节中将要查看的项目。
摘要
在本章中,我们学习了 Anaconda 发行版和安装 Python 的不同方法。我们学习了如何使用 Anaconda 发行版设置 Python。
接下来,我们看了如何在 Anaconda 中安装各种库,以便我们更容易运行各种程序。最后,我们学习了 Jupyter Notebook 的基础知识及其工作原理。
在下一章(第二章),我们将探讨如何使用 TensorFlow 进行图像字幕生成。
第二章:使用 TensorFlow 进行图像标题生成
首先,本章将简要概述创建详细英语图像描述的过程。使用基于 TensorFlow 的图像标题生成模型,我们将能够用详细且完美描述图像的标题替换单个单词或复合词/短语。我们首先将使用预训练的图像标题生成模型,然后从头开始重新训练模型以在一系列图像上运行。
在本章中,我们将涵盖以下内容:
-
图像标题生成简介
-
Google Brain im2txt 标题生成模型
-
在 Jupyter 中运行我们的标题生成代码
-
重新训练模型
技术要求
除了 Python 知识、图像处理和计算机视觉的基础知识外,我们还需要以下 Python 库:
-
NumPy
-
Matplotlib
本章中使用的代码已添加到以下 GitHub 仓库中:
github.com/PacktPublishing/Computer-Vision-Projects-with-OpenCV-and-Python-3
图像标题生成简介
图像标题生成是一个基于图像生成文本描述的过程。为了更好地理解图像标题生成,我们首先需要将它与图像分类区分开来。
图像分类与图像标题生成的区别
图像分类是一个相对简单的过程,它只能告诉我们图像中有什么。例如,如果有一个骑自行车的男孩,图像分类不会给我们一个描述;它只会提供结果作为男孩或自行车。图像分类可以告诉我们图像中是否有女人或狗,或者是否有动作,如滑雪。这不是一个理想的结果,因为图像中并没有描述具体发生了什么。
以下是使用图像分类得到的结果:

相比之下,图像标题生成将提供一个带有描述的结果。对于前面的例子,图像标题生成的结果将是一个男孩骑在自行车上或一个男人在滑雪。这可能有助于为书籍生成内容,或者可能有助于帮助听力或视觉障碍者。
以下是使用图像标题生成得到的结果:

然而,这要困难得多,因为传统的神经网络虽然强大,但它们与序列数据不太兼容。序列数据是指按顺序到来的数据,而这个顺序实际上很重要。在音频或视频中,我们有按顺序到来的单词;打乱单词可能会改变句子的含义,或者只是使其成为完全的胡言乱语。
带有长短期记忆的循环神经网络
尽管卷积神经网络(CNNs)非常强大,但它们并不擅长处理序列数据;然而,它们非常适合非序列任务,如图像分类。
CNN 是如何工作的,以下图所示:

循环神经网络(RNNs),实际上确实是当前最先进的技术,可以处理序列任务。一个 RNN 由一系列接收数据的 CNN 组成。
RNNs 是如何工作的,以下图所示:

按序列进入的数据(x[i])通过神经网络,我们得到输出(y[i])。然后输出被送入另一个迭代,形成一个循环。这有助于我们记住之前来的数据,对于音频和语音识别、语言翻译、视频识别和文本生成等序列数据任务非常有帮助。
另一个存在已久且非常有用的概念是与 RNNs 结合的长短期记忆(LSTM)。这是一种处理长期记忆并避免仅仅将数据从一次迭代传递到下一次迭代的方法。它以稳健的方式处理迭代中的数据,并使我们能够有效地训练 RNNs。
Google Brain im2txt 字幕模型
Google Brain im2txt 被 Google 用于论文《2015 MSCOCO 图像字幕挑战》,并将成为我们将在项目中实现的图像字幕代码的基础。
Google 的 GitHub TensorFlow 页面可以在github.com/tensorflow/models/tree/master/research/im2txt找到.
在研究目录中,我们将找到 Google 在论文《2015 MSCOCO 图像字幕挑战》中使用的im2txt文件,该文件可在arxiv.org/abs/1609.06647免费获取。它详细介绍了 RNNs、LSTM 和基本算法。
我们可以检查 CNN 是如何用于图像分类的,也可以学习如何使用 LSTM RNNs 来实际生成序列字幕输出。
我们可以从 GitHub 链接下载代码;然而,它尚未设置得易于运行,因为它不包含预训练模型,所以我们可能会遇到一些挑战。我们已经为您提供了预训练模型,以避免从头开始训练图像分类器,因为这是一个耗时的过程。我们对代码进行了一些修改,使得代码在 Jupyter Notebook 上运行或集成到您自己的项目中变得容易。使用 CPU,预训练模型学习非常快。没有预训练模型的相同代码实际上可能需要几周时间才能学习,即使在好的 GPU 上也是如此。
在 Jupyter 上运行字幕代码
现在我们将在 Jupyter Notebook 上运行我们自己的代码版本。我们可以启动自己的 Jupyter Notebook,并从 GitHub 仓库加载Section_1-Tensorflow_Image_Captioning.ipynb文件(github.com/PacktPublishing/Computer-Vision-Projects-with-OpenCV-and-Python-3/blob/master/Chapter01/Section_1-Tensorflow_Image_Captioning.ipynb)。
一旦我们在 Jupyter Notebook 中加载了文件,它看起来会像这样:

在第一部分,我们将加载一些基本库,包括math、os和tensorflow。我们还将使用我们方便的实用函数%pylab inline,以便在 Notebook 中轻松读取和显示图像。
选择第一个代码块:
# load essential libraries
import math
import os
import tensorflow as tf
%pylab inline
当我们按下Ctrl + Enter来执行单元格中的代码时,我们将得到以下输出:

我们现在需要加载 TensorFlow/Google Brain 的基础代码,我们可以从github.com/PacktPublishing/Computer-Vision-Projects-with-OpenCV-and-Python-3获取。
有多个实用函数,但在我们的示例中,我们只会使用和执行其中的一些:
# load Tensorflow/Google Brain base code
# https://github.com/tensorflow/models/tree/master/research/im2txt
from im2txt import configuration
from im2txt import inference_wrapper
from im2txt.inference_utils import caption_generator
from im2txt.inference_utils import vocabulary
我们需要告诉我们的函数在哪里可以找到训练模型和词汇表:
# tell our function where to find the trained model and vocabulary
checkpoint_path = './model'
vocab_file = './model/word_counts.txt'
训练模型和词汇表的代码已添加到 GitHub 仓库中,您可以通过此链接访问:github.com/PacktPublishing/Computer-Vision-Projects-with-OpenCV-and-Python-3
github.com/PacktPublishing/Computer-Vision-Projects-with-OpenCV-and-Python-3
文件夹包含checkpoint、word_counts.txt和预训练模型。我们需要确保我们使用这些文件,并避免使用可能不与 TensorFlow 最新版本兼容的其他过时文件。word_counts.txt文件包含一个词汇表,其中包含我们从训练模型中得到的计数,这是我们的图像标题生成器所需要的。
一旦这些步骤完成,我们就可以查看我们的main函数,它将为我们生成标题。该函数可以接受一个字符串形式的输入文件(以逗号分隔)或仅一个我们想要处理的文件。
将冗余度设置为tf.logging.FATAL,这是可用的不同日志级别之一,因为它会告诉我们是否真的出了问题:

在主代码的初始部分,我们执行以下步骤:
-
将冗余度设置为
tf.logging.FATAL。 -
加载我们的预训练模型。
-
从 Google 提供的实用文件中加载推理包装器。
-
从之前单元格中建立的
checkpoint路径加载我们的预训练模型。 -
运行
finalize函数:
# this is the function we'll call to produce our captions
# given input file name(s) -- separate file names by a,
# if more than one
def gen_caption(input_files):
# only print serious log messages
tf.logging.set_verbosity(tf.logging.FATAL)
# load our pretrained model
g = tf.Graph()
with g.as_default():
model = inference_wrapper.InferenceWrapper()
restore_fn = model.build_graph_from_config(configuration.ModelConfig(),
checkpoint_path)
g.finalize()
- 再次从之前运行的单元格中加载词汇表文件:
# Create the vocabulary.
vocab = vocabulary.Vocabulary(vocab_file)
- 预处理文件名:
filenames = []
for file_pattern in input_files.split(","):
- 执行
Glob操作:
filenames.extend(tf.gfile.Glob(file_pattern))
- 创建一个文件名列表,这样你可以知道图像标题生成器正在哪个文件上运行:
tf.logging.info("Running caption generation on %d files matching %s",
len(filenames), input_files)
- 创建一个会话。由于我们正在使用预训练的模型,我们需要使用
restore函数:
with tf.Session(graph=g) as sess:
# Load the model from checkpoint.
restore_fn(sess)
这些步骤的代码包含在此处:
# this is the function we'll call to produce our captions
# given input file name(s) -- separate file names by a,
# if more than one
def gen_caption(input_files):
# only print serious log messages
tf.logging.set_verbosity(tf.logging.FATAL)
# load our pretrained model
g = tf.Graph()
with g.as_default():
model = inference_wrapper.InferenceWrapper()
restore_fn = model.build_graph_from_config(configuration.ModelConfig(),
checkpoint_path)
g.finalize()
# Create the vocabulary.
vocab = vocabulary.Vocabulary(vocab_file)
filenames = []
for file_pattern in input_files.split(","):
filenames.extend(tf.gfile.Glob(file_pattern))
tf.logging.info("Running caption generation on %d files matching %s",
len(filenames), input_files)
with tf.Session(graph=g) as sess:
# Load the model from checkpoint.
restore_fn(sess)
我们现在转向主代码的第二部分。一旦会话已恢复,我们执行以下步骤:
- 从我们的模型和存储在名为
generator的对象中的词汇中加载caption_generator:
generator = caption_generator.CaptionGenerator(model, vocab)
- 制作标题列表:
captionlist = []
- 遍历文件并将它们加载到名为
beam_search的生成器中,以分析图像:
for filename in filenames:
with tf.gfile.GFile(filename, "rb") as f:
image = f.read()
captions = generator.beam_search(sess, image)
- 打印标题:
print("Captions for image %s:" % os.path.basename(filename))
- 迭代以创建多个标题,迭代已为模型设置:
for i, caption in enumerate(captions):
# Ignore begin and end words.
sentence = [vocab.id_to_word(w) for w in caption.sentence[1:-1]]
sentence = " ".join(sentence)
print(" %d) %s (p=%f)" % (i, sentence, math.exp(caption.logprob)))
captionlist.append(sentence)
- 返回
captionlist:
return captionlist
运行代码以生成函数。
请看以下代码块中的完整代码:
# Prepare the caption generator. Here we are implicitly using the default
# beam search parameters. See caption_generator.py for a description of the
# available beam search parameters.
generator = caption_generator.CaptionGenerator(model, vocab)
captionlist = []
for filename in filenames:
with tf.gfile.GFile(filename, "rb") as f:
image = f.read()
captions = generator.beam_search(sess, image)
print("Captions for image %s:" % os.path.basename(filename))
for i, caption in enumerate(captions):
# Ignore begin and end words.
sentence = [vocab.id_to_word(w) for w in caption.sentence[1:-1]]
sentence = " ".join(sentence)
print(" %d) %s (p=%f)" % (i, sentence, math.exp(caption.logprob)))
captionlist.append(sentence)
return captionlist
在下一个代码块中,我们将对来自test文件夹的样本股票照片执行代码。代码将创建一个图形,显示它,然后运行标题生成器。然后我们可以使用print语句显示输出。
以下是我们用来选择用于计算的图像的代码:
testfile = 'test_images/dog.jpeg'
figure()
imshow(imread(testfile))
capts = gen_caption(testfile)
当我们运行我们的第一个测试图像,dog.jpeg,我们得到以下输出:

结果,“一位女士和一只狗站在草地上”,是对图像的一个很好的描述。由于所有三个结果都很相似,我们可以说我们的模型工作得相当好。
分析结果标题
让我们拿几个例子来检查我们的模型。当我们执行football.jpeg时,我们得到以下输出:

在这里,我们清楚地看到图像中正在进行美式足球比赛,而“一对男子在踢足球”是一个非常好的结果。然而,第一个结果,“一对男子在玩飞盘”,并不是我们想要的结果,也不是“一对男子在踢足球”。因此,在这种情况下,第二个标题通常会是最好的,但并不总是完美的,这取决于对数概率。
让我们再试一个例子,giraffes.jpeg:

很明显,我们有一张长颈鹿的图片,第一个标题,“一排长颈鹿并排站立”,看起来似乎是正确的,除了语法问题。其他两个结果是“一排长颈鹿站在田野中”和“一排长颈鹿在田野中并排站立”。
让我们再看一个例子,headphones.jpeg:

在这里,我们选择了headphones.jpeg,但结果中没有包含耳机。结果是“一位女士手里拿着一部手机”,这是一个很好的结果。第二个结果,“一位女士把手机举到耳边”,技术上是不正确的,但总体上是一些好的标题。
让我们再举一个例子,ballons.jpeg。当我们运行图像时,我们得到以下输出:

对于这张图像,我们得到的结果是“一个站在海滩上放风筝的妇女”、“一个在海滩上放风筝的妇女”,以及“一个在海滩上放风筝的年轻女孩”。所以,模型得到了“妇女”或“年轻女孩”,但它得到了“风筝”而不是气球,尽管“气球”在词汇表中。因此,我们可以推断出模型并不完美,但它很令人印象深刻,可以包含在你的应用程序中。
在 Jupyter 上运行多图像的标题代码
也可以通过使用逗号分隔不同图像的图像路径,将多个图像作为输入字符串添加。字符串图像的执行时间将大于我们之前看到的任何时间。
以下是一个多个输入文件的示例:
input_files = 'test_images/ballons.jpeg,test_images/bike.jpeg,test_images/dog.jpeg,test_images/fireworks.jpeg,test_images/football.jpeg,test_images/giraffes.jpeg,test_images/headphones.jpeg,test_images/laughing.jpeg,test_images/objects.jpeg,test_images/snowboard.jpeg,test_images/surfing.jpeg'
capts = gen_caption(input_files)
我们将不会显示图像,所以输出将只包括结果。我们可以看到,一些结果比其他结果要好:

这就完成了预训练图像标题模型的运行。我们现在将介绍从头开始训练模型并在标题图像上运行它。
重新训练标题模型
因此,既然我们已经看到了图像标题代码的实际应用,我们接下来将在我们自己的数据上重新训练图像标题器。然而,我们需要知道,如果想要在合理的时间内处理,这将非常耗时,并且需要超过 100 GB 的硬盘空间。即使有好的 GPU,完成计算可能也需要几天或一周的时间。既然我们有意愿实施并且有资源,让我们开始重新训练模型。
在笔记本中,第一步是下载预训练的 Inception 模型。webbrowser 模块将使打开 URL 并下载文件变得容易:
# First download pretrained Inception (v3) model
import webbrowser
webbrowser.open("http://download.tensorflow.org/models/inception_v3_2016_08_28.tar.gz")
# Completely unzip tar.gz file to get inception_v3.ckpt,
# --recommend storing in im2txt/data directory
以下将是输出:

当我们选择代码块并执行它时,我们可能无法在网页上查看内容,但我们可以点击对话框中的“保存”来下载文件。解压文件以获取 Inception v3 检查点文件。我们可以使用任何可用的解压工具,但最好是使用 7-zip 来获取 Inception v3 检查点文件,并将其存储在项目目录的 im2txt/data 中。
cd 命令用于导航到 im2txt/data 目录,那里存放着所有我们的文件。run_build_mscoco_data.py Python 脚本将抓取并处理所有图像数据和预制的标题数据。这个过程可能需要超过 100 GB 的空间,并且需要超过一个小时来完成执行。
一旦计算完成,我们将在项目目录中看到三个 ZIP 文件。我们可以解压这些文件以获取以下目录:

训练和验证的 JSON 文件位于annotations文件夹中。其他目录包含图像训练和验证数据。在train2014目录下,我们将找到与训练数据对应的 JPEG 图像。同样,与验证数据对应的资源将存在于val2014文件夹中。我们也可以替换自己的图像,并编辑annotations文件夹中相应的 JSON 文件。我们需要很多示例,因为少量的示例不会提供有效的结果。train2014目录中有超过 80,000 张图像,处理它们将需要大量的资源。
执行run_build_mscoco_data.py命令后,我们需要加载所需的模块:
# Now gather and prepare the MSCOCO data
# Comment out cd magic command if already in data directory
%cd im2txt/data
# This command will take an hour or more to run typically.
# Note, you will need a lot of HD space (>100 GB)!
%run build_mscoco_data.py
# At this point you have files in im2txt/data/mscoco/raw-data that you can train
# on, or you can substitute your own data
%cd ..
# load needed modules
import tensorflow as tf
from im2txt import configuration
from im2txt import show_and_tell_model
我们需要在im2txt文件夹中加载configuration和show_and_tell_model,以及 TensorFlow。我们可以运行cd ..命令以进入正确的目录。
现在,我们将定义以下变量:
-
input_file_pattern:定义指向预训练 Inception 检查点的文件,这些文件将来自我们的模型 -
train_dir:包含下载并解压后存储训练数据的路径 -
train_inception:设置为false,因为我们不会在初始运行时训练 Inception 模型 -
number_of_steps:我们的函数为一百万步 -
log_every_n_steps:将我们的函数设置为1
下面是代码:
# Initial training
input_file_pattern = 'im2txt/data/mscoco/train-?????-of-00256'
# change these if you put your stuff somewhere else
inception_checkpoint_file = 'im2txt/data/inception_v3.ckpt'
train_dir = 'im2txt/model'
# Don't train inception for initial run
train_inception = False
number_of_steps = 1000000
log_every_n_steps = 1
现在,让我们定义我们的train函数。train函数中执行的步骤如下:
-
创建
train目录 -
创建图文件
-
加载必要的文件
-
添加 TensorFlow 开始训练模型所需的变量,以获得每批次的延迟步数的学习率
-
设置层
-
设置用于保存和恢复模型检查点的保存器
-
调用 TensorFlow 进行训练
以下是我们train函数的内容:
- 定义(但尚未运行)我们的字幕训练函数:
def train():
model_config = configuration.ModelConfig()
model_config.input_file_pattern = input_file_pattern
model_config.inception_checkpoint_file = inception_checkpoint_file
training_config = configuration.TrainingConfig()
- 创建训练目录:
train_dir = train_dir
if not tf.gfile.IsDirectory(train_dir):
tf.logging.info("Creating training directory: %s", train_dir)
tf.gfile.MakeDirs(train_dir)
- 构建 TensorFlow 图:
g = tf.Graph()
with g.as_default():
- 构建模型:
model = show_and_tell_model.ShowAndTellModel(
model_config, mode="train", train_inception=train_inception)
model.build()
- 设置学习率:
learning_rate_decay_fn = None
if train_inception:
learning_rate = tf.constant(training_config.train_inception_learning_rate)
else:
learning_rate = tf.constant(training_config.initial_learning_rate)
if training_config.learning_rate_decay_factor > 0:
num_batches_per_epoch = (training_config.num_examples_per_epoch /
model_config.batch_size)
decay_steps = int(num_batches_per_epoch *
training_config.num_epochs_per_decay)
def _learning_rate_decay_fn(learning_rate, global_step):
return tf.train.exponential_decay(
learning_rate,
global_step,
decay_steps=decay_steps,
decay_rate=training_config.learning_rate_decay_factor,
staircase=True)
learning_rate_decay_fn = _learning_rate_decay_fn
- 设置训练操作:
train_op = tf.contrib.layers.optimize_loss(
loss=model.total_loss,
global_step=model.global_step,
learning_rate=learning_rate,
optimizer=training_config.optimizer,
clip_gradients=training_config.clip_gradients,
learning_rate_decay_fn=learning_rate_decay_fn)
- 设置用于保存和恢复模型检查点的
Saver:
saver = tf.train.Saver(max_to_keep=training_config.max_checkpoints_to_keep)
# Run training.
tf.contrib.slim.learning.train(
train_op,
train_dir,
log_every_n_steps=log_every_n_steps,
graph=g,
global_step=model.global_step,
number_of_steps=number_of_steps,
init_fn=model.init_fn,
saver=saver)
按Ctrl + Enter执行此代码单元格,因为我们现在可以执行它。之后,我们需要调用train函数:
train()
这将需要很长时间来处理,即使在好的 GPU 上也是如此,但如果我们有资源并且仍然想改进模型,请运行以下代码以微调我们的inception模型:
# Fine tuning
input_file_pattern = 'im2txt/data/mscoco/train-?????-of-00256'
# change these if you put your stuff somewhere else
inception_checkpoint_file = 'im2txt/data/inception_v3.ckpt'
train_dir = 'im2txt/model'
# This will refine our results
train_inception = True
number_of_steps = 3000000
log_every_n_steps = 1
# Now run the training (warning: takes even longer than initial training!!!)
train()
该模型将运行三百万步。它实际上是从初始训练完成的地方继续,生成新的检查点和改进的模型,然后再运行train函数。这将需要更多的时间来处理并提供良好的结果。我们可以在 Jupyter Notebook 中通过正确指定我们的checkpoint路径和词汇文件路径来完成这项工作:
# tell our function where to find the trained model and vocabulary
checkpoint_path = './model'
vocab_file = './model/word_counts.txt'
之后,我们可以重新运行 Jupyter Notebook 文件中的代码块 4,该文件位于github.com/PacktPublishing/Computer-Vision-Projects-with-OpenCV-and-Python-3/blob/master/Chapter01/Section_1-Tensorflow_Image_Captioning.ipynb,以找到gen_caption。
最后一步是运行以下代码,就像我们在“在 Jupyter 上运行标题代码”部分所做的那样:
testfile = 'test_images/ballons.jpeg'
figure()
imshow(imread(testfile))
capts = gen_caption(testfile)
一旦计算完成,我们应该得到一些不错的结果。这标志着使用 TensorFlow 的图像标题的结束。
摘要
在本章中,我们介绍了不同的图像标题方法。我们学习了谷歌大脑的 im2txt 标题模型。在项目工作中,我们能够在 Jupyter Notebook 上运行我们的预训练模型,并根据结果分析模型。在章节的最后部分,我们从零开始重新训练了我们的图像标题模型。
在下一章中,我们将介绍使用 OpenCV 读取车牌。
第三章:使用 OpenCV 读取车牌
本章概述了如何从任何包含车牌的样本照片中提取和显示车牌字符。OpenCV 及其车牌实用函数帮助我们找到车牌上的字符,并让我们对计算机视觉和图像处理的工作有良好的了解。
在本章中,我们将学习以下内容:
-
读取车牌所需的步骤
-
车牌实用函数
-
寻找车牌字符
-
寻找和读取车牌
识别车牌
在这个项目中,我们将检测和读取汽车照片中的车牌。我们将执行多个步骤,从定位车牌到显示定位车牌中的字符。
让我们参考 Jupyter Notebook 中分析我们的样本图像所需的代码:
%pylab notebook
figure()
imshow(imread('tests/p1.jpg'))
运行代码后,我们得到以下照片:

我们有一张汽车的照片,车牌清晰可见且可读。挑战在于定位车牌,将其从照片的其余部分中分离出来,并从中提取字符。
我们现在可以更仔细地查看车牌,使用可用的实用函数:

有许多算法可以帮助我们完成这两项任务。例如,YOLO:实时目标检测等对象检测器可以使用相关的机器学习方法执行此类任务,并做得非常好。
然而,我们将采用一种简单的方法,使用传统的图像处理和计算机视觉技术,而不是复杂的机器学习技术,如深度学习和 TensorFlow。
我们将使用的算法将帮助我们学习计算机视觉和图像处理技术,从而更好地理解项目。让我们从我们的代码开始,检查我们将使用的车牌实用函数。
车牌实用函数
让我们跳转到 Jupyter Notebook 中的代码,以便了解车牌实用函数。我们首先将导入我们的实用工具。
我们将导入以下库:
-
OpenCV(版本 3.4)
-
NumPy
-
Pickle,它允许我们保存 Python 数据和案例函数
按照以下方式导入库:
import cv2
import numpy as np
import pickle
def gray_thresh_img(input_image):
h, w, _ = input_image.shape
grayimg = cv2.cvtColor(input_image, cv2.COLOR_BGR2HSV)[:,:,2]
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
tophat = cv2.morphologyEx(grayimg, cv2.MORPH_TOPHAT, kernel)
blackhat = cv2.morphologyEx(grayimg, cv2.MORPH_BLACKHAT, kernel)
graytop = cv2.add(grayimg, tophat)
contrastgray = cv2.subtract(graytop, blackhat)
blurred = cv2.GaussianBlur(contrastgray, (5,5), 0)
thesholded = cv2.adaptiveThreshold(blurred, 255.0,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY_INV, 19, 9)
我们将使用这些库来加载用于读取字符的 k 最近邻分类器,这隐式地依赖于 scikit-learn。
我们现在将讨论我们代码中将使用的实用工具。
灰度阈值图像函数和形态学函数
gray_thresh_img 函数接受一个输入图像并将其转换为灰度。我们需要灰度图像,因为彩色图像可能会引起歧义,因为车牌的颜色会根据地区而有所不同。gray_thres_img 函数为我们提供了一个二值化图像。
我们可以使用形态学操作进行预处理,因为这有助于我们减少噪声和间隙。这将去除图像中的噪声并移除多余的特征。
内核
内核是一个三乘三的正方形,我们将在这个正方形上使用tophat、blackhat和graytop操作来创建灰度图像。这也有助于我们去除图像噪声——噪声通常存在于自然图像中,对于计算机视觉来说并不理想。图像也可以使用高斯模糊进行去噪。
我们将使用自适应阈值,它检查图像中的局部统计和平均值,以确定它相对于其邻域是亮还是暗。与硬阈值相比,这是更好的选择,因为它将以更好的方式二值化我们的图像。
我们使用return函数来获取灰度图像和二值化图像,如下所示:
return grayimg, thesholded
匹配字符函数
让我们看看下一个函数来获取匹配的字符:
def getmatchingchars(char_cands):
char_list = []
for char_cand in char_cands:
ch_matches = [] \n",
for matching_candidate in char_cands:
if matching_candidate == char_cand:
continue
chardistance = np.sqrt((abs(char_cand.x_cent - matching_candidate.x_cent) ** 2) +
(abs(char_cand.y_cent - matching_candidate.y_cent)**2))
x = float(abs(char_cand.x_cent - matching_candidate.x_cent))
y = float(abs(char_cand.y_cent - matching_candidate.y_cent))
angle = np.rad2deg(np.arctan(y/x) if x != 0.0 else np.pi/2)
deltaarea = float(abs(matching_candidate.rect_area - char_cand.rect_area))\
/ float(char_cand.rect_area)
deltawidth = float(abs(matching_candidate.rect_w-char_cand.rect_w))\
/ float(char_cand.rect_w)
deltaheight = float(abs(matching_candidate.rect_h-char_cand.rect_h))
/ float(char_cand.rect_h)
if (chardistance < (char_cand.hypotenuse * 5.0) and
angle < 12.0 and deltaarea < 0.5 and deltawidth < 0.8
and deltaheight < 0.2):
ch_matches.append(matching_candidate)
ch_matches.append(char_cand)
if len(ch_matches) < 3:
continue
char_list.append(ch_matches)
getmatchingchars函数帮助我们根据以下标准找到我们的字符候选:
-
大小
-
相对距离
-
角度
-
面积
如果潜在的字符与其邻居的距离合理,角度与 JSON 字符相比不是太大,面积也不是太大,那么我们可以说可能的字符是一个字符候选。
以下代码将返回一个包含车牌字符的列表,然后创建一个容器类,该类将包含对象,如字符子图像的宽度、高度、中心、对角距离或斜边,以及宽高比:
for charlist in getmatchingchars(list(set(char_cands)-set(ch_matches))):
char_list.append(charlist)
break
return char_list
# information container for possible characters in images
class charclass:
def __init__(self, _contour):
self.contour = _contour
self.boundingRect = cv2.boundingRect(self.contour)
self.rect_x, self.rect_y, self.rect_w, self.rect_h = self.boundingRect
self.rect_area = self.rect_w * self.rect_h
self.x_cent = (self.rect_x + self.rect_x + self.rect_w) / 2
self.y_cent = (self.rect_y + self.rect_y + self.rect_h) / 2
self.hypotenuse = np.sqrt((self.rect_w ** 2) + (self.rect_h ** 2))
self.aspect_ratio = float(self.rect_w) / float(self.rect_h)
k-最近邻数字分类器
预训练的 scikit-learn k-最近邻(k-nn)数字分类器也需要加载,如下所示:
# load pre-trained scikit-learn knn digit classifier
with open('knn.p', 'rb') as f:
knn = pickle.load(f) "
k-nn 分类器将一个小图像与它已知的一系列图像进行比较,以找到最接近的匹配。
我们在这个例子中并没有使用复杂的算法,因为车牌上的字符是相似的。这就是为什么我们可以使用 k-nn 方法,它将进行像素级的比较以找到最接近的匹配。车牌上的字符不是手写的数字,字体可能不同,这需要更多的计算。
在分类器中,p代表 Pickle,这是 Python 存储数据的方式。
寻找车牌字符
接下来,我们执行初始搜索以找到车牌字符。首先,我们找到大致的字符,然后根据特定标准找到候选者。
让我们从 Notebook 中的以下行开始:
%pylab notebook
现在我们可以执行我们的函数单元,用于导入、工具和加载我们的库:
import cv2
import numpy as np
import pickle
def getmatchingchars(char_cands):
char_list = []
for char_cand in char_cands:
ch_matches = [] \n",
for matching_candidate in char_cands:
if matching_candidate == char_cand:
continue
chardistance = np.sqrt((abs(char_cand.x_cent - matching_candidate.x_cent) ** 2) +
(abs(char_cand.y_cent - matching_candidate.y_cent)**2))
x = float(abs(char_cand.x_cent - matching_candidate.x_cent))
y = float(abs(char_cand.y_cent - matching_candidate.y_cent))
angle = np.rad2deg(np.arctan(y/x) if x != 0.0 else np.pi/2)
deltaarea = float(abs(matching_candidate.rect_area - char_cand.rect_area))\
/ float(char_cand.rect_area)
deltawidth = float(abs(matching_candidate.rect_w-char_cand.rect_w))\
/ float(char_cand.rect_w)
deltaheight = float(abs(matching_candidate.rect_h-char_cand.rect_h))
/ float(char_cand.rect_h)
if (chardistance < (char_cand.hypotenuse * 5.0) and
angle < 12.0 and deltaarea < 0.5 and deltawidth < 0.8
and deltaheight < 0.2):
ch_matches.append(matching_candidate)
ch_matches.append(char_cand)
if len(ch_matches) < 3:
continue
char_list.append(ch_matches)
for charlist in getmatchingchars(list(set(char_cands)-set(ch_matches))):
char_list.append(charlist)
break
return char_list
# information container for possible characters in images
class charclass:
def __init__(self, _contour):
self.contour = _contour
self.boundingRect = cv2.boundingRect(self.contour)
self.rect_x, self.rect_y, self.rect_w, self.rect_h = self.boundingRect
self.rect_area = self.rect_w * self.rect_h
self.x_cent = (self.rect_x + self.rect_x + self.rect_w) / 2
self.y_cent = (self.rect_y + self.rect_y + self.rect_h) / 2
self.hypotenuse = np.sqrt((self.rect_w ** 2) + (self.rect_h ** 2))
self.aspect_ratio = float(self.rect_w) / float(self.rect_h)
现在我们可以加载我们的输入图像,它将被用于分析。在这里我们使用plt函数而不是 OpenCV,因为 OpenCV 默认以蓝绿红(BGR)格式而不是红绿蓝(RGB)格式加载图像。这对于你的自定义项目很重要,但对我们项目来说并不重要,因为我们将会将图像转换为灰度。
让我们加载我们的图像:
input_image = plt.imread('tests/p5.jpg') #use cv2.imread or
#import matplotlib.pyplot as plt
#if running outside notebook
figure()
imshow(input_image)
这是输出照片:

让我们仔细看看这辆车的车牌:

我们将从这张图像中找到字符。然而,我们首先需要移除背景,这对我们来说并不重要。在这里,我们需要对图像进行初始预处理,使用 gray_thresh_img、blurred 和 morphology 函数,这将帮助我们去除背景。
这里是初始预处理的代码:
def gray_thresh_img(input_image):
h, w, _ = input_image.shape
grayimg = cv2.cvtColor(input_image, cv2.COLOR_BGR2HSV)[:,:,2]
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
tophat = cv2.morphologyEx(grayimg, cv2.MORPH_TOPHAT, kernel)
blackhat = cv2.morphologyEx(grayimg, cv2.MORPH_BLACKHAT, kernel)
graytop = cv2.add(grayimg, tophat)
contrastgray = cv2.subtract(graytop, blackhat)
blurred = cv2.GaussianBlur(contrastgray, (5,5), 0)
thesholded = cv2.adaptiveThreshold(blurred, 255.0,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY_INV, 19, 9)
让我们看看我们的主要代码:
h, w = input_image.shape[:2]
# We don't use color information
# + we need to binarize (theshold) image to find characters
grayimg, thesholded = gray_thresh_img(input_image)
contours = cv2.findContours(thesholded, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)[1]
# initialize variables for possible characters/plates in image
char_cands = []
plate_candidates = []
我们将给出图像形状,这将返回照片的高度、宽度和 RGB 深度。我们现在不需要 RGB 深度,所以我们只提取 2 个元素;高度和宽度。由于我们将处理灰度图像而不是彩色图像,我们将调用我们方便的 gray_thresh_img 函数,该函数将返回灰度和二值化的阈值图像。
为了找到轮廓,我们需要图像中的子图像,这些子图像对应于字符,然后对应于轮廓。我们将使用 OpenCV 的内置算法 findContours 来找到可能作为字符和作为我们的 k-nn 使用的复杂形状的轮廓细节。然后我们将初始化我们的 char_cands 和 plate_candidates 变量。
让我们尝试第一次寻找字符:
for index in range(0, len(contours)):
char_cand = charclass(contours[index])
if (char_cand.rect_area > 80 and char_cand.rect_w > 2
and char_cand.rect_h > 8 and 0.25 < char_cand.aspect_ratio
and char_cand.aspect_ratio < 1.0):
char_cands.append(char_cand)
我们将使用字符来寻找车牌,这是一种与其他机器学习算法不同的方法。这种方法将帮助我们更好地理解寻找字符的过程。
我们将遍历所有轮廓,并使用我们已定义的 charclass 类(charclass 类)。这个类会自动提取中心、对角线长度和宽高比,以确定图像是否过大或过小,或者宽高比是否过于倾斜。从这个推断中,我们可以得出结论,该字符不是车牌上的字母或数字。这有助于我们仅考虑符合几何标准的轮廓。
寻找匹配的字符和字符组
一旦第一次遍历完成,我们将细化我们的匹配,以找到可能属于车牌的一组字符。参考以下代码:
for ch_matches in getmatchingchars(char_cands):
class blank: pass
plate_candidate = blank()
ch_matches.sort(key = lambda ch: ch.x_cent)
plate_w = int((ch_matches[len(ch_matches) - 1].rect_x + \
ch_matches[len(ch_matches) - 1].rect_w - ch_matches[0].rect_x) * 1.3)
sum_char_h = 0
for ch in ch_matches:
sum_char_h += ch.rect_h
avg_char_h = sum_char_h / len(ch_matches)
plate_h = int(avg_char_h * 1.5)
y = ch_matches[len(ch_matches) - 1].y_cent - ch_matches[0].y_cen
r = np.sqrt((abs(ch_matches[0].x_cent
- ch_matches[len(ch_matches) - 1].x_cent) ** 2)
+ (abs(ch_matches[0].y_cent
- ch_matches[len(ch_matches) - 1].y_cent) ** 2))
rotate_angle = np.rad2deg(np.arcsin(y / r))
我们将通过调用之前使用的 getmatchingchars 函数遍历所有潜在的字符,该函数根据标准提供额外的过滤。它取决于与相邻字符的角、三角学、宽度和高度的比较,以及邻居的类型。这些标准帮助我们实现一致性。
一旦我们有了车牌候选者,我们可以创建一个 blank 对象。因此,我们有一个没有任何属性的 blank 对象,并创建了一个列表。我们首先按字符的中心排序,这将帮助我们按从左到右的顺序通过匹配进行排序。
sum_char_h 的求和将帮助我们找到字符的平均高度和宽度。
让我们看看以下代码:
platex = (ch_matches[0].x_cent + ch_matches[len(ch_matches) - 1].x_cent) / 2
platey = (ch_matches[0].y_cent + ch_matches[len(ch_matches) - 1].y_cent) / 2
plate_cent = platex, platey
车牌的理想位置是垂直于摄像头的。如果车牌的角度大于特定的可接受角度,或者颠倒,那么我们可能无法读取车牌。
我们从代码中找到x和y,并校正车牌的角度,如果它在合理的角度范围内。
然后,我们根据这里找到的角度确定车牌位置,并使用rotationMatrix将其存储起来,以便稍后进行计算。我们可以一步完成,因为我们已经找到了这个角度。我们希望围绕车牌的中心旋转,如下所示:
plate_candidate.plateloc = (tuple(plate_cent), (plate_w, plate_h), rotate_angle)
rotationMatrix = cv2.getRotationMatrix2D(tuple(plate_cent), rotate_angle, 1.0)
我们在这里创建旋转后的图像,cv2.wrapAffine函数将帮助我们进行拉伸、倾斜、旋转和平移,以及更高阶的变换,如缩放、拉伸和旋转:
rotated = cv2.warpAffine(input_image, rotationMatrix, tuple(np.flipud(input_image.shape[:2])))
plate_candidate.plate_im = cv2.getRectSubPix(rotated, (plate_w, plate_h), tuple(plate_cent))
if plate_candidate.plate_im is not None:
plate_candidates.append(plate_candidate)
一旦我们有了旋转并围绕车牌候选中心对齐的子图像,我们就将其保存到我们之前初始化的车牌候选列表中。现在我们有了字符和车牌候选的初始猜测,利用这些我们可以找到并读取车牌候选。
使用 OpenCV 查找和读取车牌
我们已经找到了字符,它们是车牌候选。现在我们需要确定哪些字符匹配,以便我们可以提取文本数据并将字符映射到车牌中。
首先,我们运行每个车牌候选通过我们的gray_thresh_img函数,该函数执行我们的去噪和二值化。在这种情况下,我们得到更干净的输出,因为我们使用的是子图像而不是完整图像。
这是我们将要使用的提取代码:
for plate_candidate in plate_candidates:
plate_candidate.grayimg, plate_candidate.thesholded = \
gray_thresh_img(plate_candidate.plate_im)
plate_candidate.thesholded = cv2.resize(plate_candidate.thesholded,
(0, 0), fx = 1.6, fy = 1.6)
thresholdValue, plate_candidate.thesholded = \
cv2.threshold(plate_candidate.thesholded,
0.0, 255.0,
cv2.THRESH_BINARY | cv2.THRESH_OTSU)
我们需要字符具有相同的大小,因为我们将会使用 k-nn 方法,它是区分大小写的。如果大小不同,我们将收到垃圾值。在将图像调整到大小后,我们需要进行阈值处理,我们将使用OTSU方法。
然后,我们需要在子图像中找到轮廓,并进行合理性检查,以确保我们找到的子图像中的轮廓满足某些标准,其中大小和宽高比是合理的,如下所示:
contours = cv2.findContours(plate_candidate.thesholded, cv2.RETR_LIST,
cv2.CHAIN_APPROX_SIMPLE)[1]
plate_chars = []
for contour in contours:
char_cand = charclass(contour)
if (char_cand.rect_area > 80 and char_cand.rect_w > 2
and char_cand.rect_h > 8 and 0.25 < char_cand.aspect_ratio
and char_cand.aspect_ratio < 1.0):
plate_chars.append(char_cand)
如果轮廓不符合标准,这意味着我们可能没有看到车牌,或者没有得到好的字符。
一旦完成合理性检查,我们就运行getmatchingchars函数,这将确保我们得到一组大小大致相同的良好字符:
plate_chars = getmatchingchars(plate_chars)
if (len(plate_chars) == 0):
plate_candidate.chars = \"
continue
for index in range(0, len(plate_chars)):
plate_chars[index].sort(key = lambda ch: ch.x_cent)
filt_matching_chars = list(plate_chars[index])
这是一个冗余检查,但对于获得干净和可靠的结果是必要的。我们按顺序从左到右迭代所有字符,以检查字符是否足够远。我们这样做是因为,理论上,重叠的轮廓可能是重叠的字符,这在现实中的车牌中是不会发生的。
我们需要确保字符之间距离足够远,因为我们不是在重复检测相同的东西;在这里我们执行多个for循环,并将字符相互比较,如下所示:
for thischar in plate_chars[index]:
for alt_char in plate_chars[index]:
if thischar != alt_char:
chardistance = np.sqrt((abs(thischar.x_cent-alt_char.x_cent)**2)
+ (abs(thischar.y_cent-alt_char.y_cent) ** 2))
if chardistance < (thischar.hypotenuse * 0.3):
if thischar.rect_area < alt_char.rect_area:
if thischar in filt_matching_chars:
filt_matching_chars.remove(thischar)
else:
if alt_char in filt_matching_chars:
filt_matching_chars.remove(alt_char)
我们需要确保所有内容都在感兴趣的区域中心,这样在执行缩放、旋转和平移等操作时,字符就不会丢失,因为我们正在寻找 k-nn。
在此代码中,我们遍历我们的字符列表中的每个字符和每个阈值区域,以确保我们将区域调整到20乘以30,这与我们的 k-nn 预测相匹配:
charlistlen = 0
char_index = 0
for index in range(0, len(plate_chars)):
if len(plate_chars[index]) > charlistlen:
charlistlen = len(plate_chars[index])
char_index = index
full_char_list = plate_chars[char_index]
full_char_list.sort(key = lambda ch: ch.x_cent)
plate_candidate.chars = \
for thischar in full_char_list:
roi = plate_candidate.thesholded[thischar.rect_y :
thischar.rect_y + thischar.rect_h,
thischar.rect_x :
thischar.rect_x + thischar.rect_w]
resized_roi = np.float32(cv2.resize(roi, (20, 30)).reshape((1, -1)))
plate_candidate.chars += str(chr(int(knn.predict(resized_roi)[0])))
现在,所有这些区域长度都是 600。NumPy 的reshape函数将二维输入的区域通过某些维度映射,以得到 1/600。
thischar函数最初实际上是一个空字符串,但随着我们找到 k-nn,它将不断被填充。
此外,当我们寻找最佳候选者时,我们需要确保我们的plate_candidates不是空的:
if len(plate_candidates) > 0:
plate_candidates.sort(key = lambda plate_candidate:
len(plate_candidate.chars), reverse = True)
best_plate = plate_candidates[0]
print("License plate read: " + best_plate.chars + "\n")
对于给定的图像,您可能会找到多个车牌候选者,但通常它们是同一件事。您可能只是找到了四个字符,而实际上有六个,或者类似的情况。具有最多字符的那个可能是正确的,但您也可以查看其他候选者。
我们将再次按字符串长度提取并排序,找到best_plate,并打印出结果。
结果分析
当我们使用最佳候选代码块运行我们的代码时,我们得到以下结果:
if len(plate_candidates) > 0:
plate_candidates.sort(key = lambda plate_candidate:
len(plate_candidate.chars), reverse = True)
best_plate = plate_candidates[0]
print("License plate read: " + best_plate.chars + "\n")
License plate read: LTLDBENZ
一旦我们得到输出,我们可以使用以下代码显示我们的结果:
figure()
imshow(best_plate.thesholded)
显示的图像如下:

虽然有一个多余的字符,但我们可以看到我们的显示图像与车牌字符非常接近。我们可以用我们的其他可能的车牌字符来检查,以获得最接近的结果。
让我们再试一个车牌,以检查我们的代码如何工作:
input_image = plt.imread('tests/p2.jpg') #use cv2.imread or
#import matplotlib.pyplot as plt
#if running outside notebook
figure()
imshow(input_image)
这里是输出:

显示的照片如下:

如果您只想获取车牌的子图像,可以使用以下代码:
imshow(best_plate.plate_im)

我们还可以找到结果的位置:
figure()
# best_plate.plate_im
imshow(best_plate.plate_im)
best_plate.plateloc
输出中您得到以下位置:

因此,这里我们有x和y坐标,宽度,高度和一些偏移信息。
我们可以尝试其他可用的函数,如下所示:

让我们看看另一个例子,其中车牌号码不够清晰:
input_image = plt.imread('tests/p3.jpg') #use cv2.imread or
#import matplotlib.pyplot as plt
#if running outside notebook
figure()
imshow(input_image)
这给我们以下输出:

让我们更仔细地看看车牌:

我们的display函数给出了相当不错的结果,如下所示:

让我们看看我们的最后一个例子:
input_image = plt.imread('tests/p1.jpg') #use cv2.imread or
#import matplotlib.pyplot as plt
#if running outside notebook
figure()
imshow(input_image)
这里是视图:

以下截图显示了输出:

结果照片显示如下:

摘要
在本章中,我们学习了如何使用 OpenCV 进行车牌识别,这让我们对计算机视觉和图像处理的工作原理有了很好的了解。
我们首先学习了不同的车牌效用函数,这帮助我们找到了车牌特征。然后,我们使用 OpenCV 找到了车牌字符的可能候选者。最后,我们分析了我们的结果,以检查我们算法的效率。
在下一章,第四章,使用 TensorFlow 进行人体姿态估计,我们将使用 DeeperCut 算法和 ArtTrack 模型进行人体姿态估计。
第四章:使用 TensorFlow 进行人体姿态估计
在本章中,我们将介绍如何使用 DeeperCut 算法通过 TensorFlow 进行人体姿态估计。我们将学习使用 DeeperCut 和 ArtTrack 模型进行单人和多人姿态检测。稍后,我们还将学习如何使用该模型与视频一起使用,并重新训练它以用于我们项目中的定制图像。
在本章中,我们将涵盖以下主题:
-
使用 DeeperCut 和 ArtTrack 进行姿态估计
-
单人姿态检测
-
多人姿态检测
-
视频和重新训练
使用 DeeperCut 和 ArtTrack 进行姿态估计
人体姿态估计是从图像或视频中估计身体(姿态)配置的过程。它包括地标(点),这些点类似于脚、脚踝、下巴、肩膀、肘部、手、头部等关节。我们将使用深度学习自动完成这项工作。如果您考虑面部,地标相对刚性,或者说相对恒定,例如眼睛相对于鼻子的相对位置,嘴巴相对于下巴,等等。
以下照片提供了一个示例:

虽然身体结构保持不变,但我们的身体不是刚性的。因此,我们需要检测身体的不同部位相对于其他部位的位置。例如,相对于膝盖检测脚部是非常具有挑战性的,与面部检测相比。此外,我们可以移动我们的手和脚,这可能导致各种各样的姿势。以下图片提供了一个示例:

这在我们从世界各地不同研究小组在计算机视觉方面的突破之前是非常困难的。已经开发了不同的代码来执行姿态估计,但我们将介绍一个名为DeeperCut的算法。
您可以参考 MPII 人体姿态模型(pose.mpi-inf.mpg.de)以获取详细信息。
DeeperCut 是由德国马克斯·普朗克学会的一个研究小组开发的,与斯坦福大学合作,他们发布了他们的算法并发表了论文。建议查看他们的论文《DeepCut:用于多人姿态估计的联合子集划分和标记》,该论文概述了 DeeperCut 之前的早期算法,其中他们讨论了如何检测身体部位以及他们如何运行优化算法以获得良好的结果。您还可以参考他们后续的论文《DeeperCuts》:一个更深、更强、更快的多人姿态估计模型,该论文由同一组作者发表,这将涵盖许多技术细节。我们肯定不会得到精确的结果,但您可以用合理的概率确定一些事情。
在 GitHub 页面github.com/eldar/pose-tensorflow,有他们代码的公开实现,包括 DeeperCut 和一个新版本 ArtTrack。这是在野外进行的人体姿态跟踪,你可以在下面的照片中看到输出结果:

我们将运行一个修改后的代码版本,它被设计在 Jupyter Notebook 环境中运行,并且适用于所有学习目的,因此它应该比直接从 GitHub 获取要简单一些。我们将学习如何运行代码并在我们的项目中使用它。所有预训练的模型都包含在这里:github.com/eldar/pose-tensorflow.
单人姿态检测
现在我们已经了解了人体姿态估计和新的 DeeperCut 算法的概述,我们可以运行单人姿态检测的代码,并在 Jupyter Notebook 中检查它。
我们将从单人检测开始。在开始之前,我们需要确保我们使用的是一个干净的内核。你可以重启你的内核,或者你可以使用快捷键来完成同样的操作。当你处于命令模式时,你可以按下0键两次,这与实际编辑单元格时的编辑模式相反。
让我们从以下示例中的单人检测代码开始:
!pip install pyyaml easydict munkres
感叹号表示执行一个 shell 命令。这将安装一些你可能没有的库,如果你在你的系统中安装了 Python 3,你可能需要将命令更改为pip 3。
在下一个单元格中,我们将调用%pylab notebook函数,这将允许我们在笔记本中使用一些有用的控件查看图像,以及加载一些数值库,例如numpy等。我们将进行一些通用导入,例如os、sys和cv2。为了进行注释,我们将使用imageio的imread函数并从randint获取一切。你不需要导入numpy,因为我们已经使用了%pylab notebook,但如果你想在笔记本外复制粘贴此代码,你需要它。然后,我们需要导入tensorflow,它已经包含了一些来自pose-tensorflow仓库的粘合工具。代码,仅供参考,如下所示:
%pylab notebook
import os
import sys
import cv2
from imageio import imread
from random import randint
import numpy as np
import tensorflow as tf
from config import load_config
from nnet.net factory import pose_net
然后我们执行前面的单元格。
我们现在将设置姿态预测,如下面的代码所示:
def setup_pose_prediction(cfg):
inputs = tf.placeholder(tf.float32, shape=[cfg.batch_size, None, None, 3])
outputs = pose_net(cfg).test(inputs)
restorer = tf.train.Saver()
sess = tf.Session()
sess.run(tf.global_variables_initializer())
sess.run(tf.local_variables_initializer())
# Restore variables from disk.
restorer.restore(sess, cfg.init_weights)
return sess, inputs, outputs
它将启动会话并加载我们的模型。我们将使用一个预训练模型,您可以从 GitHub 仓库快速访问它。tf.Session()将启动 TensorFlow 会话并将其保存到sess变量中,我们将返回它。请注意,当您运行此函数时,它将保持 TensorFlow 会话开启,所以如果您想继续做其他事情,比如加载新模型,那么您将不得不关闭会话或重新启动。在这里这很有用,因为我们将要查看多张图片,如果每次都加载会话,将会更慢。然后,我们将获取配置,它加载相应的模型和变量,并将返回运行模型所需的必要值。
然后,我们使用extract_cnn_outputs函数提取 CNN 输出。在输出中,我们将获得联合位置,以了解一切相对于其他事物的确切位置。我们希望得到一个有序的二维数组,其中我们知道脚踝、手或肩膀的位置的 X 和 Y 坐标。以下是一个示例:
def extract_cnn_output(outputs_np, cfg, pairwise_stats = None):
scmap = outputs_np['part_prob']
scmap = np.squeeze(scmap)
locref = None
pairwise_diff = None
if cfg.location_refinement:
locref = np.squeeze(outputs_np['locref'])
shape = locref.shape
locref = np.reshape(locref, (shape[0], shape[1], -1, 2))
locref *= cfg.locref_stdev
if cfg.pairwise_predict:
pairwise_diff = np.squeeze(outputs_np['pairwise_pred'])
shape = pairwise_diff.shape
pairwise_diff = np.reshape(pairwise_diff, (shape[0], shape[1], -1, 2))
num_joints = cfg.num_joints
for pair in pairwise_stats:
pair_id = (num_joints - 1) * pair[0] + pair[1] - int(pair[0] < pair[1])
pairwise_diff[:, :, pair_id, 0] *= pairwise_stats[pair]["std"][0]
pairwise_diff[:, :, pair_id, 0] += pairwise_stats[pair]["mean"][0]
pairwise_diff[:, :, pair_id, 1] *= pairwise_stats[pair]["std"][1]
pairwise_diff[:, :, pair_id, 1] += pairwise_stats[pair]["mean"][1]
return scmap, locref, pairwise_diff
这将把神经网络输出(有点难以理解)转换成我们可以实际使用的格式。然后,我们将输出传递给其他东西,或者在这种情况下可视化它。argmax_pose_predict与我们之前所做的是互补的。它是一个辅助函数,将帮助我们理解输出,以下是一个示例:
def argmax_pose_predict(scmap, offmat, stride):
"""Combine scoremat and offsets to the final pose."""
num_joints = scmap.shape[2]
pose = []
for joint_idx in range(num_joints):
maxloc = np.unravel_index(np.argmax(scmap[:, :, joint_idx]),
scmap[:, :, joint_idx].shape)
offset = np.array(offmat[maxloc][joint_idx])[::-1] if offmat is not None else 0
pos_f8 = (np.array(maxloc).astype('float') * stride + 0.5 * stride +
offset)
pose.append(np.hstack((pos_f8[::-1],
[scmap[maxloc][joint_idx]])))
return np.array(pose)
现在,让我们执行定义函数的那个单元格。它将立即运行。
以下代码将加载配置文件,即demo/pose_cfg.yaml,setup_pose_prediction(cfg)将返回sess、inputs和outputs。以下是一个示例:
cfg = load_config("demo/pose_cfg.yaml")
sess, inputs, outputs = setup_pose_prediction(cfg)
当我们运行前面的代码时,它将保持 TensorFlow 会话开启,建议只运行一次以避免错误,或者您可能需要重新启动内核。因此,如果命令被执行,我们理解模型已经被加载,正如您在以下输出中看到的那样:
INFO:tensorflow:restoring parameters from models/mpii/mpii-single-resnet-101
现在,我们将看到如何实际应用该模型:
file_name = "testcases/standing-lef-lift.jpg"
image = np.array(imread(file_name))
image_batch = np.expand_dims(image, axis=0).astype(float)
outputs_np = sess.run(outputs, feed_dict={inputs: image_batch})
scmap, locref, pairwise_diff = extract_cnn_output(outputs_np, cfg)
pose = argmax_pose_predict(scmap, locref, cfg.stride)
对于我们的模型,我们必须给我们的文件命名。因此,我们有一个名为testcases的目录,里面有一系列不同姿势的人的股票照片,我们将使用这些照片进行测试。然后,我们需要以合适的格式加载standing-leg-lift.jpg图像。我们将把图像转换成 TensorFlow 实际需要的格式。输入类似于image_batch,它将在0轴上扩展维度。所以,只需创建 TensorFlow 可以使用的数组。然后,outputs_np将运行会话,在下一行提取 CNN 输出,然后进行实际的姿态预测。pose变量在这里使用最好。然后我们应该执行单元格并按Esc按钮进入命令模式。然后,我们需要创建一个新的单元格;输入pose并按Ctrl + Enter*。然后我们将得到以下 2D 数组输出:

输出给出了与手腕、脚踝、膝盖、头部、下巴、肩膀等关节对应的x和y坐标。从这些坐标中,我们得到x坐标、y坐标和匹配分数。我们不需要亚像素级别的精度,所以我们可以将其四舍五入到最接近的整数。在下面的示例中,你可以看到我们已经用数字标记了对应的关节,并在它们之间画了线:
pose2D = pose[:, :2]
image_annot = image.copy()
for index in range(5):
randcolor = tuple([randint(0, 255) for i in range(3)])
thickness = int(min(image_annot[:,:,0].shape)/250) + 1
start_pt = tuple(pose2D[index].astype('int'))
end_pt = tuple(pose2D[index+1].astype('int'))
image_annot = cv2.line(image_annot, start_pt, end_pt, randcolor, thickness)
for index in range(6,11): #next bunch are arms/shoulders (from one hand to other)
randcolor = tuple([randint(0,255) for i in range(3)])
thickness = int(min(image_annot[:,:,0].shape)/250) + 1
start_pt = tuple(pose2D[index].astype('int'))
end_pt = tuple(pose2D[index+1].astype('int'))
image_annot = cv2.line(image_annot, start_pt, end_pt, randcolor, thickness)
#connect Line from chin to top of head
image_annot = cv2.line(image_annot,
tuple(pose2D[12].astype('int')), tuple(pose2D[13].astype('int'))
tuple([randint(0,255) for i in range(3)]), thickness)
我们需要在这里创建一个pose2D标签,然后我们将从前两列中提取 x 和 y 坐标。我们将使用image.copy()来制作一个副本,因为我们希望我们的注释图像与原始图像分开。
我们将运行以下代码来显示原始图像:
figure()
imshow(image)
现在,我们将学习如何注释原始图像。我们将创建图像的一个副本,然后我们将遍历前六个关节并在它们之间画线。它从脚踝开始,标记为1,然后穿过臀部,最后下降到另一只脚踝。数字6到11将是手臂和肩膀,最后两个点是下巴和头顶。我们现在将使用pose2D中的所有这些点用线连接起来。实际上,我们没有腰部和衣领的点,但我们可以很容易地从臀部和肩膀的中点估计它们,这对于完成骨骼很有用。
让我们看看以下代码,它帮助我们估计中点:
# There no actual joints on waist or coLLar,
# but we can estimate them from hip/shoulder midpoints
waist = tuple(((pose2D[2]+pose2D[3])/2).astype('int'))
collar = tuple(((pose2D[8]+pose2D[9])/2).astype('int'))
# draw the "spine"
image_annot = cv2.line(image_annot, waist, collar,
tuple([randint(0,255) for i in range(3)]), thickness)
image_annot = cv2.line(image_annot, tuple(pose2D[12].astype('int')), collar,
tuple([randint(0,255) for i in range(3)]), thickness)
# now Label the joints with numbers
font = cv2.FONT_HERSHEY_SIMPLEX
fontsize = min(image_annot[:,:,0].shape)/750 #scale the font size to the image size
for idx, pt in enumerate(pose2D):
randcolor = tuple([randint(0,255) for i in range(3)])
image_annot = cv2.putText(image_annot, str(idx+1),
tup1e(pt.astype('int')),font, fontsize,
randcolor,2,cv2.LINE_AA)
figure()
imshow(image_annot)
现在,我们可以通过从腰部到衣领,再从衣领到下巴画点来绘制脊柱。我们还可以标记这些关节,以显示我们连接的确切位置,这有助于你的定制应用。我们将标记关节,创建图形,显示注释图像,并处理随机颜色。以下截图显示了输出看起来像什么:

在这里,1 是右脚踝,但它可能是左脚踝,取决于人的面向方向。所以,除了 13(在这里有点遮挡)和 14(稍微超出图像)之外,所有的链接都已经连接。这个的好处是,即使其他关节被遮挡(例如,如果它们在屏幕外或被某物覆盖),它也可能工作。你会注意到图像很简单,有一个平坦的背景,平坦的地板,简单的姿态和衣服。代码也可以处理更复杂的图像,如果你在阅读细节时遇到任何困难,可以使用这里的工具并放大查看。
让我们尝试使用不同的图像并分析我们的结果,如下所示:
file_name = "testcases/mountain_pose.jpg"
image = np.array(imread(file_name))
image_batch = np.expand_dims(image, axis=0).astype(float)
outputs_np = sess.run(outputs, feed_dict={inputs: image_batch})
scmap, locref, pairwise_diff = extract_cnn_output(outputs_np, cfg)
pose = argmax_pose_predict(scmap, locref, cfg.stride)
下面是我们将要测试的图片:

当我们再次运行我们的模型,使用不同的图像时,我们得到以下输出:

如果我们拍一个交叉双臂的人的图像,我们会得到以下截图:

即使双臂交叉,结果仍然非常好。
现在,让我们看看一些比较困难的图像。这可能不会给我们一个完整的运动捕捉姿态估计解决方案的准确结果,但仍然非常令人印象深刻。
选择acrobatic.jpeg,如下所示:

当我们运行这张照片时,得到的输出如下所示:

看起来它找到了关节,或多或少,但没有正确连接它们。它显示这个人的头在他的手上,手触地。我们可以看到结果并不那么好。但我们不能期望所有图像都能得到准确的结果,即使这是最先进的技术。
多人姿态检测
现在,让我们从单人姿态检测转到多人姿态检测。在单人姿态检测中,我们看到代码会取一个单个人的图像,并生成带有所有关节标记的姿态估计。我们现在将学习一个更高级的模型,称为 ArtTrack,它将允许我们计数人数,找到人,并估计他们的姿态。
让我们看看多人姿态检测的代码,以下是一个示例:
import os
import sys
import numpy as np
import cv2 I
from imageio import imread, imsave
from config import load_config
from dataset.factory import create as create_dataset
from nnet import predict
from dataset.pose_dataset import data_to_input
from multiperson.detections import extract_detections
from multiperson.predict import SpatialModel, eval_graph, get_person_conf_mu1ticut
# from muLtiperson.visuaLize import PersonDraw, visuaLize_detections
这有点复杂。我们将首先使用当前目录下的!ls命令列出我们的目录,在那里你会找到一个名为compile.sh的文件。
我们需要运行这个文件,因为这个模块中包含一些二进制依赖项。但是这是一个 shell 脚本文件,你可能在 macOS 或 Linux 上遇到一些问题。因此,为了生成那些特定于操作系统的文件/命令,你需要运行这个脚本。对于 Windows,那些二进制文件已经生成好了。所以,如果你使用的是 Python 和 TensorFlow 的最新版本,那么文件将是兼容的,二进制文件应该可以正常工作。
如果它不起作用,您将需要下载并安装 Visual Studio Community。您可以在github.com/eldar/pose-tensorflow的demo代码部分找到一些关于多人姿态的安装说明。
一旦一切运行正常,我们就可以开始我们的示例。此外,正如我们之前已经讨论过的,我们需要确保重启内核。这是因为如果您已经打开了会话来运行不同的项目,TensorFlow 可能无法计算代码,因为已经加载了之前的模型。始终从一个全新的内核开始是一个好的做法。
我们将运行我们的 %pylab notebook 来进行可视化和数值计算。代码的工作方式与我们之前已经覆盖的类似,其中我们有一些模板代码并加载了一个预训练模型。预训练模型已经包含在内,所以我们不需要下载它。由于 TensorFlow 的原因,代码将在一秒内执行,我们将导入模块并加载存储库。此外,我们还需要分别加载模型并进行预测。如果我们按下 C*trl + Shift *+ -,我们可以将预测分别放入不同的单元格中,使其看起来更整洁。
当我们运行第一个单元格时,我们得到以下输出:

这不是一个大的错误消息,这是因为在这里定义了 imread;笔记本覆盖了它,只给你一个警告消息。我们可以重新运行那段代码来忽略警告并得到整洁的输出。
在这个单元格中,我们将加载 ArtTrack/DeeperCut 作者提供的多人配置文件。
以下行加载了数据集:
cf = load_config("demo/pose_cfg_multi.yaml)
然后,以下行创建模型并加载它:
dataset = create_dataset(cfg)
sm = SpatialModel(cfg)
sm.load()
sess, inputs, outputs = predict.setup_pose_prediction(cfg)
当我们执行这个操作时,我们得到以下输出:

我们将在这里保持会话开启,这样我们就可以继续运行不同的事情,并快速浏览不同的帧。
我们现在将运行一些测试案例,这些案例实际上有多个人,如下所示:
file_name = "testcases/bus_people.jpg"
image = np.array(imread(file_name))
image_batch = data_to_input(image)
# Compute prediction with the CNN
outputs_np = sess.run(outputs, feed_dict={inputs: image_batch})
scmap, locref, pairwise_diff = predict.extract_cnn_output(outputs_np, cfg, dataset
detections = extract_detections(cfg, scmap, locref, pairwise_diff)
unLab, pos_array, unary_array, pwidx_array, pw_array = eval_graph(sm, detections)
person_conf_multi = get_person_conf_multicut(sm, unLab, unary_array, pos_array)
image_annot = image.copy()
for pose2D in person_conf_mu1ti:
font = cv2.FONT_HERSHEY_SIMPLEX
fontsize = min(image_annot[:,:,0].shape)/1000
我们需要将 np.array 转换为平面数组网络,以便使用 sess.run 进行预测,然后使用模型工具提取 CNN 输出和 detections。我们在这里不会标记骨骼,而是用数字标记关节。
当我们运行代码时,我们得到以下输出:

这是一张多人的简单图像,穿着朴素的衣服,背景平坦。这实际上起作用了。然而,数字与之前不同。之前,数字 1 对应右脚踝,向上通过 2、3、4、5 和 6,然后 7 是右腕,以此类推。所以,数字不同,而且更多,这实际上检测到了更多的关节,因为面部有多个数字,所以这里有多点。让我们放大查看细节,如下面的图片所示:

在这里,我们有了面部特征点 1、2、3、4 和 5,因此这可以与第六章中提到的 dlib 检测器结合使用,即dlib 的面部特征追踪和分类。如果我们想了解某人的面部表情,除了全身特征检测器和它们的姿态,这里也可以做到。我们还可以得到一个非常详细的描述,说明人们面向哪个方向,以及他们在图像中确切在做什么。
让我们尝试另一个exercise_class.jpeg图像,它给出了以下输出:

在这里,我们可以看到最右侧的女士膝盖上有多个点。这仍然是一个不错的结果。
让我们再试一张图片,这是我们之前在 GitHub 页面上看到的,gym.png。
你可以看到以下输出:

这个模型确实检测到了这里的身体部位。所以,让我们尝试使用这个模型来检测单个人的姿态。你认为它会起作用吗?答案是是的,它确实起作用了。你可能想知道为什么我们有这个模型,还要使用之前的模型。这个模型在计算上稍微高效一些,所以如果你知道只有一个人,实际上你不需要它,因为这个算法提供了人数。
你可以从可用的照片中选择单个人的照片。例如,我们将选择mountain_pose.jpg,它给出了以下输出:

这也将显示人数,如下面的代码所示:

但是,如果你为单个人使用多人检测器,它可能会过度拟合,并检测到图像中实际不存在的人数。所以,如果你已经知道只有一个人,那么仍然使用原始模型而不是 ArtTrack 模型可能仍然是一个好主意。但如果它确实起作用,尝试两者,或者使用最适合你应用的方法。然而,这可能在复杂图像和复杂多样的姿态上可能不会完美工作。
让我们尝试最后一个island_dance.jpeg图像。以下截图显示了结果:

重新训练人体姿态估计模型
我们现在将讨论如何处理视频以及重新训练我们的人类姿态估计网络。我们已经涵盖了人脸检测以及如何将模型应用于视频。打开视频相当直接,OpenCV 提供了相应的机制。它基本上是逐帧执行相同的事情。以下示例显示了相应的代码:
predictor_path = "./shape_predictor_68_face_landmarks.dat"
detector = dlib.get_fronta1_face_detector()
predictor = dlib.shape_predictor(predictor_path)
#Uncomment Line below if you want to use your webcam
#cap = cv2.VideoCapture(0) #0 is the first camera on your computer, change if you
#more than one camera
#Comment out the Line below if using webcam
cap = cv2.VideoCapture('./rollerc.mp4')
figure(100)
font = cv2.FONT_HERSHEY_SIMPLEX
首先,我们需要创建一个 cv2 捕获设备,然后打开文件,在读取文件的同时,我们应该加载图像并在图像上运行网络。请参考以下代码:
font = cv2.FONT_HERSHEY_SIMPLEX
while(True):
#Capture frame-by-frame
ret, img = cap.read()
img.flags['WRITEABLE']=True #just in case
try:
dets = detector(img, 1)
shape = predictor(img, dets[0])
except:
print('no face detected', end='\r')
cap.release()
break
#similar to previous example, except frame-by-frame here
annotated=img.copy()
head_width = shape.part(16).x-shape.part(6).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)
#Let's see our results
fig=imshow(cv2.cvtColor(annotated,cv2.COLOR_BGR2RGB)) #OpenCV uses BGR format
display.c1ear_output(wait=True)
display.display(gcf())
#When everything is done, release the capture
cap.release()
使用一个好的 GPU,我们应该能够以每秒几帧的速度进行计算,如果不是 30 到 60 FPS,这取决于你的硬件。你应该几乎能够实时完成。
对于训练你的模型,你首先需要确保你有良好的硬件和大量的时间。首先,你需要下载 ImageNet 和 ResNet 模型。然后,你需要查看github.com/eldar/pose-tensorflow/blob/master/models/README.md页面上的步骤和说明。你需要大量的数据,因此你可以使用他们提供的数据。使用你自己的数据可能既耗时又难以获得,但这是可能的。你可以参考提供的上一个链接以获取完整的说明。
这里使用的说明在某些地方使用了 MATLAB 来转换数据,尽管在 Python 中也有方法可以做到这一点,并用 MS COCO 数据集训练模型。这与我们在第二章中做的类似,即使用 TensorFlow 进行图像标题生成,同时也提供了如何使用自己的数据集训练模型的说明。这需要大量的工作和计算能力。你可以尝试这样做,或者使用预训练模型中已经提供的内容,这些内容可以做很多事情。
摘要
在本章中,我们学习了人类姿态估计的基础知识,然后在我们的项目中使用了 DeeperCut 和 ArtTrack 模型进行人类姿态估计。使用这些模型,我们进行了单人和多人的姿态检测。在章节的末尾,我们学习了如何使用模型处理视频,并对定制图像重新训练了模型。
在下一章第五章,《使用 scikit-learn 和 TensorFlow 进行手写数字识别》中,我们将学习如何使用 scikit-learn 和 TensorFlow 进行手写数字识别。
第五章:使用 scikit-learn 和 TensorFlow 进行手写数字识别
在本章中,我们将学习如何将机器学习应用于计算机视觉项目,使用几个不同的 Python 模块。我们还将创建并训练一个支持向量机,它将实际执行我们的数字分类。
在本章中,我们将涵盖以下主题:
-
获取和处理 MNIST 数字数据
-
创建和训练支持向量机
-
将支持向量机应用于新数据
-
介绍 TensorFlow 与数字分类
-
评估结果
获取和处理 MNIST 数字数据
如前所述,我们将使用 scikit-learn 和 TensorFlow 来处理手写数字识别。在这里,我们将学习如何将机器学习应用于计算机视觉项目,我们将学习几种不同的方法和模型,使用几个不同的 Python 模块。让我们开始吧。
你可能已经听说过机器学习。在这里,我们将特别讨论监督机器学习,其中我们有一系列想要完成的例子。所以,我们不是明确告诉计算机我们想要什么,而是给出一个例子。
让我们以 0 到 9 的手写数字为例,这些数字由人类创建的标签指示它们应该是什么。因此,我们不是手动编码特征并明确告诉计算机算法,我们将构建一个模型,其中我们接受这些输入,优化一些函数,如一组变量,然后训练计算机将输出设置为我们所希望的。
因此,我们将从手写数字开始,从 0、1、2、3 等等开始。这是机器学习的一般范式,我们将在这里介绍三种不同的算法。
那么,让我们开始运行一些代码。
打开你的 Jupyter Notebook,就像我们在上一章中所做的那样,让我们在本章中从头开始。如您在以下代码中所观察到的,我们将导入我们的基本模块,例如numpy,它是 Python 中数值计算的基础:
#import necessary modules here
#--the final notebook will have complete codes that can be
#--copied out into self-contained .py scripts
import numpy as np
import matplotlib.pyplot as plt
import cv2
import sys
import tempfile
from sklearn import svm, metrics
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
如您在前面的代码中所见,我们正在导入pyplot,这样我们就可以可视化我们所做的事情。我们还将使用一点 OpenCV 来转换一些图像。我们还将使用 scikit-learn,它在实际模块中缩写为sklearn,同时导入支持向量机,以及一些将给我们提供度量指标的工具。这将告诉我们事情实际上工作得有多好。我们还将导入 TensorFlow,缩写为tf,因为我们将从其中获取我们的数据。
scikit-learn 和 TensorFlow 的一个主要优势是它们内置了获取数字识别的功能,这在计算机视觉和机器学习包中是如此常见。因此,你不必去网站下载,然后自己编写这些行。它会为你处理。因此,scikit-learn 实际上有相当多的内置数据集,一些用于计算机视觉,一些用于其他任务。它有一个数字示例,然后我们可以通过编写datasets并按Tab键来选择可用的内置数据集,如下面的截图所示:

现在,我们有一个内置数据集的列表。例如,你想知道california_housing的价格;也就是说,你想根据房屋的平方英尺和卧室数量等因素估计房价——有一个数据集是针对这个的。其中一些是图像数据,一些不是。所以,如果你想尝试不同的机器学习技术,这可能是一个你想检查的项目,但对于dataset.load_digits(),我们有以下代码来展示它是如何工作的:
#what kind of data do we already have?
from sklearn import datasets
digits=datasets.load_digits()
example_image=digits.images[0]
print(type(example_image))
plt.imshow(example_image); plt.show()
example_image.reshape((8*8,1))
让我们分解并理解这段代码。首先,我们加载一个示例图像,即集合中的第一个图像,如下所示:
example_image=digits.images[0]
print(type(example_image))
数据实际上存储在图像中,它是一个示例数组,其中每个示例都是一个 8 x 8 的手写数字图像。
然后,我们按照以下方式绘制图像:
plt.imshow(example_image); plt.show()
example_image.reshape((8*8,1))
我们应该看到以下输出:

但是我喜欢使用一个稍微高分辨率的示例,我们稍后会从 MNIST 中看到。低分辨率图像在计算上稍微快一些,因为它们是更小的图像。如果我们想要预处理这些图像,它们存储为 8 x 8,我们需要将每个图像转换为 1D 数组。我们可以通过使用reshape函数轻松地做到这一点,就像我们在之前的代码中做的那样:
example_image.reshape((8*8,1))
这将为我们提供一个输出,其中,而不是 8 x 8 的数组,我们得到一个 1 x 64 的数组,如下所示:

现在,我们将使用以下网站可用的 MNIST 数据:
这是一个相当标准的数据集。TensorFlow 足够好,提供了获取这些数据的功能,因此你不必去网站手动下载。我们需要定义data_dir并指定一个保存数据的位置。所以,只需创建这个/tmp/tensorflow/mnist/input_data目录,这将是好的,无论你运行的是哪种操作系统,然后我们有一些从tensorflow和read_data_sets导入的input_data。现在,让我们运行以下代码:
#acquire standard MNIST handwritten digit data
#http://yann.lecun.com/exdb/mnist/
data_dir = '/tmp/tensorflow/mnist/input_data'
mnist = input_data.read_data_sets(data_dir, one_hot=True)
我们应该得到以下输出:

如果你没有文件,代码将下载 gzip 文件,如果你已经有了,它将只读取现有的 gzip 文件并将它们存储在 mnist 变量中。one_hot=True 确保你得到标签,以向量的形式表示,这意味着它不会用像零、一、二、三、四这样的美国数字来标记,而是一个主要由零组成的数组。它将是一个长度为 10 的数组,其中除了一个值为 1 的元素外,其余都是 0。所以,如果我们有,例如,0, 1, 0, 0, 0, 0,等等,那将代表一个 1,如果它是 9,那么除了最后一个值为 1 的元素外,其余都是 0。所以,这是机器学习标记输出的一个有用方法。这是我们获取数据的方式,我们将会使用它;这对于我们实际使用 TensorFlow 时更有帮助,但对于 scikit-learn 实际上确实需要数值。
在我们深入实际机器学习之前,让我们先了解数据。我们有 mnist 变量,它已经按照训练和测试数据分开。在机器学习中,你不想用所有数据来训练;你不想用所有数据来构建你的模型,因为那样你就不知道它将如何处理之前未见过的新的数据例子。你想要做的是将其分成训练数据和测试数据。所以,训练数据将用来构建模型,而测试数据将用来验证它。所以,数据的分割已经为我们完成,只需要以下变量:
#now we load and examine the data
train_data=mnist.train.images
print(train_data.shape)
n_samples = train_data.shape[0]
train_labels=np.array(np.where(mnist.train.labels==1))[1]
plt.imshow(train_data[1000].reshape((28,28))); plt.show()
让我们分解代码以更好地理解。
首先,我们按照以下方式从 train.images 加载 train_data:
#now we load and examine the data
train_data=mnist.train.images
我们将使用 .shape 来查看形状,以便理解它,如下所示:
print(train_data.shape)
如果你需要知道样本数量,我们可以从 shape 输出中提取,如下所示:
n_samples = train_data.shape[0]
再次强调,它是一个 NumPy 数组,所以所有 NumPy 函数和特性都在那里。
然后,执行以下代码以获取 train_labels:
train_labels=np.array(np.where(mnist.train.labels==1))[1]
plt.imshow(train_data[1000].reshape((28,28))); plt.show()
在这里,我们只查看 train.label 等于 1 的位置,并提取这些值以创建一个包含这些值的数组,这将给我们 train_labels。所以,一个一维数组对应于包含每个实际输出的例子数量。我们将只看一个例子;让我们从 55000 个训练例子中取出 1000 个。
运行此代码将给出以下输出:

784 是图像中的像素数,因为它们是 28 x 28 的方块,28 x 28 = 784。所以,我们有 55000 个例子,每个例子有 784 个像素,或者我们称之为特征,然后 train_labels 的长度将是 55000,另一个维度只是 1。这里有一个例子。这些数据已经以 1D 数组的形式提供,这就是为什么我们使用了 reshape 函数并传递了 28 x 28 的值,以便将其转换为我们可以看到的实际图像。
太好了,我们的数据已加载并处理完毕,准备使用,因此我们可以开始实际的机器学习。现在我们的数据已经设置好并准备就绪,我们可以继续到下一节,在那里我们将创建和训练我们的支持向量机,并执行我们的数字分类。
创建和训练支持向量机
在本节中,我们将创建并训练一个支持向量机,该机器将实际执行我们的数字分类。
在第一个例子中,我们将使用 scikit-learn,我们将使用所谓的支持向量机,这是一种非常强大、非常通用的经典机器学习技术,可以学习各种函数和从输入到输出的各种映射。我们将进行分类,即映射输入为一个像素数组,在我们的情况下,我们将把每个输入分类到十个类别之一,对应于十个数字。但我们可以将不同类型的事物分类为连续有序函数,这被称为回归,这可能很有用,例如,如果你想提取位置或体积区域,它不仅仅适合于一个整洁的分类。
对于本节,我们将主要进行分类。因此,scikit-learn 使得创建此类模型变得非常简单。可以使用svm.SVC调用支持向量分类器,其中支持向量机来自sklearn包,我们为模型有一个名为gamma的元参数,它类似于半径的倒数,是支持向量的影响区域,如下面的代码所示:
# Create a classifier: a support vector classifier
classifier = svm.SVC(gamma=0.001)
# Learn about gamma and other SVM parameters here:
# http://scikit-learn.org/stable/auto_examples/svm/plot_rbf_parameters.html
# Exercise: Experiment with the parameters to see how they affect execution
# time and accuracy
# Train the model -- we're only going to use the training data (and not
# the test data) to ensure that our model generalizes to unseen cases.
# This (training) is typically what takes the most computational time
# when doing machine learning.
classifier.fit(train_data, train_labels)
支持向量机的工作原理在此处没有涉及,因为关于这个主题有大量的文献,并且为了学习它,并不绝对有必要完全理解它。现在,我们只是看看我们如何将其应用于某些情况。
gamma参数是我建议你作为练习进行实验的东西。我们将从一个已知的工作良好的gamma参数开始,即.001,但你应该了解其他可用的参数。我建议你访问scikit-learn.org/stable/auto_examples/svm/plot_rbf_parameters.html,并且我再次建议你尝试使用它来查看它如何影响执行时间和准确性。但是,这里重要的是我们要知道我们可以用一行代码创建我们的模型。它定义了模型,但我们实际上并没有训练它。我们实际上没有提供任何数据,也没有调整其参数,以便它能够产生期望的输出。现在,如果我们给它一个五的图像,它会说,没问题,这是一个 5。所以,为了做到这一点,我们必须对其进行拟合。
在前面的代码中,我们创建了我们的分类器,它非常简单:classifier.fit。我们给它我们从前一个代码执行中获得的train_data和train_labels。提前提醒一下,这个执行将需要几分钟;通常是这样的。通常,训练过程是机器学习中最慢的部分。这通常是情况,但这不应该太糟糕。这只需要几分钟,而且,我们再次只是使用您的训练数据,这样我们就可以验证这将对未见案例进行泛化。
现在我们已经看到了我们的支持向量机,并且它实际上已经被训练了,我们可以继续到下一个部分,在那里我们将支持向量机应用于它未训练过的新的数据。
将支持向量机应用于新数据
现在我们有了我们的训练支持向量机,我们可以实际上将支持向量机应用于未见过的新的数据,并看到我们的数字分类器实际上是否在起作用。
细胞执行成功并且如果一切正常工作,我们应该看到以下输出:

这只是创建支持向量分类器的输出。这仅仅提供了我们使用的元数据参数的信息;我们使用了所谓的径向基函数核,拟合数据没有产生任何错误信息。所以,这意味着代码已经工作。所以,现在我们有了我们的训练模型,我们想看看它在它未见过的数据上表现如何。
现在,我们将获取我们的测试数据,如下所示:
# Now predict the value of the digit on the test data:
test_data=mnist.test.images
test_labels=np.array(np.where(mnist.test.labels==1))[1]
expected = test_labels
predicted = classifier.predict(test_data)
我们获取mnist.test.images,它等于mnist.train.images,并以相同的方式提取标签,通过调用expected变量,然后我们将从classifier模型计算predicted,使用classifier.predict(test_data)。所以,这需要一点时间来执行。执行后,应该没有错误信息,这表明我们的预测运行成功。
所以,现在我们可以看到我们做得怎么样。我们将使用 scikit-learn 的内置度量函数。我们将记录一些度量,例如精确度和召回率,如果您想了解这些含义,我推荐以下维基百科文章:
简而言之,它们是评估你的机器学习算法表现如何的不同指标。准确率可能是最常用的。它是简单的:正确数据点除以总数。但也有精确率和召回率,它权衡了真实阳性、真实阴性、假阳性和假阴性,哪个是最好的取决于你的应用。它取决于假阳性和假阴性哪个更糟糕,以及如此等等。此外,我们还将输出所谓的混淆矩阵,它告诉你哪些是成功的,哪些被错误分类了。让我们运行以下代码:
# And display the results
print("See https://en.wikipedia.org/wiki/Precision_and_recall to understand metric definitions")
print("Classification report for classifier %s:\n%s\n"
% (classifier, metrics.classification_report(expected, predicted)))
print("Confusion matrix:\n%s" % metrics.confusion_matrix(expected, predicted))
images_and_predictions = list(zip(test_data, predicted))
for index, (image, prediction) in enumerate(images_and_predictions[:4]):
plt.subplot(2, 4, index + 5)
plt.axis('off')
plt.imshow(image.reshape((28,28)), cmap=plt.cm.gray_r, interpolation='nearest')
plt.title('Prediction: %i' % prediction)
plt.show()
它应该给出以下输出:

好吧,所以我们得到了分类报告,我们可以看到precision(精确率)、recall(召回率)以及另一个称为f1-score的指标,你可以在同一篇维基百科文章中了解到这些。简而言之,零是最坏的情况,一是最理想的情况。在先前的屏幕截图中,我们可以看到不同数字的precision、recall和f1-score,我们可以看到我们处于 90%的范围内;它有所变化,这是可以接受的。它取决于你的应用,但这可能已经足够好了,或者可能非常糟糕。这取决于。我们实际上稍后会看到如何使用更强大的模型做得更好。我们可以看到它总体上是有效的。我们来看看混淆矩阵,其中列告诉你实际值是什么,行告诉你预测值是什么。理想情况下,我们会看到对角线上的所有大值,其他地方都是零。总是会有一些错误,因为我们都是人类,所以这种情况会发生,但就像我说的,我们将看看是否可以做得更好,在大多数情况下,它确实有效。现在,我们可以看到一些随机输出的示例,其中有一些数字如下:

如我们所见,所有的预测都根据它们的图像是正确的。好吧,一切都很好,但我感觉我现在有点是在相信计算机的话了。我想用我自己的数据来测试它。我想看看这实际上工作得怎么样,这在机器学习中通常是一个推荐的步骤。你想要用你自己的数据来测试它,以便真正知道它是否在正常工作,而且,如果不是其他原因,这会让人感到更加满意。所以,这里有一小段代码,它将使用 Jupyter 的小部件功能,它的交互功能:
#Let's test our model on images we draw ourselves!
from matplotlib.lines import Line2D
%pylab notebook
#This is needed for plot widgets
class Annotator(object):
def __init__(self, axes):
self.axes = axes
self.xdata = []
self.ydata = []
self.xy = []
self.drawon = False
def mouse_move(self, event):
if not event.inaxes:
return
x, y = event.xdata, event.ydata
if self.drawon:
self.xdata.append(x)
self.ydata.append(y)
self.xy.append((int(x),int(y)))
line = Line2D(self.xdata,self.ydata)
line.set_color('r')
self.axes.add_line(line)
plt.draw()
def mouse_release(self, event):
# Erase x and y data for new line
self.xdata = []
self.ydata = []
self.drawon = False
def mouse_press(self, event):
self.drawon = True
img = np.zeros((28,28,3),dtype='uint8')
fig, axes = plt.subplots(figsize=(3,3))
axes.imshow(img)
plt.axis("off")
plt.gray()
annotator = Annotator(axes)
plt.connect('motion_notify_event', annotator.mouse_move)
plt.connect('button_release_event', annotator.mouse_release)
plt.connect('button_press_event', annotator.mouse_press)
axes.plot()
plt.show()
所以,现在我们实际上要创建一个小绘图小部件。它将允许我们生成自己的数字。让我们看看代码。
让我们从matplotlib.line导入Line2D,这将允许我们绘制单独的线条,就像根据我们的鼠标移动创建一种矢量图像一样:
#Let's test our model on images we draw ourselves!
from matplotlib.lines import Line2D
我们执行%pylab notebook;百分号表示以下魔法命令:
%pylab notebook
这是一种 Jupyter 和 Pylab 笔记本中的元命令,它将大量内容加载到你的命名空间中用于绘图和数值计算。这不是必需的,因为我们已经用 NumPy 和 Matplotlib 做了这件事,但为了启用小部件,我们使用这个命令。
然后,创建这个Annotator类,它包含当我们在显示的图像上移动鼠标时发生回调的代码,如下所示:
class Annotator(object):
def __init__(self, axes):
self.axes = axes
self.xdata = []
self.ydata = []
self.xy = []
self.drawon = False
def mouse_move(self, event):
if not event.inaxes:
return
x, y = event.xdata, event.ydata
if self.drawon:
self.xdata.append(x)
self.ydata.append(y)
self.xy.append((int(x),int(y)))
line = Line2D(self.xdata,self.ydata)
line.set_color('r')
self.axes.add_line(line)
plt.draw()
def mouse_release(self, event):
# Erase x and y data for new line
self.xdata = []
self.ydata = []
self.drawon = False
def mouse_press(self, event):
self.drawon = True
我们不需要理解Annotator类,但如果你将来想要进行标注或绘制某些内容,以及获取完整的代码片段,这可能会很有用。
然后,我们将创建一个空白图像,大小与我们的图像相同。目前它只是三个 RGB 值。即使我们最终会将其变为黑白,因为它就是我们的数据。创建图像如下:
img = np.zeros((28,28,3),dtype='uint8')
现在,创建一个图表,显示它,并将我们的annotator函数连接到它,如下所示:
fig, axes = plt.subplots(figsize=(3,3))
axes.imshow(img)
plt.axis("off")
plt.gray()
annotator = Annotator(axes)
plt.connect('motion_notify_event', annotator.mouse_move)
plt.connect('button_release_event', annotator.mouse_release)
plt.connect('button_press_event', annotator.mouse_press)
axes.plot()
plt.show()
运行代码后,我们应该得到以下输出:

那么,让我们画一下数字三:

现在,这有点慢,而且并不完全能替代 Photoshop,但这种方法仍然比进入一个单独的程序创建图像文件、确保其格式正确、保存它,然后编写代码加载它并使其正确要快。因此,这将使我们能够快速地玩和实验我们的模型。我们刚刚创建了一种线数组,因此我们需要将其光栅化并处理,使其看起来更像实际的手写数字,这可能是来自扫描的铅笔草图或压力感应平板。以下是如何做到这一点:
# Now we see how our model "sees" (predicts the digit from)
# our hand drawn image...
# First, we rasterize (convert to pixels) our vector data
# and process the image to more closely resemble something
# drawn with a pencil or pressure-sensitive tablet.
digimg = np.zeros((28,28,3),dtype='uint8')
for ind, points in enumerate(annotator.xy[:-1]):
digimg=cv2.line(digimg, annotator.xy[ind], annotator.xy[ind+1],(255,0,0),1)
digimg = cv2.GaussianBlur(digimg,(5,5),1.0)
digimg = (digimg.astype('float') *1.0/np.amax(digimg)).astype('float')[:,:,0]
digimg **= 0.5; digimg[digimg>0.9]=1.0
#The model is expecting the input in a particular format
testim = digimg.reshape((-1,28*28))
print("Support vector machine prediction:",classifier.predict( testim ))
outimg = testim.reshape((28,28))
figure(figsize=(3,3)); imshow(outimg);
让我们看看代码。首先,我们创建一个空白图像,如下所示:
digimg = np.zeros((28,28,3),dtype='uint8')
我们遍历来自annotator的xy对,然后我们将在光栅化图像上绘制线条,如下所示:
for ind, points in enumerate(annotator.xy[:-1]):
digimg=cv2.line(digimg, annotator.xy[ind], annotator.xy[ind+1],(255,0,0),1)
digimg = cv2.GaussianBlur(digimg,(5,5),1.0)
然后,我们将图像转换为float类型,范围从0到1,就像我们的输入数据一样,如下所示:
digimg = (digimg.astype('float') *1.0/np.amax(digimg)).astype('float')[:,:,0]
然后,我们将它稍微调整得更接近1,因为这就是我们的输入图像看起来像的,以及我们的模型所期望的:
digimg **= 0.5; digimg[digimg>0.9]=1.0
然后,我们有了二维图像,但当然,为了运行它通过我们的模型,我们需要将其展平为1 x 784,这就是reshape函数的作用:
#The model is expecting the input in a particular format
testim = digimg.reshape((-1,28*28))
然后,我们将运行它通过我们的classifier,并打印输出。我们将创建一个图表,我们可以看到我们的光栅化图像如下所示:
print("Support vector machine prediction:",classifier.predict( testim ))
outimg = testim.reshape((28,28))
figure(figsize=(3,3)); imshow(outimg);
我们应该得到以下输出:

我们画了一个三,预测结果是 3。太棒了。让我们尝试其他的东西。通过按 Ctrl + Enter 清除之前的输出,我们得到一个警告信息;它只是告诉我们它覆盖了一些创建的变量。这不是什么大问题。你可以安全地忽略它。只是提前提醒,你的使用效果可能会有所不同,这取决于你的书写风格和训练数据中的内容。如果你希望每次都能完美工作,或者尽可能接近完美,你可能需要在自己的书写上训练它。
让我们尝试一个零:

以下是输出结果:

因此,你可以看到它不起作用的例子。预测应该是零,但模型不知何故预测了三。有可能如果你重新绘制它,它可能会工作。所以,再次提醒,你的使用效果可能会有所不同。你可以尝试实验,你也可以玩玩预处理,尽管据我所知,这工作得相当好。但无论如何,我们可以看到我们的模型至少在大部分情况下是正常工作的。所以,关于 scikit-learn 支持向量机的内容就到这里了。现在,在我们下一节中,我们将介绍 TensorFlow 并使用它进行数字分类。
使用数字分类介绍 TensorFlow
我们将看到 TensorFlow 的实际应用,并了解如何用可管理的代码量进行数字分类。TensorFlow 是 Google 的机器学习库,用于一般的数值分析。它被称为 TensorFlow,因为它据说可以流动张量,其中张量被定义为 n 维的数组。张量具有多维数组所不具备的真正几何意义,但我们只是使用这个术语。张量只是一个多维数组。
在这里,我们将进行一个简单的 softmax 示例。这是一个非常简单的模型;你可以访问 TensorFlow 的官方网站 (www.tensorflow.org/get_started/mnist/beginners) 获取更多信息。让我们看一下以下代码:
data_dir = '/tmp/tensorflow/mnist/input_data'
mnist = input_data.read_data_sets(data_dir, one_hot=True)
# Create the model
x = tf.placeholder(tf.float32, [None, 784])
W = tf.Variable(tf.zeros([784, 10]))
b = tf.Variable(tf.zeros([10]))
y = tf.matmul(x, W) + b
# Define loss and optimizer
y_ = tf.placeholder(tf.float32, [None, 10])
cross_entropy = tf.reduce_mean(
tf.nn.softmax_cross_entropy_with_logits(labels=y_, logits=y))
train_step = tf.train.GradientDescentOptimizer(0.5).minimize(cross_entropy)
sess = tf.InteractiveSession()
tf.global_variables_initializer().run()
# Train
for _ in range(1000):
batch_xs, batch_ys = mnist.train.next_batch(100)
sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})
# Test trained model
correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
print(\"Model accuracy:\",sess.run(accuracy, feed_dict={x: mnist.test.images,
y_: mnist.test.labels}))
简而言之,你将取你的输入数据,并将其乘以一个矩阵。数据有784个点。每个点都有一个矩阵值,对于10个类别中的每一个,你将通过乘以 784 × 784 并求和来计算一个内积。将会有10个输出。它将是一个 1 乘以 10 的数组,你将向数组的输出添加一个偏置变量,并通过softmax函数运行它,这将将其转换为某种东西。矩阵的输出加上偏置将计算一个在0到1范围内的值,这大致对应于该数据属于该类别的概率。例如,可能有一个 0.4%的概率或 40%的概率是1,2%的概率是2,90%的概率是9,输出将是那个最大输出。
TensorFlow 非常复杂。这里的设置比 scikit-learn 示例中要复杂一些。你可以在他们的网站上了解更多信息。现在,让我们详细地通过以下代码:
data_dir = '/tmp/tensorflow/mnist/input_data'
mnist = input_data.read_data_sets(data_dir, one_hot=True)
我们已经在前面的例子中这样做过了。现在,我们将从data_dir获取数据;确保它在我们的mnist变量中。
然后,我们创建模型,其中x对应于我们的输入数据,尽管我们还没有加载数据,但我们只需要创建一个占位符,这样 TensorFlow 就知道东西在哪里了。我们不需要知道有多少个例子,这就是None维度的含义,但我们确实需要知道每个例子有多大,在这个例子中是784。W是乘以x类别的矩阵,对图像进行内积,784 点 784,你这样做10次。所以,这对应于一个 784/10 的矩阵,10是类别的数量;然后,你向那个添加b偏置变量。W和b的值是 TensorFlow 将根据我们的输入为我们产生的,y定义了对我们的数据进行矩阵乘法时实际要执行的操作。我们按照以下方式向它添加b偏置变量:
x = tf.placeholder(tf.float32, [None, 784])
W = tf.Variable(tf.zeros([784, 10]))
b = tf.Variable(tf.zeros([10]))
y = tf.matmul(x, W) + b
我们需要为我们的标记数据创建一个占位符,如下所示:
y_ = tf.placeholder(tf.float32, [None, 10])
为了进行机器学习,你需要一个损失函数或适应度函数,它告诉你根据像W和b这样的学习参数,你的模型做得有多好。因此,我们将使用所谓的交叉熵;我们不会深入讨论交叉熵,但那将给我们一些标准,让我们知道我们正在接近一个工作的模型,如下面的代码行所示:
cross_entropy = tf.reduce_mean(
tf.nn.softmax_cross_entropy_with_logits(labels=y_, logits=y))
train_step = tf.train.GradientDescentOptimizer(0.5).minimize(cross_entropy)
随着我们添加越来越多的数据,我们将使用所谓的GradientDescentOptimizer来最小化误差,最小化交叉熵,并尽可能使我们的模型拟合得更好。
在下面的代码中,我们实际上将首先创建一个交互式会话,如下所示:
sess = tf.InteractiveSession()
tf.global_variables_initializer().run()
for _ in range(1000):
batch_xs, batch_ys = mnist.train.next_batch(100)
sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})
我们希望使其成为一个交互式会话,这样我们就可以在之后使用我们的模型并给它添加新数据。我们将初始化run(),然后我们将分批计算数据。TensorFlow 是一个非常强大的程序,它允许你分割你的数据。我们在这里不会这样做,但你可以用它轻松地运行并行化代码。在这里,我们将迭代1000次,并在分批中输入我们的训练数据。
在运行之后,我们将看看我们做得如何,并查看我们的预测数据与给定的标签相等的部分。我们可以通过查看平均有多少预测是正确的来计算accuracy。然后,按照以下方式打印数据:
correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
print(\"Model accuracy:\",sess.run(accuracy, feed_dict={x: mnist.test.images,
y_: mnist.test.labels}))
以下是对应的输出:

由于这是一个非常简单的模型,它运行得很快,我们可以看到我们得到了不到 92%的准确率。代码执行得更快,但准确率略低于我们的支持向量机(SVM),但这没关系。这段代码只是提供了一个 TensorFlow 如何工作的非常简单的例子。
你会很快变得稍微高级一些,但让我们像之前一样测试以下代码:
img = np.zeros((28,28,3),dtype='uint8')
fig, axes = plt.subplots(figsize=(3,3))
axes.imshow(img)
plt.axis("off")
plt.gray()
annotator = Annotator(axes)
plt.connect('motion_notify_event', annotator.mouse_move)
plt.connect('button_release_event', annotator.mouse_release)
plt.connect('button_press_event', annotator.mouse_press)
axes.plot()
plt.show()
我们得到了以下输出:

我们初始化了注释器,并输入一个数字。试一个3:

现在,我们将对绘制的数字进行预处理,这几乎与之前的代码相同,它将遍历我们的数据及其可能的类别,看看 TensorFlow 的softmax模型认为哪个是最好的:
for tindex in range(10):
testlab = np.zeros((1,10))
testlab[0,tindex] = 1
if sess.run(accuracy, feed_dict={x: testim, y_ : testlab}) == 1:
break
因此,我们将运行前面的代码块,如图所示,它从3预测出3:

有时它可能无法正确预测,这很遗憾。所以,有两种方法可以改进:在自己的手写数据上训练或使用更好的模型。
我们将进入本节中最强大的模型。在这里,我们将简要介绍使用卷积神经网络(CNNs)的深度学习。这里我们不涉及理论。关于深度学习和多层神经网络有很多东西要了解。深度学习是一个深奥的主题,但在这个章节中,我们将看看我们如何实际上使用相对简单的代码块将最先进的机器学习技术应用于数字识别。
因此,我们这里有一个deepnn(x)函数,它创建我们的深度神经网络,找到我们的隐藏层或卷积层,池化层等等,并定义了我们从输入所需的所有内容:
def deepnn(x):
with tf.name_scope('reshape'):
x_image = tf.reshape(x, [-1, 28, 28, 1])
deepnn构建用于对数字进行分类的深度网络图,reshape函数是在卷积神经网络中使用。这里使用的参数是:
-
x:一个具有维度(N_examples,784)的输入张量,其中784是标准 MNIST 图像中的像素数。 -
y:一个形状为(N_examples,10)的张量,其值等于将数字分类到 10 个类别(数字 0-9)的逻辑。keep_prob是一个表示 dropout 概率的标量占位符。
这返回一个元组(y, keep_prob)。最后一个维度是用于特征的——这里只有一个,因为图像是灰度的——对于 RGB 图像将是 3,对于 RGBA 将是 4,依此类推。
第一个卷积层将一个灰度图像映射到32个特征图:
with tf.name_scope('conv1'):
W_conv1 = weight_variable([5, 5, 1, 32])
b_conv1 = bias_variable([32])
h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
# Pooling layer - downsamples by 2X.
with tf.name_scope('pool1'):
h_pool1 = max_pool_2x2(h_conv1)
# Second convolutional layer -- maps 32 feature maps to 64.
with tf.name_scope('conv2'):
W_conv2 = weight_variable([5, 5, 32, 64])
b_conv2 = bias_variable([64])
h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
# Second pooling layer.
with tf.name_scope('pool2'):
h_pool2 = max_pool_2x2(h_conv2)
# Fully connected layer 1 -- after 2 round of downsampling, our 28x28 image
# is down to 7x7x64 feature maps -- maps this to 1024 features.
with tf.name_scope('fc1'):
W_fc1 = weight_variable([7 * 7 * 64, 1024])
b_fc1 = bias_variable([1024])
h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64])
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)
# Dropout - controls the complexity of the model, prevents co-adaptation of
# features.
with tf.name_scope('dropout'):
keep_prob = tf.placeholder(tf.float32)
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)
# Map the 1024 features to 10 classes, one for each digit
with tf.name_scope('fc2'):
W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])
y_conv = tf.matmul(h_fc1_drop, W_fc2) + b_fc2
return y_conv, keep_prob
def conv2d(x, W):
"""conv2d returns a 2d convolution layer with full stride."""
return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')
def max_pool_2x2(x):
"""max_pool_2x2 downsamples a feature map by 2X."""
return tf.nn.max_pool(x, ksize=[1, 2, 2, 1],
strides=[1, 2, 2, 1], padding='SAME')
def weight_variable(shape):
"""weight_variable generates a weight variable of a given shape."""
initial = tf.truncated_normal(shape, stddev=0.1)
return tf.Variable(initial)
def bias_variable(shape):
"""bias_variable generates a bias variable of a given shape."""
initial = tf.constant(0.1, shape=shape)
return tf.Variable(initial)
我们有执行卷积、权重变量、偏置变量等函数。然后,我们有这里的主要代码:
###begin main code
data_dir= '/tmp/tensorflow/mnist/input_data'
# Import data
mnist = input_data.read_data_sets(data_dir, one_hot=True)
# Create the model
x = tf.placeholder(tf.float32, [None, 784])
# Define loss and optimizer
y_ = tf.placeholder(tf.float32, [None, 10])
# Build the graph for the deep net
y_conv, keep_prob = deepnn(x)
with tf.name_scope('loss'):
cross_entropy = tf.nn.softmax_cross_entropy_with_logits(labels=y_,
logits=y_conv)
cross_entropy = tf.reduce_mean(cross_entropy)
with tf.name_scope('adam_optimizer'):
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
with tf.name_scope('accuracy'):
correct_prediction = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y_, 1))
correct_prediction = tf.cast(correct_prediction, tf.float32)
accuracy = tf.reduce_mean(correct_prediction)
graph_location = tempfile.mkdtemp()
print('Saving graph to: %s' % graph_location)
train_writer = tf.summary.FileWriter(graph_location)
train_writer.add_graph(tf.get_default_graph())
# Let's run the model
sess = tf.InteractiveSession()
sess.run(tf.global_variables_initializer())
for i in range(20000):
batch = mnist.train.next_batch(50)
if i % 100 == 0:
train_accuracy = accuracy.eval(feed_dict={
x: batch[0], y_: batch[1], keep_prob: 1.0})
print('step %d, training accuracy %g' % (i, train_accuracy))
train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})
# How did we do?
print('test accuracy %g' % accuracy.eval(feed_dict={
x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0}))
mnist变量获取数据,以防我们还没有它。我们定义了输入的占位符,输出构建了图。然后我们定义了fitness函数和cross-entropy并创建了我们的图。在创建会话时我们必须小心;在他们网站的示例中,他们只是创建了一个正常的会话。我们希望有一个交互式会话,这样我们就可以将我们的模型应用于我们自己生成数据,并且我们将将其分成批次。我们将运行它,每100次迭代它将告诉我们它正在做什么,然后,在最后,它将告诉我们我们的准确率。
让我们运行代码并提取数据,你可以看到以下统计数据:

它开始时的训练准确率非常差,但很快就上升到超过 90%,然后跃升至1。它并不完全是 100%,但通常这意味着它大约是 99%,所以非常接近1。这通常需要几分钟。好的,现在我们已经创建了我们的 TensorFlow 分类器。
评估结果
我们完成训练后,如以下截图所示,我们得到了超过 99%的结果,这比我们用softmax或我们的 SVM 得到的结果要好得多:

深度学习可能是有史以来最强大的机器学习技术,因为它能够学习非常复杂的模式识别。它几乎统治了所有其他技术,包括高级计算机视觉、语音处理等——这些是传统机器学习技术不太成功的领域。然而,这并不意味着你想要在所有事情上都使用深度学习。深度学习通常需要大量示例——有时是数千甚至数百万个示例——并且它也可能非常计算密集。因此,它并不总是最佳解决方案,尽管它非常强大,正如我们在这里所看到的。所以,99%几乎就是你能得到的最佳结果。
下面的代码用于绘制数字:
# Test on handwritten digits again
img = np.zeros((28,28,3),dtype='uint8')
fig, axes = plt.subplots(figsize=(3,3))
axes.imshow(img)
plt.axis("off")
plt.gray()
annotator = Annotator(axes)
plt.connect('motion_notify_event', annotator.mouse_move)
plt.connect('button_release_event', annotator.mouse_release)
plt.connect('button_press_event', annotator.mouse_press)
axes.plot()
plt.show()
以下代码将手写数字图像进行光栅化和预处理:
# Rasterize and preprocess the above
digimg = np.zeros((28,28,3),dtype='uint8')
for ind, points in enumerate(annotator.xy[:-1]):
digimg=cv2.line(digimg, annotator.xy[ind], annotator.xy[ind+1],(255,0,0),1)
digimg = cv2.GaussianBlur(digimg,(5,5),1.0)
digimg = (digimg.astype('float') *1.0/np.amax(digimg)).astype('float')[:,:,0]
digimg **= 0.5; digimg[digimg>0.9]=1.0
testim = digimg.reshape((-1,28*28))
# And run through our model
for tindex in range(10):
testlab = np.zeros((1,10))
testlab[0,tindex] = 1
if accuracy.eval(feed_dict={x: testim, y_: testlab,
keep_prob: 1.0}) == 1:
break
print("Predicted #:",tindex) #tindex = TF model prediction
# Display our rasterized digit
outimg = testim.reshape((28,28))
figure(figsize=(3,3)); imshow(outimg)
因此,让我们再次在我们的手写数字0上测试它:

再次强调,我们处理矢量化图像的代码是相似的,我们得到了输出和光栅化形式,然后将其通过我们的模型,在这里进行 accuracy.eval。正如我们可以在前面的屏幕截图中所见,我们得到了预期的零,这是完美的。因此,在下一章中,我们将更多地讨论使用 CNN 的深度学习,但我们已经看到,它只需要相对较少的代码就非常强大,并且我们能够将其应用于我们特定的数字识别问题。好的,那么,有了这个,我们将继续进入下一章,即第六章,使用 dlib 进行面部特征跟踪和分类。
摘要
在本章中,我们学习了如何使用 TensorFlow 的 softmax 进行数字分类。我们学习了如何获取和处理 MNIST 数字数据。然后我们学习了如何创建和训练支持向量机,并将其应用于新数据。
在下一章中,我们将学习使用 dlib 进行面部特征跟踪和分类。
第六章:使用 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 来执行你的自定义项目。


浙公网安备 33010602011771号