Python-OpenCV-3-x-示例-全-
Python OpenCV 3.x 示例(全)
零、前言
计算机视觉在现代技术中无处不在。 适用于 Python 的 OpenCV 使我们能够实时运行计算机视觉算法。 随着功能强大的机器的出现,我们获得了更多的处理能力。 使用这项技术,我们可以将计算机视觉应用无缝集成到云中。 Web 开发人员可以开发复杂的应用,而不必重新发明轮子。 本书是一本实用的教程,涵盖了不同级别的各种示例,向您介绍了 OpenCV 的不同功能及其实际实现。
这本书是给谁的
本书适用于刚接触 OpenCV 并希望使用 OpenCV 和 Python 开发计算机视觉应用的 Python 开发人员。 对于想在云上部署计算机视觉应用的通用软件开发人员,这本书也很有用。 熟悉基本数学概念(例如向量和矩阵)将很有帮助。
本书涵盖的内容
第 1 章,“将几何变换应用于图像”解释了如何将几何变换应用于图像。 在本章中,我们将讨论仿射和投影变换,并了解如何使用它们将酷炫的几何效果应用于照片。 本章将从在多个*台(例如 Mac OS X,Linux 和 Windows)上安装 OpenCV-Python 的过程开始。 您还将学习如何以各种方式操作图像,例如调整大小和更改色彩空间。
第 2 章,“检测边缘并应用图像过滤器”展示了如何使用基本的图像处理运算符以及如何使用它们来构建更大的项目。 我们将讨论为什么需要边缘检测以及如何在计算机视觉应用中以各种不同方式使用边缘检测。 我们将讨论图像过滤以及如何将其应用于照片的各种视觉效果。
第 3 章,“卡通化图像”显示了如何使用图像过滤器和其他变换对给定图像进行卡通化。 我们将看到如何使用网络摄像头捕获实时视频流。 我们将讨论如何构建实时应用,在该应用中,我们从流中的每个帧中提取信息并显示结果。
第 4 章,“检测和跟踪不同的身体部位”显示了如何检测和跟踪实时视频流中的面部。 我们将讨论面部检测管道,并了解如何使用它来检测和跟踪面部的不同部分,例如眼睛,耳朵,嘴巴和鼻子。
第 5 章,“从图像中提取特征”与检测图像中的显着点(称为关键点)有关。 我们将讨论为什么这些要点很重要,以及如何使用它们来理解图像的内容。 我们将讨论可用于检测显着点并从图像中提取特征的不同技术。
第 6 章,“接缝雕刻”显示了如何进行内容感知的图像大小调整。 我们将讨论如何检测图像的有趣部分,并了解如何调整给定图像的大小而又不恶化那些有趣的部分。
第 8 章,“检测形状和分割图像”显示了如何进行图像分割。 我们将讨论如何以最佳方式将给定图像划分为其组成部分。 您还将学习如何在图像中将前景与背景分开。
第 8 章,“对象跟踪”向您展示如何跟踪实时视频流中的不同对象。 在本章的最后,您将能够跟踪通过网络摄像头捕获的实时视频流中的任何对象。
第 9 章,“对象识别”显示了如何构建对象识别系统。 我们将讨论如何使用这些知识来构建视觉搜索引擎。
第 10 章,“增强现实”显示了如何构建增强现实应用。 到本章末,您将能够使用网络摄像头构建一个有趣的增强现实项目。
第 11 章,“通过人工神经网络进行机器学习”,展示了如何使用最新的 OpenCV 实现构建高级图像分类器和对象识别。 到本章末,您将能够了解神经网络如何工作以及如何将其应用于机器学习以构建高级图像工具。
充分利用这本书
您需要以下软件:
- OpenCV 3.1 或更高版本
- NumPy 1.13 或更高
- SciPy 1.0 或更高
- Scikit-learn 0.19 或更高
- pickleshare 0.7 或更高
硬件规格要求是任何至少具有 8 GB DDR3 RAM 的计算机。
下载示例代码文件
您可以从 www.packtpub.com 的帐户中下载本书的示例代码文件。 如果您在其他地方购买了此书,则可以访问 www.packtpub.com/support 并注册以将文件直接通过电子邮件发送给您。
您可以按照以下步骤下载代码文件:
- 登录或登录 www.packtpub.com 。
- 选择支持选项卡。
- 单击代码下载和勘误。
- 在搜索框中输入书籍的名称,然后按照屏幕上的说明进行操作。
下载文件后,请确保使用以下最新版本解压缩或解压缩文件夹:
- Windows 的 WinRAR/7-Zip
- Mac 的 Zipeg/iZip/UnRarX
- Linux 的 7-Zip/PeaZip
本书的代码包也托管在 GitHub 上。 我们还从这里提供了丰富的书籍和视频目录中的其他代码包。 去看一下!
下载彩色图像
我们还提供了 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。 您可以在此处下载。
使用约定
本书中使用了许多文本约定。
CodeInText:指示文本,数据库表名称,文件夹名称,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄中的代码字。 这是一个示例:“imwrite()方法会将灰度图像保存为名为output.png的输出文件。”
代码块设置如下:
import cv2
img = cv2.imread('images/input.jpg')
cv2.imwrite('images/output.png', img, [cv2.IMWRITE_PNG_COMPRESSION])
当我们希望引起您对代码块特定部分的注意时,相关行或项目以粗体显示:
import cv2
img = cv2.imread('images/input.jpg')
cv2.imwrite('images/output.png', img, [cv2.IMWRITE_PNG_COMPRESSION])
任何命令行输入或输出的编写方式如下:
$ pip install numpy
粗体:表示新术语,重要单词或您在屏幕上看到的单词。 例如,菜单或对话框中的单词会出现在这样的文本中。 这是一个示例:“RGB:可能是最受欢迎的色彩空间。它代表红色,绿色和蓝色。”
警告或重要提示如下所示。
提示和技巧如下所示。
一、将几何变换应用于图像
在本章中,我们将学习如何将冷酷的几何效果应用于图像。 在开始之前,我们需要安装 OpenCV-Python。 我们将解释如何编译和安装必要的库,以遵循本书中的每个示例。
在本章结束时,您将了解:
- 如何安装 OpenCV-Python
- 如何读取,显示和保存图像
- 如何转换为多个色彩空间
- 如何应用几何变换,例如*移,旋转,
和缩放 - 如何使用仿射和投影变换将有趣的几何效果应用于照片
安装 OpenCV-Python
在本节中,我们说明如何在多个*台上使用 Python 2.7 安装 OpenCV3.X。 如果需要,OpenCV 3.X 还支持使用 Python 3.X,它将与本书中的示例完全兼容。 建议使用 Linux,因为本书中的示例已在该 OS 上进行了测试。
Windows
为了启动并运行 OpenCV-Python,我们需要执行以下步骤:
- 安装 Python:确保您的计算机上安装了 Python2.7.x。 如果没有它,则可以从以下位置进行安装。
- 安装 NumPy:NumPy 是使用 Python 进行数值计算的出色包。 它非常强大,并具有多种功能。 OpenCV-Python 与 NumPy 配合良好,在本书中我们将大量使用此包。 您可以从以下位置安装最新版本。
我们需要将所有这些包安装在它们的默认位置。 安装 Python 和 NumPy 之后,我们需要确保它们能正常工作。 打开 Python shell 并输入以下内容:
>>> import numpy
如果安装顺利,则不会出现任何错误。 确认后,您可以继续从以下位置下载最新的 OpenCV 版本。
下载完成后,双击以安装它。 我们需要进行一些更改,如下所示:
- 导航到
opencv/build/python/2.7/。 - 您将看到一个名为
cv2.pyd的文件。 将此文件复制到C:/Python27/lib/site-packages。
你们都准备好了! 让我们确保 OpenCV 正常运行。 打开 Python shell 并输入以下内容:
>>> import cv2
如果您没有看到任何错误,那就很好了! 现在您可以使用 OpenCV-Python 了。
MacOSX
要安装 OpenCV-Python,我们将使用 Homebrew。 Homebrew 是 MacOSX 的出色包管理器,当您在 MacOSX 上安装各种库和工具时,它会派上用场。如果没有 Homebrew,则可以通过在终端中运行以下命令来安装它:
$ ruby -e "$(curl -fsSL
https://raw.githubusercontent.com/Homebrew/install/master/install)"
即使 OS X 带有内置的 Python,我们也需要使用 Homebrew 安装 Python 以使我们的生活更轻松。 这个版本称为 Brewed Python。 安装 Homebrew 之后,下一步就是安装酿造的 Python。 打开终端,然后键入以下内容:
$ brew install python
这也将自动安装。 PIP 是一个包管理工具,用于在 Python 中安装包,我们将使用它来安装其他包。 让我们确保酿造的 Python 正常工作。 转到终端并输入以下内容:
$ which python
您应该会在终端上看到/usr/local/bin/python。 这意味着我们正在使用酿造的 Python,而不是内置的系统 Python。 现在,我们已经安装了酝酿的 Python,我们可以继续添加存储库homebrew/science,它是 OpenCV 所在的位置。 打开终端并运行以下命令:
$ brew tap homebrew/science
确保已安装 NumPy 包。 如果没有,请在终端中运行以下命令:
$ pip install numpy
现在,我们准备安装 OpenCV。 继续并从终端运行以下命令:
$ brew install opencv --with-tbb --with-opengl
现在已在您的计算机上安装了 OpenCV,您可以在/usr/local/Cellar/opencv/3.1.0/上找到它。 您暂时无法使用它。 我们需要告诉 Python 在哪里可以找到我们的 OpenCV 包。 让我们通过符号链接 OpenCV 文件来做到这一点。 从终端运行以下命令(请仔细检查您实际上使用的是正确版本,因为它们可能略有不同):
$ cd /Library/Python/2.7/site-packages/
$ ln -s /usr/local/Cellar/opencv/3.1.0/lib/python2.7/site-packages/cv.py
cv.py
$ ln -s /usr/local/Cellar/opencv/3.1.0/lib/python2.7/site-packages/cv2.so
cv2.so
你们都准备好了! 让我们看看它是否正确安装。 打开 Python shell 并输入以下内容:
> import cv2
如果安装顺利,您将看不到任何错误消息。 现在您可以在 Python 中使用 OpenCV 了。
如果要在虚拟环境中使用 OpenCV,可以遵循“虚拟环境”部分中的说明,对 MacOSX 的每个命令进行少量更改。
Linux(Ubuntu)
首先,我们需要安装操作系统要求:
[compiler] $ sudo apt-get install build-essential
[required] $ sudo apt-get install cmake git libgtk2.0-dev pkg-config
libavcodec-dev libavformat-dev libswscale-dev git
libgstreamer0.10-dev libv4l-dev
[optional] $ sudo apt-get install python-dev python-numpy libtbb2
libtbb-dev libjpeg-dev libpng-dev libtiff-dev libjasper-dev
libdc1394-22-dev
安装完操作系统要求后,我们需要下载并编译最新版本的 OpenCV 以及几个受支持的标志,以使我们能够实现以下代码示例。 在这里,我们将安装版本 3.3.0:
$ mkdir ~/opencv
$ git clone -b 3.3.0 https://github.com/opencv/opencv.git opencv
$ cd opencv
$ git clone https://github.com/opencv/opencv_contrib.git opencv_contrib
$ mkdir release
$ cd release
$ cmake -D CMAKE_BUILD_TYPE=RELEASE -D CMAKE_INSTALL_PREFIX=/usr/local -D INSTALL_PYTHON_EXAMPLES=ON -D INSTALL_C_EXAMPLES=OFF -D OPENCV_EXTRA_MODULES_PATH=~/opencv/opencv_contrib/modules -D BUILD_PYTHON_SUPPORT=ON -D WITH_XINE=ON -D WITH_OPENGL=ON -D WITH_TBB=ON -D WITH_EIGEN=ON -D BUILD_EXAMPLES=ON -D BUILD_NEW_PYTHON_SUPPORT=ON -D WITH_V4L=ON -D BUILD_EXAMPLES=ON ../
$ make -j4 ; echo 'Running in 4 jobs'
$ sudo make install
如果您使用的是 Python 3,则将-D和标志放在一起,如以下命令所示:
cmake -DCMAKE_BUILD_TYPE=RELEASE....
虚拟环境
如果您使用虚拟环境使测试环境与操作系统的其余部分完全分开,则可以按照以下教程安装名为 virtualenvwrapper 的工具。
要使 OpenCV 在此 Virtualenv 上运行,我们需要安装 NumPy 包:
$(virtual_env) pip install numpy
按照前面的所有步骤,只需在cmake的编译中添加以下三个标志(请注意,正在重新定义标志CMAKE_INSTALL_PREFIX):
$(<env_name>) > cmake ...
-D CMAKE_INSTALL_PREFIX=~/.virtualenvs/<env_name> \
-D PYTHON_EXECUTABLE=~/.virtualenvs/<env_name>/bin/python
-D PYTHON_PACKAGES_PATH=~/.virtualenvs/<env_name>/lib/python<version>/site-packages ...
确保安装正确。 打开 Python shell 并输入以下内容:
> import cv2
如果没有看到任何错误,那就很好了。
故障排除
如果找不到cv2库,请标识该库的编译位置。 它应该位于/usr/local/lib/python2.7/site-packages/cv2.so处。 如果是这种情况,请确保您的 Python 版本与已存储的一个包匹配,否则只需将其移至 Python 的site-packages文件夹中,包括 Virtualenv 的文件夹即可。
在执行cmake命令期间,尝试加入-DMAKE ...和其余-D行。 此外,如果在编译过程中执行失败,则操作系统初始要求中可能缺少某些库。 确保已全部安装。
您可以在以下网站上找到有关如何在 Linux 上安装最新版本的 OpenCV 的官方教程。
如果您尝试使用 Python 3 进行编译,并且未安装cv2.so,请确保已安装操作系统依赖项 Python 3 和 NumPy。
OpenCV 文档
OpenCV 的官方文档位于这个页面。 共有三个文档类别:Doxygen,Sphinx 和 Javadoc。
为了更好地理解如何使用本书中使用的每个函数,我们建议您打开其中一个文档页面,并研究示例中使用的每种 OpenCV 库方法的不同用法。 作为建议,Doxygen 文档提供了有关 OpenCV 使用的更准确和扩展的信息。
读取,显示和保存图像
让我们看看如何在 OpenCV-Python 中加载图像。 创建一个名为first_program.py的文件,然后在您喜欢的代码编辑器中将其打开。 在当前文件夹中创建一个名为images的文件夹,并确保该文件夹中有一个名为input.jpg的图像。
完成后,将以下行添加到该 Python 文件中:
import cv2
img = cv2.imread(img/input.jpg')
cv2.imshow('Input image', img)
cv2.waitKey()
如果运行上述程序,则会在新窗口中显示图像。
刚刚发生了什么?
让我们逐行地理解前面的代码。 在第一行中,我们将导入 OpenCV 库。 对于代码中将要使用的所有函数,我们都需要它。 在第二行中,我们正在读取图像并将其存储在变量中。 OpenCV 使用 NumPy 数据结构存储图像。 您可以通过http://www.numpy.org了解有关 NumPy 的更多信息。
因此,如果打开 Python shell 并键入以下内容,您将在终端上看到打印的数据类型:
> import cv2
> img = cv2.imread(img/input.jpg')
> type(img)
<type 'numpy.ndarray'>
在下一行中,我们在新窗口中显示图像。 cv2.imshow中的第一个参数是窗口的名称。 第二个参数是您要显示的图像。
您一定想知道为什么我们在这里有最后一行。 函数cv2.waitKey()在 OpenCV 中用于键盘绑定。 它以数字作为参数,该数字表示时间(以毫秒为单位)。 基本上,我们使用此函数等待指定的持续时间,直到遇到键盘事件为止。 该程序此时停止,并等待您按任意键继续。 如果我们不传递任何参数,或者我们将其作为参数传递,则此函数将无限期地等待键盘事件。
最后一条语句cv2.waitKey(n)执行之前步骤中加载的图像的渲染。 它需要一个数字来表示渲染时间(以毫秒为单位)。 基本上,我们使用此函数等待指定的时间,直到遇到键盘事件。 该程序此时停止,并等待您按任意键继续。 如果我们不传递任何参数,或者如果传递 0 作为参数,则此函数将无限期地等待键盘事件。
加载和保存图像
OpenCV 提供了多种加载图像的方法。 假设我们要以灰度模式加载彩色图像,可以使用以下代码来实现:
import cv2
gray_img = cv2.imread('images/input.jpg', cv2.IMREAD_GRAYSCALE)
cv2.imshow('Grayscale', gray_img)
cv2.waitKey()
在这里,我们将 ImreadFlag 用作cv2.IMREAD_GRAYSCALE,并以灰度模式加载图像,尽管您可以在官方文档中找到更多读取模式。
您可以在新窗口中看到显示的图像。 这是输入图像:

以下是相应的灰度图像:

我们也可以将该图像另存为文件:
cv2.imwrite('images/output.jpg', gray_img)
这会将灰度图像保存为名为output.jpg的输出文件。 确保您对在 OpenCV 中阅读,显示和保存图像感到满意,因为在本书学习过程中我们将做很多事情。
更改图像格式
我们也可以将此图像另存为文件,并将原始图像格式更改为 PNG:
import cv2
img = cv2.imread('images/input.jpg')
cv2.imwrite('images/output.png', img, [cv2.IMWRITE_PNG_COMPRESSION])
imwrite()方法会将灰度图像保存为名为output.png的输出文件。 这是在IMWRITE标志和cv2.IMWRITE_PNG_COMPRESSION的帮助下使用 PNG 压缩完成的。 IMWRITE标志允许输出图像更改格式,甚至图像质量。
图像色彩空间
在计算机视觉和图像处理中,色彩空间是指组织色彩的特定方式。 颜色空间实际上是颜色模型和映射函数两件事的组合。 我们需要颜色模型的原因是因为它有助于我们使用元组表示像素值。 映射函数将颜色模型映射到可以表示的所有可能颜色的集合。
有许多有用的不同颜色空间。 一些较流行的颜色空间是 RGB,YUV,HSV,Lab 等。 不同的色彩空间提供不同的优势。 我们只需要选择适合给定问题的色彩空间即可。 我们来看几个色彩空间,看看它们提供了什么信息:
- RGB:可能是最受欢迎的色彩空间。 它代表红色,绿色和蓝色。 在此颜色空间中,每种颜色都表示为红色,绿色和蓝色的加权组合。 因此,每个像素值都表示为三个数字的元组,分别对应于红色,绿色和蓝色。 每个值的范围是 0 到 255。
- YUV:尽管 RGB 在许多方面都有好处,但对于许多现实生活中的应用而言,RGB 往往非常有限。 人们开始考虑将强度信息与颜色信息分开的不同方法。 因此,他们提出了 YUV 颜色空间。 Y 表示亮度或强度,U/V 通道表示颜色信息。 这在许多应用中效果很好,因为人类视觉系统感知到的强度信息与颜色信息大不相同。
- HSV:事实证明,即使 YUV 对于某些应用仍然不够好。 因此人们开始思考人类如何看待色彩,然后他们想到了 HSV 色彩空间。 HSV 代表色相,饱和度和值。 这是一个圆柱系统,其中我们将颜色的三个最主要的属性分开,并使用不同的通道表示它们。 这与人类视觉系统如何理解颜色密切相关。 这使我们在处理图像方面具有很大的灵活性。
转换色彩空间
考虑到所有颜色空间,OpenCV 中提供了大约 190 个转换选项。 如果要查看所有可用标志的列表,请转到 Python shell 并键入以下内容:
import cv2
print([x for x in dir(cv2) if x.startswith('COLOR_')])
您将看到 OpenCV 中可用于从一种颜色空间转换为另一种颜色空间的选项列表。 我们几乎可以将任何颜色空间转换为任何其他颜色空间。 让我们看看如何将彩色图像转换为灰度图像:
import cv2
img = cv2.imread(img/input.jpg', cv2.IMREAD_COLOR)
gray_img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
cv2.imshow('Grayscale image', gray_img)
cv2.waitKey()
刚刚发生了什么?
我们使用cvtColor函数转换色彩空间。 第一个参数是输入图像,第二个参数指定颜色空间转换。
分割图像通道
您可以使用以下标志转换为 YUV:
yuv_img = cv2.cvtColor(img, cv2.COLOR_BGR2YUV)
该图像将类似于以下内容:

这可能看起来像原始图像的降级版本,但事实并非如此。 让我们分离出三个通道:
# Alternative 1
y,u,v = cv2.split(yuv_img)
cv2.imshow('Y channel', y)
cv2.imshow('U channel', u)
cv2.imshow('V channel', v)
cv2.waitKey()
# Alternative 2 (Faster)
cv2.imshow('Y channel', yuv_img[:, :, 0])
cv2.imshow('U channel', yuv_img[:, :, 1])
cv2.imshow('V channel', yuv_img[:, :, 2])
cv2.waitKey()
由于yuv_img是 NumPy(提供维选择运算符),因此我们可以通过切片将其分离出来。 如果看yuv_img.shape,您会看到它是一个 3D 数组。 因此,运行前面的代码后,您将看到三个不同的图像。 以下是 Y 通道:

通道基本上是灰度图像。 接下来是 U 通道:

最后,V 通道:

正如我们在这里看到的,通道与灰度图像相同。 它代表强度值,通道代表颜色信息。
合并图像通道
现在,我们将读取图像,将其分成单独的通道,然后合并它们,以了解如何从不同的组合中获得不同的效果:
img = cv2.imread(img/input.jpg', cv2.IMREAD_COLOR)
g,b,r = cv2.split(img)
gbr_img = cv2.merge((g,b,r))
rbr_img = cv2.merge((r,b,r))
cv2.imshow('Original', img)
cv2.imshow('GRB', gbr_img)
cv2.imshow('RBR', rbr_img)
cv2.waitKey()
在这里,我们可以看到如何重组通道以获得不同的颜色强度:

在此示例中,红色通道使用了两次,因此红色更加强烈:

这应该为您提供有关如何在色彩空间之间进行转换的基本概念。 您可以在更多的色彩空间中玩耍,以查看图像的外观。 在随后的章节中,我们将讨论相关的色彩空间以及它们何时出现。
图像*移
在本节中,我们将讨论图像移位。 假设我们要在参照系内移动图像。 在计算机视觉术语中,这称为*移。 让我们继续前进,看看我们如何做到这一点:
import cv2
import numpy as np
img = cv2.imread('images/input.jpg')
num_rows, num_cols = img.shape[:2]
translation_matrix = np.float32([ [1,0,70], [0,1,110] ])
img_translation = cv2.warpAffine(img, translation_matrix, (num_cols, num_rows), cv2.INTER_LINEAR)
cv2.imshow('Translation', img_translation)
cv2.waitKey()
如果运行前面的代码,您将看到类似以下内容:

刚刚发生了什么?
要了解前面的代码,我们需要了解扭曲的工作原理。 *移基本上意味着我们通过添加/减去x和y坐标来移动图像。 为此,我们需要创建一个转换矩阵,如下所示:

在此,t[x]和t[y]值是x和y*移值; 也就是说,图像将向右移动x个单位,向下移动y个单位。 因此,一旦创建了这样的矩阵,就可以使用函数warpAffine将其应用于图像。 warpAffine中的第三个参数指的是结果图像中的行数和列数。 如下所示,它通过了InterpolationFlags,它定义了插值方法的组合。
由于行数和列数与原始图像相同,因此最终图像将被裁剪。 原因是当我们应用转换矩阵时,输出中没有足够的空间。 为了避免裁剪,我们可以执行以下操作:
img_translation = cv2.warpAffine(img, translation_matrix,
(num_cols + 70, num_rows + 110))
如果将程序中的相应行替换为前一行,则会看到下图:

假设您要将图像移到更大图像帧的中间; 我们可以通过执行以下操作来做到这一点:
import cv2
import numpy as np
img = cv2.imread('images/input.jpg')
num_rows, num_cols = img.shape[:2]
translation_matrix = np.float32([ [1,0,70], [0,1,110] ])
img_translation = cv2.warpAffine(img, translation_matrix, (num_cols + 70, num_rows + 110))
translation_matrix = np.float32([ [1,0,-30], [0,1,-50] ])
img_translation = cv2.warpAffine(img_translation, translation_matrix, (num_cols + 70 + 30, num_rows + 110 + 50))
cv2.imshow('Translation', img_translation)
cv2.waitKey()
如果运行前面的代码,您将看到类似以下的图像:

此外,还有另外两个参数borderMode和borderValue,这些参数使您可以使用像素外推法填充*移的空白边界:
import cv2
import numpy as np
img = cv2.imread(img/input.jpg')
num_rows, num_cols = img.shape[:2]
translation_matrix = np.float32([ [1,0,70], [0,1,110] ])
img_translation = cv2.warpAffine(img, translation_matrix, (num_cols, num_rows), cv2.INTER_LINEAR, cv2.BORDER_WRAP, 1)
cv2.imshow('Translation', img_translation)
cv2.waitKey()

图像旋转
在本节中,我们将看到如何将给定图像旋转一定角度。 我们可以使用以下代码来做到这一点:
import cv2
import numpy as np
img = cv2.imread('images/input.jpg')num_rows, num_cols = img.shape[:2]
rotation_matrix = cv2.getRotationMatrix2D((num_cols/2, num_rows/2), 30, 0.7)
img_rotation = cv2.warpAffine(img, rotation_matrix, (num_cols, num_rows))
cv2.imshow('Rotation', img_rotation)
cv2.waitKey()
如果运行前面的代码,您将看到如下图像:

刚刚发生了什么?
使用getRotationMatrix2D,我们可以将图像围绕其旋转的中心点指定为第一个参数,然后指定旋转角度(以度为单位),最后指定图像的缩放比例。 我们使用 0.7 将图像缩小 30%,使其适合框架。
为了理解这一点,让我们看看我们如何数学地处理旋转。 旋转也是一种变换形式,我们可以使用以下变换矩阵来实现:

在此,θ是逆时针方向的旋转角度。 OpenCV 通过getRotationMatrix2D函数可以更好地控制此矩阵的创建。 我们可以指定围绕图像旋转的点,旋转角度(以度为单位)和图像的缩放因子。 一旦有了转换矩阵,就可以使用warpAffine函数将此矩阵应用于任何图像。
从上图中可以看到,图像内容超出范围并被裁剪。 为了防止这种情况,我们需要在输出图像中提供足够的空间。
让我们继续使用前面讨论的*移函数:
import cv2
import numpy as np
img = cv2.imread('images/input.jpg')
num_rows, num_cols = img.shape[:2]
translation_matrix = np.float32([ [1,0,int(0.5*num_cols)], [0,1,int(0.5*num_rows)] ])
rotation_matrix = cv2.getRotationMatrix2D((num_cols, num_rows), 30, 1)
img_translation = cv2.warpAffine(img, translation_matrix, (2*num_cols, 2*num_rows))
img_rotation = cv2.warpAffine(img_translation, rotation_matrix, (num_cols*2, num_rows*2))
cv2.imshow('Rotation', img_rotation)
cv2.waitKey()
如果运行前面的代码,我们将看到类似以下内容:

图像缩放
在本节中,我们将讨论调整图像的大小。 这是计算机视觉中最常见的操作之一。 我们可以使用缩放因子来调整图像的大小,也可以将其调整为特定的大小。 让我们看看如何做到这一点:
import cv2
img = cv2.imread('images/input.jpg')
img_scaled = cv2.resize(img,None,fx=1.2, fy=1.2, interpolation = cv2.INTER_LINEAR)
cv2.imshow('Scaling - Linear Interpolation', img_scaled)
img_scaled = cv2.resize(img,None,fx=1.2, fy=1.2, interpolation = cv2.INTER_CUBIC)
cv2.imshow('Scaling - Cubic Interpolation', img_scaled)
img_scaled = cv2.resize(img,(450, 400), interpolation = cv2.INTER_AREA)
cv2.imshow('Scaling - Skewed Size', img_scaled)
cv2.waitKey()
刚刚发生了什么?
每当我们调整图像大小时,都有多种方法可以填充像素值。 放大图像时,需要在像素位置之间填充像素值。 在缩小图像时,我们需要获得最佳的代表值。 当我们按非整数值缩放时,我们需要适当地插值,以便保持图像的质量。 有多种插值方法。 如果要放大图像,则最好使用线性或三次插值。 如果要缩小图像,则最好使用基于区域的插值。 三次插值在计算上更复杂,因此比线性插值要慢。 但是,最终图像的质量会更高。
OpenCV 提供了一个称为调整大小的函数来实现图像缩放。 如果未指定大小(通过使用None),则它将期望x和y缩放因子。 在我们的示例中,图像将放大 1.2 倍。 如果使用三次插值进行相同的放大,则可以看到质量有所提高,如下图所示。 以下屏幕截图显示了线性插值的形式:

这是对应的三次插值:

如果我们想将其调整为特定大小,可以使用最后一个调整大小实例中显示的格式。 我们基本上可以倾斜图像并将其调整为所需的大小。 输出将类似于以下内容:

仿射变换
在本节中,我们将讨论 2D 图像的各种广义几何变换。 在最后两节中,我们已经大量使用了warpAffine函数,现在是时候了解下面发生的事情了。
在讨论仿射变换之前,让我们了解什么是欧几里得变换。 欧几里德变换是保留长度和角度量度的一种几何变换。 如果我们采用几何形状并对其进行欧几里德变换,则该形状将保持不变。 它看起来可能旋转,移位等等,但是基本结构不会改变。 因此,从技术上讲,线将保留为线,*面将保留为*面,正方形将保留为正方形,圆形将保留为圆形。
回到仿射变换,可以说它们是欧几里得变换的概括。 在仿射变换的范围内,线将保留为线,但正方形可能会变成矩形或*行四边形。 基本上,仿射变换不会保留长度和角度。
为了构建通用仿射变换矩阵,我们需要定义控制点。 一旦有了这些控制点,就需要确定我们希望将它们映射到何处。 在这种特殊情况下,我们需要的是源图像中的三个点,以及输出图像中的三个点。 让我们看看如何将图像转换为类似*行四边形的图像:
import cv2
import numpy as np
img = cv2.imread('images/input.jpg')
rows, cols = img.shape[:2]
src_points = np.float32([[0,0], [cols-1,0], [0,rows-1]])
dst_points = np.float32([[0,0], [int(0.6*(cols-1)),0], [int(0.4*(cols-1)),rows-1]])
affine_matrix = cv2.getAffineTransform(src_points, dst_points)
img_output = cv2.warpAffine(img, affine_matrix, (cols,rows))
cv2.imshow('Input', img)
cv2.imshow('Output', img_output)
cv2.waitKey()
刚刚发生了什么?
如前所述,我们正在定义控制点。 我们只需要三个点就可以得到仿射变换矩阵。 我们希望将src_points中的三个点映射到dst_points中的相应点。 我们正在映射点,如下图所示:

为了获得转换矩阵,我们在 OpenCV 中有一个函数。 有了仿射变换矩阵后,就可以使用该函数将该矩阵应用于输入图像。
以下是输入图像:

如果运行前面的代码,则输出将类似于以下内容:

我们还可以获取输入图像的镜像。 我们只需要通过以下方式更改控制点:
src_points = np.float32([[0,0], [cols-1,0], [0,rows-1]])
dst_points = np.float32([[cols-1,0], [0,0], [cols-1,rows-1]])
在这里,映射看起来像这样:

如果用这两行替换仿射变换代码中的相应行,则会得到以下结果:

投影变换
仿射转换很不错,但是有一定的限制。 另一方面,投射性的转换给了我们更多的自由。 为了理解投影变换,我们需要了解投影几何如何工作。 我们基本上描述了当视角改变时图像会发生什么。 例如,如果您正站在一张纸上画有正方形的前面,它将看起来像一个正方形。
现在,如果您开始倾斜那张纸,则正方形将开始越来越像梯形。 投影变换使我们能够以一种很好的数学方式捕获这种动态。 这些变换既不保留大小也不保留角度,但确实保留了入射和交叉比率。
您可以在这个页面和这个页面上了解更多有关发生率和交叉比率的信息。
现在我们知道了投影变换是什么,让我们看看是否可以在此处提取更多信息。 可以说,给定*面上的任何两个图像都是由单应性相关的。 只要它们在同一*面上,我们就可以将任何东西转换成其他东西。 这具有许多实际应用,例如增强现实,图像校正,图像配准或两个图像之间的相机运动计算。 一旦从估计的单应性矩阵中提取了摄像机的旋转和*移,此信息即可用于导航,或将 3D 对象的模型插入图像或视频。 这样,它们将以正确的透视图进行渲染,看起来它们就像是原始场景的一部分。
让我们继续前进,看看如何做到这一点:
import cv2
import numpy as np
img = cv2.imread('images/input.jpg')
rows, cols = img.shape[:2]
src_points = np.float32([[0,0], [cols-1,0], [0,rows-1], [cols-1,rows-1]])
dst_points = np.float32([[0,0], [cols-1,0], [int(0.33*cols),rows-1], [int(0.66*cols),rows-1]])
projective_matrix = cv2.getPerspectiveTransform(src_points, dst_points)
img_output = cv2.warpPerspective(img, projective_matrix, (cols,rows))
cv2.imshow('Input', img)
cv2.imshow('Output', img_output)
cv2.waitKey()
如果运行前面的代码,您将看到有趣的输出,例如以下屏幕截图:

刚刚发生了什么?
我们可以在源图像中选择四个控制点,并将它们映射到目标图像。 转换后,*行线将不会保持*行线。 我们使用一个名为getPerspectiveTransform的函数来获取变换矩阵。
让我们使用投影变换应用几个有趣的效果,然后看它们的外观。 我们需要做的就是更改控制点以获得不同的效果。
这是一个例子:

控制点如下:
src_points = np.float32([[0,0], [0,rows-1], [cols/2,0],[cols/2,rows-1]])
dst_points = np.float32([[0,100], [0,rows-101], [cols/2,0],[cols/2,rows-1]])
作为练习,您应该在*面上映射前面的点,并查看如何映射这些点(就像我们前面讨论仿射变换时所做的一样)。 您将对映射系统有一个很好的了解,并且可以创建自己的控制点。 如果要在y轴上获得相同的效果,可以应用以前的变换。
图像变形
让我们对图像有更多的乐趣,看看还能实现什么。 投影变换非常灵活,但是它们仍然对我们如何变换点施加了一些限制。 如果我们想做完全随机的事情怎么办? 我们需要更多的控制权,对不对? 碰巧我们也可以做到这一点。 我们只需要创建自己的映射即可,这并不困难。 以下是通过图像变形可以实现的一些效果:

这是创建这些效果的代码:
import cv2
import numpy as np
import math
img = cv2.imread('images/input.jpg', cv2.IMREAD_GRAYSCALE)
rows, cols = img.shape
#####################
# Vertical wave
img_output = np.zeros(img.shape, dtype=img.dtype)
for i in range(rows):
for j in range(cols):
offset_x = int(25.0 * math.sin(2 * 3.14 * i / 180))
offset_y = 0
if j+offset_x < rows:
img_output[i,j] = img[i,(j+offset_x)%cols]
else:
img_output[i,j] = 0
cv2.imshow('Input', img)
cv2.imshow('Vertical wave', img_output)
#####################
# Horizontal wave
img_output = np.zeros(img.shape, dtype=img.dtype)
for i in range(rows):
for j in range(cols):
offset_x = 0
offset_y = int(16.0 * math.sin(2 * 3.14 * j / 150))
if i+offset_y < rows:
img_output[i,j] = img[(i+offset_y)%rows,j]
else:
img_output[i,j] = 0
cv2.imshow('Horizontal wave', img_output)
#####################
# Both horizontal and vertical
img_output = np.zeros(img.shape, dtype=img.dtype)
for i in range(rows):
for j in range(cols):
offset_x = int(20.0 * math.sin(2 * 3.14 * i / 150))
offset_y = int(20.0 * math.cos(2 * 3.14 * j / 150))
if i+offset_y < rows and j+offset_x < cols:
img_output[i,j] = img[(i+offset_y)%rows,(j+offset_x)%cols]
else:
img_output[i,j] = 0
cv2.imshow('Multidirectional wave', img_output)
#####################
# Concave effect
img_output = np.zeros(img.shape, dtype=img.dtype)
for i in range(rows):
for j in range(cols):
offset_x = int(128.0 * math.sin(2 * 3.14 * i / (2*cols)))
offset_y = 0
if j+offset_x < cols:
img_output[i,j] = img[i,(j+offset_x)%cols]
else:
img_output[i,j] = 0
cv2.imshow('Concave', img_output)
cv2.waitKey()
总结
在本章中,我们学习了如何在各种*台上安装 OpenCV-Python。 我们讨论了如何读取,显示和保存图像。 我们讨论了各种颜色空间的重要性,以及如何将其转换为多个颜色空间,拆分和合并它们。 我们学习了如何将几何变换应用于图像,并了解了如何使用这些变换来实现炫酷的几何效果。 我们讨论了转换矩阵的基本表示形式,以及如何根据需要制定不同类型的转换。 我们学习了如何根据所需的几何变换选择控制点。 我们讨论了投影变换,并学习了如何使用图像变形来实现任何给定的几何效果。
在下一章中,我们将讨论边缘检测和图像过滤。 我们可以使用图像过滤器来应用许多视觉效果,并且底层结构为我们提供了许多以创造性方式操作图像的自由。
二、检测边缘并应用图像过滤器
在本章中,我们将了解如何将酷炫的视觉效果应用于图像。 我们将学习如何使用基本的图像处理运算符,讨论边缘检测,以及如何使用图像过滤器将各种效果应用于照片。
在本章结束时,您将了解:
- 什么是 2D 卷积以及如何使用
- 如何模糊图像
- 如何检测图像边缘
- 如何将运动模糊应用于图像
- 如何锐化和浮雕图像
- 如何腐蚀和扩大图像
- 如何创建晕影过滤器
- 如何增强图像对比度
2D 卷积
卷积是图像处理中的基本操作。 我们基本上将数学运算符应用于每个像素,并以某种方式更改其值。 为了应用该数学运算符,我们使用另一个称为核的矩阵。 核的大小通常比输入图像小得多。 对于图像中的每个像素,我们将核放在顶部,以使核的中心与所考虑的像素重合。 然后,我们将核矩阵中的每个值与图像中的相应值相乘,然后将其求和。 这是将应用于输出图像中该位置的新值。
在这里,核称为图像过滤器,而将此核应用于给定图像的过程称为图像过滤。 将核应用于图像后获得的输出称为滤波图像。 根据核中的值,它执行不同的功能,例如模糊,检测边缘等。 下图应帮助您可视化图像过滤操作:

让我们从最简单的情况开始,即身份核。 这个核并没有真正改变输入图像。 如果我们考虑一个3x3身份核,它看起来类似于以下内容:

模糊化
模糊是指对邻域内的像素值求*均。 这也称为低通过滤器。 低通过滤器是允许低频并阻止高频的过滤器。 现在,我们想到的下一个问题是:频率在图像中意味着什么? 嗯,在这种情况下,频率是指像素值的变化率。 因此,可以说尖锐的边缘将是高频内容,因为像素值在该区域中快速变化。 按照这种逻辑,*原区域将是低频内容。 按照这个定义,低通过滤器将尝试*滑边缘。
构造低通过滤器的一种简单方法是均匀地*均像素附*的值。 我们可以根据要*滑图像的程度来选择核的大小,并且相应地会有不同的效果。 如果您选择更大的尺寸,那么您将在更大的区域进行*均。 这趋于增加*滑效果。 让我们看一下3x3低通过滤器核的样子:

我们将矩阵除以 9,因为我们希望这些值的总和为 1。 这称为归一化,这一点很重要,因为我们不想人为地增加该像素位置的强度值。 因此,您应该在将核应用于图像之前对其进行规范化。 规范化是一个非常重要的概念,它在多种情况下都可以使用,因此您应该在线阅读一些教程以很好地了解它。
这是将低通过滤器应用于图像的代码:
import cv2
import numpy as np
img = cv2.imread('images/input.jpg')
rows, cols = img.shape[:2]
kernel_identity = np.array([[0,0,0], [0,1,0], [0,0,0]])
kernel_3x3 = np.ones((3,3), np.float32) / 9.0 # Divide by 9 to normalize the kernel
kernel_5x5 = np.ones((5,5), np.float32) / 25.0 # Divide by 25 to normalize the kernel
cv2.imshow('Original', img)
# value -1 is to maintain source image depth
output = cv2.filter2D(img, -1, kernel_identity) cv2.imshow('Identity filter', output)
output = cv2.filter2D(img, -1, kernel_3x3)
cv2.imshow('3x3 filter', output)
output = cv2.filter2D(img, -1, kernel_5x5)
cv2.imshow('5x5 filter', output)
cv2.waitKey(0)
如果运行前面的代码,您将看到类似以下内容:

核大小与模糊
在前面的代码中,我们在代码中生成了kernel_identity,kernel_3x3和kernel_5x5不同的核。 我们使用filter2D函数将这些核应用于输入图像。 如果仔细查看图像,您会发现随着我们增加核大小,它们会变得越来越模糊。 其原因是因为当我们增加核大小时,我们在更大的区域进行*均。 这往往具有较大的模糊效果。
另一种执行此操作的方法是使用 OpenCV 函数blur。 如果您不想自己生成核,则可以直接使用此函数。 我们可以使用以下代码行来调用它:
output = cv2.blur(img, (3,3))
这会将3x3核应用于输入,并直接为您提供输出。
运动模糊
当我们应用运动模糊效果时,看起来就像是您沿特定方向移动时捕获的图片。 例如,您可以使图像看起来像是从行驶中的汽车上捕获的。
输入和输出图像将类似于以下图像:

以下是实现这种运动模糊效果的代码:
import cv2
import numpy as np
img = cv2.imread('images/input.jpg')
cv2.imshow('Original', img)
size = 15
# generating the kernel
kernel_motion_blur = np.zeros((size, size))
kernel_motion_blur[int((size-1)/2), :] = np.ones(size)
kernel_motion_blur = kernel_motion_blur / size
# applying the kernel to the input image
output = cv2.filter2D(img, -1, kernel_motion_blur)
cv2.imshow('Motion Blur', output)
cv2.waitKey(0)
底层原理
我们正在照常读取图像。 然后,我们正在构建运动blur核。 运动模糊核会在特定方向上*均像素值。 就像定向低通过滤器。 3x3水*运动模糊核看起来像这样:

这将使图像在水*方向上模糊。 您可以选择任何方向,它将相应地起作用。 模糊的数量将取决于核的大小。 因此,如果要使图像模糊,只需为核选择更大的尺寸即可。 为了看到全部效果,我们在前面的代码中采用了15x15核。 然后,我们使用filter2D将此核应用于输入图像,以获得运动模糊的输出。
锐化
应用锐化过滤器将锐化图像中的边缘。 当我们要增强不够清晰的图像边缘时,此过滤器非常有用。 以下是一些图像,可让您大致了解图像锐化过程的外观:

如上图所示,锐化程度取决于我们使用的核类型。 我们在这里可以自由定制核,每个核都会给您一种不同的锐化方法。 要像在上一张图片的右上角图像中那样锐化图像,我们将使用这样的核:

如果要进行过度锐化,如左下图所示,我们将使用以下核:

但是,这两个核的问题在于输出图像看起来是人为增强的。 如果我们希望图像看起来更自然,可以使用边缘增强过滤器。 基本概念保持不变,但是我们使用*似的高斯核来构建此过滤器。 当我们增强边缘时,它将帮助我们*滑图像,从而使图像看起来更自然。
这是实现上述屏幕快照中所应用效果的代码:
import cv2
import numpy as np
img = cv2.imread('images/input.jpg')
cv2.imshow('Original', img)
# generating the kernels
kernel_sharpen_1 = np.array([[-1,-1,-1], [-1,9,-1], [-1,-1,-1]])
kernel_sharpen_2 = np.array([[1,1,1], [1,-7,1], [1,1,1]])
kernel_sharpen_3 = np.array([[-1,-1,-1,-1,-1],
[-1,2,2,2,-1],
[-1,2,8,2,-1],
[-1,2,2,2,-1],
[-1,-1,-1,-1,-1]]) / 8.0
# applying different kernels to the input image
output_1 = cv2.filter2D(img, -1, kernel_sharpen_1)
output_2 = cv2.filter2D(img, -1, kernel_sharpen_2)
output_3 = cv2.filter2D(img, -1, kernel_sharpen_3)
cv2.imshow('Sharpening', output_1)
cv2.imshow('Excessive Sharpening', output_2)
cv2.imshow('Edge Enhancement', output_3)
cv2.waitKey(0)
如果您注意到,在前面的代码中,我们没有将前两个核除以归一化因子。 这样做的原因是核内部的值总和为 1,因此我们将矩阵隐式除以 1。
了解模式
您一定已经在图像过滤代码示例中注意到了一种常见模式。 我们构建一个核,然后使用filter2D获得所需的输出。 这也正是此代码示例中发生的事情! 您可以使用核中的值,看看是否可以获得不同的视觉效果。 确保在应用核之前对核进行了标准化,否则图像将显得太亮,因为您是在人为地增加图像中的像素值。
浮雕
浮雕过滤器将拍摄图像并将其转换为浮雕图像。 我们基本上会获取每个像素,然后将其替换为阴影或高光。 假设我们正在处理图像中相对较*坦的区域。 在这里,我们需要用纯灰色代替它,因为那里没有太多信息。 如果在特定区域有很多对比度,我们将根据浮雕的方向将其替换为白色像素(高光)或深色像素(阴影)。
它将是这样的:

让我们看一下代码,看看如何做到这一点:
import cv2
import numpy as np
img_emboss_input = cv2.imread('images/input.jpg')
# generating the kernels
kernel_emboss_1 = np.array([[0,-1,-1],
[1,0,-1],
[1,1,0]])
kernel_emboss_2 = np.array([[-1,-1,0],
[-1,0,1],
[0,1,1]])
kernel_emboss_3 = np.array([[1,0,0],
[0,0,0],
[0,0,-1]])
# converting the image to grayscale
gray_img = cv2.cvtColor(img_emboss_input,cv2.COLOR_BGR2GRAY)
# applying the kernels to the grayscale image and adding the offset to produce the shadow
output_1 = cv2.filter2D(gray_img, -1, kernel_emboss_1) + 128
output_2 = cv2.filter2D(gray_img, -1, kernel_emboss_2) + 128
output_3 = cv2.filter2D(gray_img, -1, kernel_emboss_3) + 128
cv2.imshow('Input', img_emboss_input)
cv2.imshow('Embossing - South West', output_1)
cv2.imshow('Embossing - South East', output_2)
cv2.imshow('Embossing - North West', output_3)
cv2.waitKey(0)
如果运行前面的代码,您将看到输出图像已浮雕。 从前面的核可以看到,我们只是将当前像素值替换为特定方向上相邻像素值的差。 通过将图像中的所有像素值都偏移128来实现浮雕效果。 此操作将高光/阴影效果添加到图片。
边缘检测
边缘检测的过程涉及检测图像中的尖锐边缘,并生成二进制图像作为输出。 通常,我们在黑色背景上绘制白线以指示这些边缘。 我们可以将边缘检测视为高通滤波操作。 高通过滤器允许高频内容通过并阻止低频内容。 如前所述,边缘是高频内容。 在边缘检测中,我们要保留这些边缘并丢弃其他所有内容。 因此,我们应该构建一个等效于高通过滤器的核。
让我们从一个称为Sobel过滤器的简单边缘检测过滤器开始。 由于边缘会同时出现在水*和垂直方向,因此Sobel过滤器由以下两个核组成:

左侧的核检测水*边缘,右侧的核检测垂直边缘。 OpenCV 提供了直接将Sobel过滤器应用于给定图像的函数。 这是使用 Sobel 过滤器检测边缘的代码:
import cv2
import numpy as np
img = cv2.imread('images/input_shapes.png', cv2.IMREAD_GRAYSCALE)
rows, cols = img.shape
# It is used depth of cv2.CV_64F.
sobel_horizontal = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=5)
# Kernel size can be: 1,3,5 or 7.
sobel_vertical = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=5)
cv2.imshow('Original', img)
cv2.imshow('Sobel horizontal', sobel_horizontal)
cv2.imshow('Sobel vertical', sobel_vertical)
cv2.waitKey(0)
对于 8 位输入图像,这将导致截断的导数,因此可以使用深度值cv2.CV_16U代替。 如果边缘定义不明确,可以调整核的值,将其设置为较小可获得较薄的边缘,而对于相反的目的则为较大。
输出将类似于以下内容:

在上图中,中间的图像是水*边缘检测器的输出,而右边的图像是垂直边缘检测器。 正如我们在这里看到的,Sobel过滤器可以检测水*或垂直方向上的边缘,并且不能为我们提供所有边缘的整体视图。 为了克服这个问题,我们可以使用Laplacian过滤器。 使用此过滤器的优点是它在两个方向上都使用了双导数。 您可以使用以下行来调用该函数:
laplacian = cv2.Laplacian(img, cv2.CV_64F)
输出将类似于以下屏幕截图:

即使Laplacian核在这种情况下也能很好地工作,但它并不总是能很好地工作。 如下面的屏幕快照所示,这会在输出中引起很多噪声。 这是Canny边缘检测器派上用场的地方:

正如我们在前面的图像中看到的,Laplacian核会产生一个嘈杂的输出,这并不是完全有用的。 为了克服这个问题,我们使用了Canny边缘检测器。 要使用Canny边缘检测器,我们可以使用以下函数:
canny = cv2.Canny(img, 50, 240)
如我们所见,Canny边缘检测器的质量要好得多。 它使用两个数字作为参数来指示阈值。 第二个参数称为低阈值值,第三个参数称为高阈值值。 如果梯度值超出高阈值,则将其标记为强边缘。 Canny边缘检测器从此点开始跟踪边缘,并继续进行处理,直到梯度值降至低阈值以下。 随着增加这些阈值,较弱的边缘将被忽略。 输出图像将更清晰,更稀疏。 您可以尝试使用阈值,并查看增加或减小阈值会发生什么。 总体表述很深。 您可以通过以下网址了解更多信息。
侵蚀和膨胀
侵蚀和膨胀是形态图像处理操作。 形态图像处理基本上涉及修改图像中的几何结构。 这些操作主要是为二进制图像定义的,但是我们也可以在灰度图像上使用它们。 侵蚀基本上剥夺了结构中最外面的像素层,而膨胀使结构增加了额外的像素层。
让我们看看这些操作是什么样的:

以下是实现此目的的代码:
import cv2
import numpy as np
img = cv2.imread('images/input.jpg', 0)
kernel = np.ones((5,5), np.uint8)
img_erosion = cv2.erode(img, kernel, iterations=1)
img_dilation = cv2.dilate(img, kernel, iterations=1)
cv2.imshow('Input', img)
cv2.imshow('Erosion', img_erosion)
cv2.imshow('Dilation', img_dilation)
cv2.waitKey(0)
事后思考
OpenCV 提供直接腐蚀和扩大图像的函数。 它们分别称为腐蚀和膨胀。 值得注意的是这两个函数中的第三个参数。 迭代次数将确定您要腐蚀/扩大给定图像的数量。 它基本上将操作顺序地应用于所得图像。 您可以拍摄样本图像,并使用此参数来查看结果。
创建晕影过滤器
使用我们拥有的所有信息,让我们看看是否可以创建一个漂亮的小插图过滤器。 输出将类似于以下内容:

这是实现此效果的代码:
import cv2
import numpy as np
img = cv2.imread('images/input.jpg')
rows, cols = img.shape[:2]
# generating vignette mask using Gaussian kernels
kernel_x = cv2.getGaussianKernel(cols,200)
kernel_y = cv2.getGaussianKernel(rows,200)
kernel = kernel_y * kernel_x.T
mask = 255 * kernel / np.linalg.norm(kernel)
output = np.copy(img)
# applying the mask to each channel in the input image
for i in range(3):
output[:,:,i] = output[:,:,i] * mask
cv2.imshow('Original', img)
cv2.imshow('Vignette', output)
cv2.waitKey(0)
到底发生了什么?
晕影过滤器基本上将亮度聚焦在图像的特定部分上,而其他部分则显得褪色。 为了实现这一点,我们需要使用高斯核过滤掉图像中的每个通道。 OpenCV 提供了执行此操作的函数,称为getGaussianKernel。 我们需要构建一个 2D 核,其大小与图像的大小匹配。 函数的第二个参数getGaussianKernel很有趣。 它是高斯的标准差,它控制明亮的中心区域的半径。 您可以试用此参数,并查看它如何影响输出。
构建 2D 核后,需要通过标准化该核并按比例放大来构建遮罩,如以下行所示:
mask = 255 * kernel / np.linalg.norm(kernel)
这是重要的一步,因为如果您不按比例放大图像,图像将看起来很黑。 发生这种情况是因为在将遮罩叠加在输入图像上之后,所有像素值都将接*于零。 此后,我们遍历所有颜色通道并将遮罩应用于每个通道。
我们如何转移焦点?
现在,我们知道如何创建聚焦于图像中心的小插图过滤器。 假设我们要实现相同的晕影效果,但我们要关注图像中的其他区域,如下图所示:

我们需要做的是建立一个更大的高斯核,并确保该峰与兴趣区域重合。 以下是实现此目的的代码:
import cv2
import numpy as np
img = cv2.imread('images/input.jpg')
rows, cols = img.shape[:2]
# generating vignette mask using Gaussian kernels
kernel_x = cv2.getGaussianKernel(int(1.5*cols),200)
kernel_y = cv2.getGaussianKernel(int(1.5*rows),200)
kernel = kernel_y * kernel_x.T
mask = 255 * kernel / np.linalg.norm(kernel)
mask = mask[int(0.5*rows):, int(0.5*cols):]
output = np.copy(img)
# applying the mask to each channel in the input image
for i in range(3):
output[:,:,i] = output[:,:,i] * mask
cv2.imshow('Input', img)
cv2.imshow('Vignette with shifted focus', output)
cv2.waitKey(0)
增强图像的对比度
每当我们在弱光条件下拍摄图像时,图像就会变暗。 当您在晚上或昏暗的房间中拍摄图像时,通常会发生这种情况。 您一定已经多次看到这种情况! 发生这种情况的原因是,当我们在这种条件下捕获图像时,像素值趋于集中在零附*。 发生这种情况时,人眼无法清晰看到图像中的许多细节。 人眼喜欢对比度,因此我们需要调整对比度以使图像看起来既美观又令人愉悦。 许多相机和照片应用已经隐式地执行了此操作。 我们使用称为直方图均衡的过程来实现这一目标。
举个例子,这是对比度增强前后的样子:

如我们在这里看到的,左侧的输入图像确实很暗。 为了解决这个问题,我们需要调整像素值,以使它们分布在整个值范围内,即介于 0-255 之间。
以下是用于调整像素值的代码:
import cv2
import numpy as np
img = cv2.imread('images/input.jpg', 0)
# equalize the histogram of the input image
histeq = cv2.equalizeHist(img)
cv2.imshow('Input', img)
cv2.imshow('Histogram equalized', histeq)
cv2.waitKey(0)
直方图均衡化适用于灰度图像。 OpenCV 提供了equalizeHist函数来实现此效果。 就像我们在这里看到的那样,代码非常简单,我们读取图像并均衡其直方图以归一化亮度并增加图像的对比度。
我们如何处理彩色图像?
既然我们知道如何均衡灰度图像的直方图,您可能想知道如何处理彩色图像。 直方图均衡化是一个非线性过程。 因此,我们不能仅将 RGB 图像中的三个通道分离出来,分别对直方图进行均衡,然后再将它们组合以形成输出图像。 直方图均衡化的概念仅适用于图像中的强度值。 因此,我们必须确保在进行此操作时不要修改颜色信息。
为了处理彩色图像的直方图均衡化,我们需要将其转换为色彩空间,其中强度与色彩信息分开。 YUV 是这种颜色空间的一个很好的例子,因为 YUV 模型根据一个亮度(Y)和两个色度(UV)成分。 一旦将其转换为 YUV,我们只需要均衡 Y 通道并将其与其他两个通道组合即可获得输出图像。
以下是其外观的示例:

这是实现彩色图像直方图均衡的代码:
import cv2
import numpy as np
img = cv2.imread('images/input.jpg')
img_yuv = cv2.cvtColor(img, cv2.COLOR_BGR2YUV)
# equalize the histogram of the Y channel
img_yuv[:,:,0] = cv2.equalizeHist(img_yuv[:,:,0])
# convert the YUV image back to RGB format
img_output = cv2.cvtColor(img_yuv, cv2.COLOR_YUV2BGR)
cv2.imshow('Color input image', img)
cv2.imshow('Histogram equalized', img_output)
cv2.waitKey(0)
总结
在本章中,我们学习了如何使用图像过滤器将酷炫的视觉效果应用于图像。 我们讨论了基本的图像处理运算符,以及如何使用它们来构建各种东西。 我们学习了如何使用各种方法检测边缘。 我们了解了 2D 卷积的重要性以及如何在不同的场景中使用它。 我们讨论了如何使图像*滑,运动模糊,锐化,浮雕,腐蚀和扩大图像。 我们学习了如何创建晕影过滤器,以及如何更改焦点区域。 我们讨论了对比度增强以及如何使用直方图均衡来实现它。
在下一章中,我们将讨论如何对给定图像进行卡通化。
三、卡通化图像
在本章中,我们将学习如何将图像转换为卡通图像。 我们将学习如何在实时视频流中访问网络摄像头并进行键盘/鼠标输入。 我们还将学习一些高级图像过滤器,并了解如何使用它们来对输入视频进行卡通化。
在本章结束时,您将了解:
- 如何访问网络摄像头
- 如何在实时视频流中进行键盘和鼠标输入
- 如何创建一个交互式应用
- 如何使用高级图像过滤器
- 如何将图像卡通化
访问网络摄像头
我们可以使用网络摄像头的实时视频流构建非常有趣的应用。 OpenCV 提供了一个视频捕获对象,该对象可以处理与网络摄像头的打开和关闭有关的所有事情。 我们需要做的就是创建该对象并保持从中读取帧。
以下代码将打开网络摄像机,捕获帧,将它们按比例缩小 2 倍,然后在窗口中显示它们。 您可以按Esc键退出:
import cv2
cap = cv2.VideoCapture(0)
# Check if the webcam is opened correctly
if not cap.isOpened():
raise IOError("Cannot open webcam")
while True:
ret, frame = cap.read()
frame = cv2.resize(frame, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_AREA)
cv2.imshow('Input', frame)
c = cv2.waitKey(1)
if c == 27:
break
cap.release()
cv2.destroyAllWindows()
底层原理
如我们在前面的代码中看到的,我们使用 OpenCV 的VideoCapture函数创建视频捕获对象上限。 创建完毕后,我们将开始无限循环并不断从网络摄像头读取帧,直到遇到键盘中断为止。
在while循环的第一行中,我们有以下行:
ret, frame = cap.read()
此处,ret是由read函数返回的布尔值,它指示是否成功捕获了帧。 如果正确捕获了帧,则将其存储在变量frame中。 该循环将一直运行,直到我们按Esc键。 因此,我们继续在以下行中检查键盘中断:
if c == 27:
众所周知,Esc的 ASCII 值为 27。一旦遇到它,我们就会中断循环并释放视频捕获对象。 cap.release()行很重要,因为它可以正常释放网络摄像头资源,以便其他应用可以使用它。
扩展捕获选项
在前面讨论的代码中,cv2.VideoCapture(0)使用自动检测到的读取器实现定义了默认连接的网络摄像头的使用,但是有多个选项涉及如何从网络摄像头读取图像。
在撰写本书时,版本 OpenCV 3.3.0 没有正确的方法列出可用的网络摄像头,因此在运行此代码时连接多个网络摄像头的情况下, 必须增加VideoCapture 的索引值,直到使用所需的索引值为止。
如果有多个可用阅读器实现,例如cv2.CAP_FFMPEG和cv2.CAP_IMAGES,或以cv2.CAP_*开头的所有东西,也可以强制实现特定的阅读器。 例如,您可以在网络摄像头索引 1 上使用QuickTime阅读器:
cap = cv2.VideoCapture(1 + cv2.CAP_QT) // Webcam index 1 + reader implementation QuickTime
键盘输入
现在我们知道了如何从网络摄像头捕获实时视频流,让我们看看如何使用键盘与显示视频流的窗口进行交互:
import cv2
def print_howto():
print("""
Change color space of the
input video stream using keyboard controls. The control keys are:
1\. Grayscale - press 'g'
2\. YUV - press 'y'
3\. HSV - press 'h'
""")
if __name__=='__main__':
print_howto()
cap = cv2.VideoCapture(0)
# Check if the webcam is opened correctly
if not cap.isOpened():
raise IOError("Cannot open webcam")
cur_mode = None
while True:
# Read the current frame from webcam
ret, frame = cap.read()
# Resize the captured image
frame = cv2.resize(frame, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_AREA)
c = cv2.waitKey(1)
if c == 27:
break
# Update cur_mode only in case it is different and key was pressed
# In case a key was not pressed during the iteration result is -1 or 255, depending
# on library versions
if c != -1 and c != 255 and c != cur_mode:
cur_mode = c
if cur_mode == ord('g'):
output = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
elif cur_mode == ord('y'):
output = cv2.cvtColor(frame, cv2.COLOR_BGR2YUV)
elif cur_mode == ord('h'):
output = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
else:
output = frame
cv2.imshow('Webcam', output)
cap.release()
cv2.destroyAllWindows()
与应用交互
该程序将显示输入视频流,并等待键盘输入更改色彩空间。 如果运行先前的程序,则将看到一个窗口,显示来自网络摄像头的输入视频流。 如果按G,您将看到输入流的色彩空间已转换为灰度。 如果按Y,则输入流将转换为 YUV 色彩空间。 同样,如果按H,您将看到图像正在转换为 HSV 色彩空间。
众所周知,我们使用函数waitKey()来监听键盘事件。 当我们遇到不同的按键时,我们将采取适当的措施。 我们使用函数ord()的原因是因为waitKey()返回键盘输入的 ASCII 值。 因此,我们需要在检查字符值之前将其转换为 ASCII 形式。
鼠标输入
在本节中,我们将看到如何使用鼠标与显示窗口进行交互。 让我们从简单的事情开始。 我们将编写一个程序,该程序将检测检测到鼠标单击的象限。 一旦检测到它,我们将突出显示该象限:
import cv2
import numpy as np
def detect_quadrant(event, x, y, flags, param):
if event == cv2.EVENT_LBUTTONDOWN:
if x > width/2:
if y > height/2:
point_top_left = (int(width/2), int(height/2))
point_bottom_right = (width-1, height-1)
else:
point_top_left = (int(width/2), 0)
point_bottom_right = (width-1, int(height/2))
else:
if y > height/2:
point_top_left = (0, int(height/2))
point_bottom_right = (int(width/2), height-1)
else:
point_top_left = (0, 0)
point_bottom_right = (int(width/2), int(height/2))
img = param["img"]
# Repaint all in white again
cv2.rectangle(img, (0,0), (width-1,height-1), (255,255,255), -1)
# Paint green quadrant
cv2.rectangle(img, point_top_left, point_bottom_right, (0,100,0), -1)
if __name__=='__main__':
width, height = 640, 480
img = 255 * np.ones((height, width, 3), dtype=np.uint8)
cv2.namedWindow('Input window')
cv2.setMouseCallback('Input window', detect_quadrant, {"img": img})
while True:
cv2.imshow('Input window', img)
c = cv2.waitKey(1)
if c == 27:
break
cv2.destroyAllWindows()
输出将如下图所示:

到底发生了什么?
让我们从该程序的main函数开始。 我们创建一个白色图像,我们将使用鼠标在其上单击。 然后,我们创建一个命名窗口并将MouseCallback函数绑定到该窗口。 MouseCallback函数基本上是检测到鼠标事件时将调用的函数。 鼠标事件有很多种,例如单击,双击,拖动等等。 在我们的例子中,我们只想检测鼠标单击。 在detect_quadrant函数中,我们检查第一个输入参数事件以查看执行了什么操作。 OpenCV 提供了一组预定义的事件,我们可以使用特定的关键字来调用它们。 如果要查看所有鼠标事件的列表,可以转到 Python shell 并键入以下内容:
>>> import cv2
>>> print([x for x in dir(cv2) if x.startswith('EVENT')])
detect_quadrant函数中的第二个和第三个参数提供了鼠标单击事件的X和Y坐标。 一旦知道了这些坐标,就可以很容易地确定其所在的象限。使用此信息,我们就可以继续使用cv2.rectangle()绘制具有指定颜色的矩形。 这是一个非常方便的函数,它使用左上角和右下角在具有指定颜色的图像上绘制矩形。
与实时视频流交互
让我们看看如何使用鼠标与网络摄像头中的实时视频流进行交互。 我们可以使用鼠标选择一个区域,然后对该区域应用负片效果,如下图所示:

在以下程序中,我们将从网络摄像头捕获视频流,用鼠标选择一个兴趣区域,然后应用此效果:
import cv2
import numpy as np
def update_pts(params, x, y):
global x_init, y_init
params["top_left_pt"] = (min(x_init, x), min(y_init, y))
params["bottom_right_pt"] = (max(x_init, x), max(y_init, y))
img[y_init:y, x_init:x] = 255 - img[y_init:y, x_init:x]
def draw_rectangle(event, x, y, flags, params):
global x_init, y_init, drawing
# First click initialize the init rectangle point
if event == cv2.EVENT_LBUTTONDOWN:
drawing = True
x_init, y_init = x, y
# Meanwhile mouse button is pressed, update diagonal rectangle point
elif event == cv2.EVENT_MOUSEMOVE and drawing:
update_pts(params, x, y)
# Once mouse botton is release
elif event == cv2.EVENT_LBUTTONUP:
drawing = False
update_pts(params, x, y)
if __name__=='__main__':
drawing = False
event_params = {"top_left_pt": (-1, -1), "bottom_right_pt": (-1, -1)}
cap = cv2.VideoCapture(0)
# Check if the webcam is opened correctly
if not cap.isOpened():
raise IOError("Cannot open webcam")
cv2.namedWindow('Webcam')
# Bind draw_rectangle function to every mouse event
cv2.setMouseCallback('Webcam', draw_rectangle, event_params)
while True:
ret, frame = cap.read()
img = cv2.resize(frame, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_AREA)
(x0,y0), (x1,y1) = event_params["top_left_pt"], event_params["bottom_right_pt"]
img[y0:y1, x0:x1] = 255 - img[y0:y1, x0:x1]
cv2.imshow('Webcam', img)
c = cv2.waitKey(1)
if c == 27:
break
cap.release()
cv2.destroyAllWindows()
如果运行上述程序,则会看到一个显示视频流的窗口。 您可以使用鼠标在窗口上绘制一个矩形,然后您会看到该区域被转换为负片。
我们是怎么做的?
正如我们在程序的main函数中看到的那样,我们初始化了一个视频捕获对象。 然后,在下面的行中将函数draw_rectangle与MouseCallback函数绑定:
cv2.setMouseCallback('Webcam', draw_rectangle, event_params)
然后,我们开始无限循环并开始捕获视频流。 让我们看看函数draw_rectangle中发生了什么; 每当我们使用鼠标绘制矩形时,我们基本上必须检测三种类型的鼠标事件:鼠标单击,鼠标移动和鼠标按钮释放。 这正是我们在此函数中所做的。 每当我们检测到鼠标单击事件时,我们都会初始化矩形的左上角。 当我们移动鼠标时,我们通过将当前位置保持为矩形的右下角来选择兴趣区域,并在通过引用传递的对象作为第三个参数(event_params)处对其进行更新。
一旦有了兴趣区域,我们只需反转像素即可应用负片效果。 我们从 255 中减去当前像素值,这给了我们想要的效果。 当鼠标移动停止并且检测到按下按钮事件时,我们将停止更新矩形的右下位置。 我们只是一直显示该图像,直到检测到另一个鼠标单击事件为止。
卡通化图像
现在,我们知道了如何处理网络摄像头和键盘/鼠标输入,让我们继续前进,看看如何将图片转换为卡通图像。 我们可以将图像转换为草图或彩色卡通图像。
以下是草图的示例:

如果将卡通化效果应用于彩色图像,它将看起来像下一个图像:

让我们看看如何实现这一目标:
import cv2
import numpy as np
def print_howto():
print("""
Change cartoonizing mode of image:
1\. Cartoonize without Color - press 's'
2\. Cartoonize with Color - press 'c'
""")
def cartoonize_image(img, ksize=5, sketch_mode=False):
num_repetitions, sigma_color, sigma_space, ds_factor = 10, 5, 7, 4
# Convert image to grayscale
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Apply median filter to the grayscale image
img_gray = cv2.medianBlur(img_gray, 7)
# Detect edges in the image and threshold it
edges = cv2.Laplacian(img_gray, cv2.CV_8U, ksize=ksize)
ret, mask = cv2.threshold(edges, 100, 255, cv2.THRESH_BINARY_INV)
# 'mask' is the sketch of the image
if sketch_mode:
return cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR)
# Resize the image to a smaller size for faster computation
img_small = cv2.resize(img, None, fx=1.0/ds_factor, fy=1.0/ds_factor, interpolation=cv2.INTER_AREA)
# Apply bilateral filter the image multiple times
for i in range(num_repetitions):
img_small = cv2.bilateralFilter(img_small, ksize, sigma_color, sigma_space)
img_output = cv2.resize(img_small, None, fx=ds_factor, fy=ds_factor, interpolation=cv2.INTER_LINEAR)
dst = np.zeros(img_gray.shape)
# Add the thick boundary lines to the image using 'AND' operator
dst = cv2.bitwise_and(img_output, img_output, mask=mask)
return dst
if __name__=='__main__':
print_howto()
cap = cv2.VideoCapture(0)
cur_mode = None
while True:
ret, frame = cap.read()
frame = cv2.resize(frame, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_AREA)
c = cv2.waitKey(1)
if c == 27:
break
if c != -1 and c != 255 and c != cur_mode:
cur_mode = c
if cur_mode == ord('s'):
cv2.imshow('Cartoonize', cartoonize_image(frame, ksize=5, sketch_mode=True))
elif cur_mode == ord('c'):
cv2.imshow('Cartoonize', cartoonize_image(frame, ksize=5, sketch_mode=False))
else:
cv2.imshow('Cartoonize', frame)
cap.release()
cv2.destroyAllWindows()
解构代码
当您运行上述程序时,您将看到一个窗口,其中包含来自网络摄像头的视频流。 如果按S,视频流将变为草图模式,并且您将看到其铅笔状的轮廓。 如果按C,您将看到输入流的彩色卡通版。 如果按任何其他键,它将返回到正常模式。
让我们看一下cartoonize_image函数,看看我们是如何做到的。 首先,我们将图像转换为灰度图像,然后通过中值过滤器进行处理。 中值过滤器非常擅长去除盐和胡椒粉的噪音。 当您在图像中看到孤立的黑色或白色像素时,就是这种噪声。 它在网络摄像头和移动相机中很常见,因此我们需要将其过滤掉,然后再继续进行。 举个例子,看下面的图片:

正如我们在输入图像中看到的,有很多孤立的绿色像素。 它们降低了图像的质量,我们需要摆脱它们。 这是中值过滤器派上用场的地方。 我们仅查看每个像素周围的NxN邻域,然后选择这些数字的中值。 由于在这种情况下,孤立的像素具有较高的值,因此取中间值将摆脱这些值,并使图像*滑。 正如您在输出图像中看到的那样,中值过滤器消除了所有这些孤立的像素,并且图像看起来很干净。 以下是此代码:
import cv2
import numpy as np
img = cv2.imread('images/input.jpg')
output = cv2.medianBlur(img, ksize=7)
cv2.imshow('Input', img)
cv2.imshow('Median filter', output)
cv2.waitKey()
该代码非常简单。 我们仅使用medianBlur函数将中值过滤器应用于输入图像。 此函数中的第二个参数指定我们正在使用的核的大小。 核的大小与我们需要考虑的邻域大小有关。 您可以试用此参数,并查看它如何影响输出。 请记住,可能的值只是奇数:1、3、5、7,依此类推。
回到cartoonize_image,我们继续检测灰度图像上的边缘。 我们需要知道边缘在哪里,以便创建铅笔线效果。 一旦检测到边缘,就对它们进行阈值处理,以使事物在字面上和隐喻上都变成黑白!
在下一步中,我们检查草图模式是否已启用。 如果是这样,那么我们只需将其转换为彩色图像并返回即可。 如果我们希望线变粗怎么办? 假设我们想看到类似下图的内容:

如您所见,线条比以前更粗。 为此,将if代码块替换为以下代码:
if sketch_mode:
img_sketch = cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR)
kernel = np.ones((3,3), np.uint8)
img_eroded = cv2.erode(img_sketch, kernel, iterations=1)
return cv2.medianBlur(img_eroded, ksize=5)
我们在这里将erode函数与3x3核一起使用。 之所以要使用此选项,是因为它使我们有机会使用线图的粗细。 现在,您可能会问:如果我们要增加某些物体的厚度,我们是否应该使用膨胀? 好吧,这种推理是正确的,但是这里有一个小小的转折。 请注意,前景为黑色,背景为白色。 侵蚀和膨胀将白色像素视为前景,将黑色像素视为背景。 因此,如果要增加黑色前景的厚度,则需要使用腐蚀。 施加腐蚀后,我们仅使用中值过滤器清除噪声并获得最终输出。
在下一步中,我们将使用双边过滤来*滑图像。 双边滤波是一个有趣的概念,它的性能比高斯过滤器好得多。 关于双边过滤的好处是它可以保留边缘,而高斯过滤器则可以使所有内容均等地*滑。 为了进行比较和对比,让我们看下面的输入图像:

让我们将高斯过滤器应用于上一张图片:

现在,让我们将双边过滤器应用于输入图像:

如您所见,如果使用双边过滤器,质量会更好。 图像看起来很*滑,边缘看起来也很清晰! 实现此目的的代码如下:
import cv2
import numpy as np
img = cv2.imread('images/input.jpg')
img_gaussian = cv2.GaussianBlur(img, (13,13), 0) # Gaussian Kernel Size 13x13
img_bilateral = cv2.bilateralFilter(img, 13, 70, 50)
cv2.imshow('Input', img)
cv2.imshow('Gaussian filter', img_gaussian)
cv2.imshow('Bilateral filter', img_bilateral)
cv2.waitKey()
如果仔细观察两个输出,可以看到高斯滤波图像的边缘看起来模糊。 通常,我们只想*滑图像中的粗糙区域并保持边缘完整。 这是双边过滤器派上用场的地方。 高斯过滤器仅查看紧邻区域,并使用由GaussianBlur方法应用的高斯核对像素值求*均。 双边过滤器通过仅对强度彼此相似的那些像素求*均,将这一概念提升到了一个新的水*。 它还使用颜色邻域度量标准来查看它是否可以替换强度相似的当前像素。 如果您看一下函数调用:
img_small = cv2.bilateralFilter(img_small, size, sigma_color,
sigma_space)
这里的最后两个参数指定颜色和空间邻域。 这就是在双边过滤器的输出中边缘看起来清晰的原因。 我们在图像上多次运行此过滤器以使其*滑化,使其看起来像卡通漫画。 然后,我们将铅笔状的遮罩叠加在此彩色图像的顶部,以创建类似卡通的效果。
总结
在本章中,我们学习了如何访问网络摄像头。 我们讨论了在实时视频流中如何获取键盘和鼠标输入。 我们使用此知识来创建交互式应用。 我们讨论了中值和双边过滤器,并讨论了双边过滤器相对于高斯过滤器的优势。 我们使用所有这些原理将输入图像转换为素描图像,然后将其卡通化。
在下一章中,我们将学习如何在静态图像以及实时视频中检测不同的身体部位。
四、检测和跟踪不同的身体部位
在本章中,我们将学习如何在实时视频流中检测和跟踪不同的身体部位。 我们将首先讨论面部检测管道以及它是如何从头开始构建的。 我们将学习如何使用此框架来检测和跟踪其他身体部位,例如眼睛,耳朵和嘴巴。
在本章结束时,您将了解:
- 如何使用 Haar 级联
- 什么是完整的图像
- 什么是自适应增强
- 如何在实时视频流中检测和跟踪面部
- 如何在实时视频流中检测和跟踪眼睛
- 如何将太阳镜自动覆盖在人的脸上
- 如何检测眼睛,耳朵和嘴巴
- 如何使用形状分析检测瞳孔
使用 Haar 级联检测对象
当我们说 Haar 级联时,实际上是在谈论基于 Haar 特征的级联分类器。 要了解这意味着什么,我们需要退后一步,并首先了解为什么需要这样做。 早在 2001 年,Paul Viola 和 Michael Jones 在他们的开创性论文中提出了一种非常有效的对象检测方法。 它已成为机器学习领域的主要标志之一。
在他们的论文中,他们描述了一种机器学习技术,其中使用了增强的简单分类器级联来获得表现非常好的整体分类器。 这样,我们就可以避免构建单个复杂分类器以实现高精度的过程。 之所以如此令人惊讶,是因为构建一个强大的单步分类器是一个计算量大的过程。 此外,我们需要大量的训练数据来建立这样的分类器。 该模型最终变得复杂,并且表现可能达不到要求。
假设我们要检测像菠萝这样的物体。 为了解决这个问题,我们需要构建一个机器学习系统,该系统将学习菠萝的外观。 它应该能够告诉我们未知图像是否包含菠萝。 为了实现这样的目标,我们需要训练我们的系统。 在机器学习领域,我们有很多方法可以训练系统。 这很像训练狗,只是它不会帮您拿到球! 为了训练我们的系统,我们拍摄了很多菠萝和非菠萝图像,然后将它们输入到系统中。 在此,将菠萝图像称为正图像,将非菠萝图像称为负图像。
就训练而言,有很多路线可供选择。 但是所有传统技术都是计算密集型的,并且会导致模型复杂。 我们不能使用这些模型来构建实时系统。 因此,我们需要保持分类器简单。 但是,如果我们保持分类器简单,那么它将是不准确的。 速度和准确率之间的权衡在机器学习中很常见。 通过构建一组简单的分类器,然后将它们级联在一起以形成一个健壮的统一分类器,我们克服了这个问题。 为了确保整体分类器运行良好,我们需要在层叠步骤中发挥创意。 这是 Viola-Jones 方法如此有效的主要原因之一。
谈到人脸检测这个话题,让我们看看如何训练一个系统来检测人脸。 如果要构建机器学习系统,则首先需要从所有图像中提取特征。 在我们的案例中,机器学习算法将使用这些特征来学习人脸。 我们使用 Haar 特征来构建特征向量。 Haar 特征是整个图像上补丁的简单求和和差异。 我们以多种图像尺寸执行此操作,以确保我们的系统缩放不变。
如果您好奇,可以通过以下网址了解有关该公式的更多信息
提取这些特征后,我们将其传递给一系列的分类器。 我们只检查所有不同的矩形子区域,并继续丢弃其中没有人脸的区域。 这样,我们可以快速得出最终答案,以查看给定的矩形是否包含面。
什么是完整图片?
如果要计算 Haar 特征,则必须计算图像中许多不同矩形区域的总和。 如果要有效地构建特征集,则需要在多个尺度上计算这些总和。 这是一个非常昂贵的过程! 如果我们要构建一个实时系统,我们就不能花费那么多的周期来计算这些和。 因此,我们使用了积分图像:

要计算图像中任何矩形的总和,我们不需要遍历该矩形区域中的所有元素。 假设AP表示矩形中由左上角的点和图像中的点P作为两个对角相对的角所形成的所有元素的总和。 因此,现在,如果要计算矩形 ABCD 的面积,可以使用以下公式:
矩形 ABCD 的面积 = AC - (AB + AD - AA)
我们为什么要关心这个特定公式? 如前所述,提取 Haar 特征包括以多个比例计算图像中大量矩形的面积。 这些计算中有很多是重复的,整个过程非常缓慢。 实际上,它是如此之慢,以至于我们无力实时运行任何东西。 这就是我们使用这种秘籍的原因! 这种方法的好处是我们不必重新计算任何东西。 该方程式右侧面积的所有值均已可用。 因此,我们仅使用它们来计算任何给定矩形的面积并提取特征。
检测和追踪人脸
OpenCV 提供了一个不错的人脸检测框架。 我们只需要加载级联文件并使用它来检测图像中的面部。 让我们看看如何做到这一点:
import cv2
import numpy as np
face_cascade = cv2.CascadeClassifier('./cascade_files/haarcascade_frontalface_alt.xml')
cap = cv2.VideoCapture(0)
scaling_factor = 0.5
while True:
ret, frame = cap.read()
frame = cv2.resize(frame, None, fx=scaling_factor,
fy=scaling_factor, interpolation=cv2.INTER_AREA)
face_rects = face_cascade.detectMultiScale(frame, scaleFactor=1.3, minNeighbors=3)
for (x,y,w,h) in face_rects:
cv2.rectangle(frame, (x,y), (x+w,y+h), (0,255,0), 3)
cv2.imshow('Face Detector', frame)
c = cv2.waitKey(1)
if c == 27:
break
cap.release()
cv2.destroyAllWindows()
如果运行前面的代码,结果将类似于下图:

更好地了解它
我们需要一个可用于检测图像中人脸的分类器模型。 OpenCV 提供了可用于此目的的 XML 文件。 我们使用函数CascadeClassifier加载 XML 文件。 一旦我们开始从网络摄像头捕获输入帧并使用detectMultiScale函数获取当前图像中所有面部的边界框,以防万一帧没有以灰度传递,则将在该方法内部运行,因为需要灰度帧来处理检测。 此函数中的第二个参数指定缩放因子的跳跃,例如,如果我们找不到当前缩放比例的图像,则在我们的示例中,下一个要检查的尺寸将比当前尺寸大 1.3 倍。 最后一个参数是一个阈值,用于指定保留当前矩形所需的最小相邻矩形数。 万一人脸识别无法按预期工作时,可以使用它来提高人脸检测器的鲁棒性,降低阈值以获得更好的识别。 如果图像由于处理检测而有些延迟,请将缩放帧的大小减小 0.4 或 0.3。
人脸上的乐趣
现在我们知道了如何检测和跟踪脸部,让我们一起玩一些乐趣。 当我们从摄像头捕获视频流时,我们可以在脸部上方覆盖有趣的遮罩。 它看起来像下图:

如果您是汉尼拔的粉丝,可以尝试以下方法:

让我们看一下代码,看看如何在输入视频流中将头骨遮罩覆盖在面部顶部:
import cv2
import numpy as np
face_cascade = cv2.CascadeClassifier('./cascade_files/haarcascade_frontalface_alt.xml')
face_mask = cv2.imread(img/mask_hannibal.png')
h_mask, w_mask = face_mask.shape[:2]
if face_cascade.empty():
raise IOError('Unable to load the face cascade classifier xml file')
cap = cv2.VideoCapture(0)
scaling_factor = 0.5
while True:
ret, frame = cap.read()
frame = cv2.resize(frame, None, fx=scaling_factor, fy=scaling_factor, interpolation=cv2.INTER_AREA)
face_rects = face_cascade.detectMultiScale(frame, scaleFactor=1.3, minNeighbors=3)
for (x,y,w,h) in face_rects:
if h <= 0 or w <= 0: pass
# Adjust the height and weight parameters depending on the sizes and the locations.
# You need to play around with these to make sure you get it right.
h, w = int(1.0*h), int(1.0*w)
y -= int(-0.2*h)
x = int(x)
# Extract the region of interest from the image
frame_roi = frame[y:y+h, x:x+w]
face_mask_small = cv2.resize(face_mask, (w, h), interpolation=cv2.INTER_AREA)
# Convert color image to grayscale and threshold it
gray_mask = cv2.cvtColor(face_mask_small, cv2.COLOR_BGR2GRAY)
ret, mask = cv2.threshold(gray_mask, 180, 255, cv2.THRESH_BINARY_INV)
# Create an inverse mask
mask_inv = cv2.bitwise_not(mask)
try:
# Use the mask to extract the face mask region of interest
masked_face = cv2.bitwise_and(face_mask_small, face_mask_small, mask=mask)
# Use the inverse mask to get the remaining part of the image
masked_frame = cv2.bitwise_and(frame_roi, frame_roi, mask=mask_inv)
except cv2.error as e:
print('Ignoring arithmentic exceptions: '+ str(e))
# add the two images to get the final output
frame[y:y+h, x:x+w] = cv2.add(masked_face, masked_frame)
cv2.imshow('Face Detector', frame)
c = cv2.waitKey(1)
if c == 27:
break
cap.release()
cv2.destroyAllWindows()
底层原理
像以前一样,我们首先加载面部级联分类器 XML 文件。 人脸检测步骤照常工作。 我们开始无限循环,并持续检测每一帧中的人脸。 一旦知道人脸在哪里,就需要稍微修改坐标以确保遮罩正确适合。 该操作过程是主观的,并且取决于所讨论的掩模。 不同的遮罩需要不同程度的调整,以使其看起来更自然。 我们在以下行中从输入帧中提取兴趣区域:
frame_roi = frame[y:y+h, x:x+w]
现在我们有了所需的关注区域,我们需要在此之上覆盖遮罩。 因此,我们调整输入遮罩的大小以确保它适合此兴趣区域。 输入掩码具有白色背景。 因此,如果我们仅将其覆盖在兴趣区域的顶部,由于白色背景,它看起来将不自然。 我们只需要覆盖头骨遮罩像素即可; 其余区域应该是透明的。
因此,在下一步中,我们通过对头骨图像进行阈值处理来创建遮罩。 由于背景是白色,因此我们对图像进行阈值处理,以使强度值大于 180 的任何像素变为零,其他所有值变为 255。就感兴趣的帧区域而言,我们需要将这个遮罩区域的所有东西涂黑。 我们可以通过简单地使用刚创建的遮罩的逆来实现。 一旦我们获得了头骨图像的遮罩版本和感兴趣的输入区域,我们就将它们加起来以获得最终图像。
从重叠图像中删除 Alpha 通道
由于使用了重叠图像,我们应该记住,可能会在黑色像素上构建一层,这会对我们的代码结果产生不良影响。 为了避免该问题,以下代码从叠加图像中删除了 Alpha 通道层,因此使我们可以在本章中的示例代码上获得良好的效果:
import numpy as np
import cv2
def remove_alpha_channel(source, background_color):
source_img = cv2.cvtColor(source[:,:,:3], cv2.COLOR_BGR2GRAY)
source_mask = source[:,:,3] * (1 / 255.0)
bg_part = (255 * (1 / 255.0)) * (1.0 - source_mask)
weight = (source_img * (1 / 255.0)) * (source_mask)
dest = np.uint8(cv2.addWeighted(bg_part, 255.0, weight, 255.0, 0.0))
return dest
orig_img = cv2.imread(img/overlay_source.png', cv2.IMREAD_UNCHANGED)
dest_img = remove_alpha_channel(orig_img)
cv2.imwrite('images/overlay_dest.png', dest_img, [cv2.IMWRITE_PNG_COMPRESSION])
检测眼睛
现在我们了解了如何检测脸部,我们也可以将概念推广到检测其他身体部位。 重要的是要了解 Viola-Jones 框架可以应用于任何对象。 准确率和鲁棒性将取决于对象的唯一性。 例如,人脸具有非常独特的特征,因此很容易将我们的系统训练成健壮的。 另一方面,毛巾,衣服或书本之类的物体太普通,并且没有明显的特征,因此要构建坚固的检测器会更加困难。
让我们看看如何构建眼睛检测器:
import cv2
import numpy as np
face_cascade = cv2.CascadeClassifier('./cascade_files/haarcascade_frontalface_alt.xml')
eye_cascade = cv2.CascadeClassifier('./cascade_files/haarcascade_eye.xml')
if face_cascade.empty():
raise IOError('Unable to load the face cascade classifier xml file')
if eye_cascade.empty():
raise IOError('Unable to load the eye cascade classifier xml file')
cap = cv2.VideoCapture(0)
ds_factor = 0.5
while True:
ret, frame = cap.read()
frame = cv2.resize(frame, None, fx=ds_factor, fy=ds_factor, interpolation=cv2.INTER_AREA)
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
faces = face_cascade.detectMultiScale(gray, scaleFactor=1.3, minNeighbors=1)
for (x,y,w,h) in faces:
roi_gray = gray[y:y+h, x:x+w]
roi_color = frame[y:y+h, x:x+w]
eyes = eye_cascade.detectMultiScale(roi_gray)
for (x_eye,y_eye,w_eye,h_eye) in eyes:
center = (int(x_eye + 0.5*w_eye), int(y_eye + 0.5*h_eye))
radius = int(0.3 * (w_eye + h_eye))
color = (0, 255, 0)
thickness = 3
cv2.circle(roi_color, center, radius, color, thickness)
cv2.imshow('Eye Detector', frame)
c = cv2.waitKey(1)
if c == 27:
break
cap.release()
cv2.destroyAllWindows()
如果运行此程序,输出将类似于下图:

事后思考
如果您注意到,该程序看起来与人脸检测程序非常相似。 除了加载人脸检测级联分类器外,我们还加载了眼睛检测级联分类器。 从技术上讲,我们不需要使用面部检测器。 但是我们知道眼睛总是在别人的脸上。 我们使用此信息并仅在感兴趣的相关区域(即脸部)中搜索眼睛。 我们首先检测人脸,然后在该子图像上运行眼睛检测器。 这样,它更快,更高效。
有趣的眼睛
现在,我们知道了如何检测图像中的眼睛,让我们看看我们是否可以对此做一些有趣的事情。 我们可以执行以下屏幕快照中所示的操作:

让我们看一下代码,看看如何做这样的事情:
import cv2
import numpy as np
face_cascade = cv2.CascadeClassifier('./cascade_files/haarcascade_frontalface_alt.xml')
eye_cascade = cv2.CascadeClassifier('./cascade_files/haarcascade_eye.xml')
if face_cascade.empty():
raise IOError('Unable to load the face cascade classifier xml file')
if eye_cascade.empty():
raise IOError('Unable to load the eye cascade classifier xml file')
cap = cv2.VideoCapture(0)
sunglasses_img = cv2.imread('images/sunglasses.png')
while True:
ret, frame = cap.read()
frame = cv2.resize(frame, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_AREA)
vh, vw = frame.shape[:2]
vh, vw = int(vh), int(vw)
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
faces = face_cascade.detectMultiScale(gray, scaleFactor=1.3, minNeighbors=1)
centers = []
for (x,y,w,h) in faces:
roi_gray = gray[y:y+h, x:x+w]
roi_color = frame[y:y+h, x:x+w]
eyes = eye_cascade.detectMultiScale(roi_gray)
for (x_eye,y_eye,w_eye,h_eye) in eyes:
centers.append((x + int(x_eye + 0.5*w_eye), y + int(y_eye + 0.5*h_eye)))
if len(centers) > 1: # if detects both eyes
h, w = sunglasses_img.shape[:2]
# Extract the region of interest from the image
eye_distance = abs(centers[1][0] - centers[0][0])
# Overlay sunglasses; the factor 2.12 is customizable depending on the size of the face
sunglasses_width = 2.12 * eye_distance
scaling_factor = sunglasses_width / w
print(scaling_factor, eye_distance)
overlay_sunglasses = cv2.resize(sunglasses_img, None, fx=scaling_factor, fy=scaling_factor, interpolation=cv2.INTER_AREA)
x = centers[0][0] if centers[0][0] < centers[1][0] else centers[1][0]
# customizable X and Y locations; depends on the size of the face
x -= int(0.26*overlay_sunglasses.shape[1])
y += int(0.26*overlay_sunglasses.shape[0])
h, w = overlay_sunglasses.shape[:2]
h, w = int(h), int(w)
frame_roi = frame[y:y+h, x:x+w]
# Convert color image to grayscale and threshold it
gray_overlay_sunglassess = cv2.cvtColor(overlay_sunglasses, cv2.COLOR_BGR2GRAY)
ret, mask = cv2.threshold(gray_overlay_sunglassess, 180, 255, cv2.THRESH_BINARY_INV)
# Create an inverse mask
mask_inv = cv2.bitwise_not(mask)
try:
# Use the mask to extract the face mask region of interest
masked_face = cv2.bitwise_and(overlay_sunglasses, overlay_sunglasses, mask=mask)
# Use the inverse mask to get the remaining part of the image
masked_frame = cv2.bitwise_and(frame_roi, frame_roi, mask=mask_inv)
except cv2.error as e:
print('Ignoring arithmentic exceptions: '+ str(e))
#raise e
# add the two images to get the final output
frame[y:y+h, x:x+w] = cv2.add(masked_face, masked_frame)
else:
print('Eyes not detected')
cv2.imshow('Eye Detector', frame)
c = cv2.waitKey(1)
if c == 27:
break
cap.release()
cv2.destroyAllWindows()
放置太阳镜
就像我们之前所做的一样,我们加载图像并检测眼睛。 一旦检测到眼睛,便会调整太阳镜图像的大小以适合当前的关注区域。 为了创建兴趣区域,我们考虑了眼睛之间的距离。 我们相应地调整图像的大小,然后继续创建遮罩。 这类似于我们之前对骷髅面罩所做的操作。 太阳镜在脸上的位置是主观的,因此如果您想使用另一副太阳镜,则必须调整权重。
检测耳朵
通过使用 Haar 级联分类器文件,下面的代码将再次识别每只耳朵,并在检测到它们时将它们突出显示。 您会注意到,需要两个不同的分类器,因为每个耳朵的坐标将被反转:
import cv2
import numpy as np
left_ear_cascade = cv2.CascadeClassifier('./cascade_files/haarcascade_mcs_leftear.xml')
right_ear_cascade = cv2.CascadeClassifier('./cascade_files/haarcascade_mcs_rightear.xml')
if left_ear_cascade.empty():
raise IOError('Unable to load the left ear cascade classifier xml file')
if right_ear_cascade.empty():
raise IOError('Unable to load the right ear cascade classifier xml file')
cap = cv2.VideoCapture(0)
scaling_factor = 0.5
while True:
ret, frame = cap.read()
frame = cv2.resize(frame, None, fx=scaling_factor, fy=scaling_factor, interpolation=cv2.INTER_AREA)
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
left_ear = left_ear_cascade.detectMultiScale(gray, scaleFactor=1.3, minNeighbors=3)
right_ear = right_ear_cascade.detectMultiScale(gray, scaleFactor=1.3, minNeighbors=3)
for (x,y,w,h) in left_ear:
cv2.rectangle(frame, (x,y), (x+w,y+h), (0,255,0), 3)
for (x,y,w,h) in right_ear:
cv2.rectangle(frame, (x,y), (x+w,y+h), (255,0,0), 3)
cv2.imshow('Ear Detector', frame)
c = cv2.waitKey(1)
if c == 27:
break
cap.release()
cv2.destroyAllWindows()
如果在图像上运行前面的代码,则应该看到类似以下屏幕截图的内容:

检测嘴巴
这次,使用 Haar 分类器,我们将从输入视频流中提取嘴巴位置,在该代码下面的代码中,我们将使用这些坐标在脸上放置小胡子:
import cv2
import numpy as np
mouth_cascade = cv2.CascadeClassifier('./cascade_files/haarcascade_mcs_mouth.xml')
if mouth_cascade.empty():
raise IOError('Unable to load the mouth cascade classifier xml file')
cap = cv2.VideoCapture(0)
ds_factor = 0.5
while True:
ret, frame = cap.read()
frame = cv2.resize(frame, None, fx=ds_factor, fy=ds_factor, interpolation=cv2.INTER_AREA)
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
mouth_rects = mouth_cascade.detectMultiScale(gray, scaleFactor=1.7, minNeighbors=11)
for (x,y,w,h) in mouth_rects:
y = int(y - 0.15*h)
cv2.rectangle(frame, (x,y), (x+w,y+h), (0,255,0), 3)
break
cv2.imshow('Mouth Detector', frame)
c = cv2.waitKey(1)
if c == 27:
break
cap.release()
cv2.destroyAllWindows()
下图显示了输出结果:

覆盖胡须
让我们在上面盖上胡须:
import cv2
import numpy as np
mouth_cascade = cv2.CascadeClassifier('./cascade_files/haarcascade_mcs_mouth.xml')
moustache_mask = cv2.imread(img/moustache.png')
h_mask, w_mask = moustache_mask.shape[:2]
if mouth_cascade.empty():
raise IOError('Unable to load the mouth cascade classifier xml file')
cap = cv2.VideoCapture(0)
scaling_factor = 0.5
while True:
ret, frame = cap.read()
frame = cv2.resize(frame, None, fx=scaling_factor, fy=scaling_factor, interpolation=cv2.INTER_AREA)
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
mouth_rects = mouth_cascade.detectMultiScale(gray, 1.3, 5)
if len(mouth_rects) > 0:
(x,y,w,h) = mouth_rects[0]
h, w = int(0.6*h), int(1.2*w)
x -= int(0.05*w)
y -= int(0.55*h)
frame_roi = frame[y:y+h, x:x+w]
moustache_mask_small = cv2.resize(moustache_mask, (w, h), interpolation=cv2.INTER_AREA)
gray_mask = cv2.cvtColor(moustache_mask_small, cv2.COLOR_BGR2GRAY)
ret, mask = cv2.threshold(gray_mask, 50, 255, cv2.THRESH_BINARY_INV)
mask_inv = cv2.bitwise_not(mask)
masked_mouth = cv2.bitwise_and(moustache_mask_small, moustache_mask_small, mask=mask)
masked_frame = cv2.bitwise_and(frame_roi, frame_roi, mask=mask_inv)
frame[y:y+h, x:x+w] = cv2.add(masked_mouth, masked_frame)
cv2.imshow('Moustache', frame)
c = cv2.waitKey(1)
if c == 27:
break
cap.release()
cv2.destroyAllWindows()
看起来是这样的:

检测瞳孔
我们将在这里采取不同的方法。 瞳孔太普通了,无法采用 Haar 级联方法。 我们还将了解如何根据事物的形状检测事物。 以下是输出结果:

让我们看看如何构建瞳孔检测器:
import math
import cv2
eye_cascade = cv2.CascadeClassifier('./cascade_files/haarcascade_eye.xml')
if eye_cascade.empty():
raise IOError('Unable to load the eye cascade classifier xml file')
cap = cv2.VideoCapture(0)
ds_factor = 0.5
ret, frame = cap.read()
contours = []
while True:
ret, frame = cap.read()
frame = cv2.resize(frame, None, fx=ds_factor, fy=ds_factor, interpolation=cv2.INTER_AREA)
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
eyes = eye_cascade.detectMultiScale(gray, scaleFactor=1.3, minNeighbors=1)
for (x_eye, y_eye, w_eye, h_eye) in eyes:
pupil_frame = gray[y_eye:y_eye + h_eye, x_eye:x_eye + w_eye]
ret, thresh = cv2.threshold(pupil_frame, 80, 255, cv2.THRESH_BINARY)
cv2.imshow("threshold", thresh)
im2, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
print(contours)
for contour in contours:
area = cv2.contourArea(contour)
rect = cv2.boundingRect(contour)
x, y, w, h = rect
radius = 0.15 * (w + h)
area_condition = (100 <= area <= 200)
symmetry_condition = (abs(1 - float(w)/float(h)) <= 0.2)
fill_condition = (abs(1 - (area / (math.pi * math.pow(radius, 2.0)))) <= 0.4)
cv2.circle(frame, (int(x_eye + x + radius), int(y_eye + y + radius)), int(1.3 * radius), (0, 180, 0), -1)
cv2.imshow('Pupil Detector', frame)
c = cv2.waitKey(1)
if c == 27:
break
cap.release()
cv2.destroyAllWindows()
如果运行此程序,您将看到如前所示的输出。
解构代码
如前所述,我们不会使用 Haar 级联来检测学生。 如果我们不能使用预先训练的分类器,那么我们将如何检测学生? 好吧,我们可以使用形状分析来检测瞳孔。 我们知道瞳孔是圆形的,因此我们可以使用此信息在图像中检测到它们。 我们反转输入图像,然后将其转换为灰度图像,如以下行所示:
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
正如我们在这里看到的,我们可以使用波浪号运算符反转图像。 在我们的情况下,将图像反转非常有用,因为瞳孔是黑色的,而黑色对应于较低的像素值。 然后,我们对图像进行阈值处理,以确保只有黑白像素。 现在,我们必须找出所有形状的边界。 OpenCV 提供了一个很好的函数来实现这一目标,即findContours。 我们将在接下来的章节中讨论更多有关此的内容。 但就目前而言,我们所需要知道的是,此函数返回在图像中找到的所有形状的边界集。
下一步是识别瞳孔的形状,并丢弃其余的瞳孔。 我们将使用圆的某些属性对此形状进行归零。 让我们考虑边界矩形的宽高比。 如果形状是圆形,则该比率将为 1。 我们可以使用boundingRect函数来获取边界矩形的坐标。 让我们考虑一下这种形状的面积。 如果我们粗略计算此形状的半径并使用公式计算圆的面积,则它应接*此轮廓的面积。 我们可以使用contourArea函数来计算图像中任何轮廓的面积。 因此,我们可以使用这些条件并过滤出形状。 完成此操作后,图像中剩下两个瞳孔。 我们可以通过将搜索区域限制在面部或眼睛来进一步完善它。 由于您知道如何检测面部和眼睛,因此可以尝试一下,看看是否可以将其用于实时视频流。
如果您想玩另一种身体检测,只需转到以下链接以找到差异分类器。
总结
在本章中,我们讨论了 Haar 级联和积分图像。 我们了解了人脸检测管道的构建方式。 我们学习了如何在实时视频流中检测和跟踪面部。 我们讨论了如何使用面部检测管道来检测身体的各个部位,例如眼睛,耳朵,鼻子和嘴巴。 我们学习了如何使用身体部位检测的结果在输入图像的顶部覆盖遮罩。 我们使用形状分析的原理来检测学生。
在下一章中,我们将讨论特征检测以及如何将其用于理解图像内容。
五、从图像中提取特征
在本章中,我们将学习如何检测图像中的显着点,也称为关键点。 我们将讨论为什么这些关键点很重要,以及如何使用它们来理解图像内容。 我们将讨论可用于检测这些关键点的不同技术,并了解如何从给定图像中提取特征。
在本章结束时,您将了解以下内容:
- 关键点是什么,我们为什么要关心它们?
- 如何检测关键点
- 如何将关键点用于图像内容分析
- 检测关键点的不同技术
- 如何构建特征提取器
我们为什么要关心关键点?
图像内容分析是指理解图像内容的过程,以便我们可以据此采取一些措施。 让我们退后一步,谈谈人类是如何做到的。 我们的大脑是一个非常强大的机器,可以非常快速地完成复杂的事情。 当我们观察事物时,我们的大脑会根据该图像的有趣的方面自动创建足迹。 在本章中,我们将讨论有趣的方法。
目前,一个有趣的方面是该地区与众不同的地方。 如果我们将一个点称为有趣的点,那么在它的邻域中不应有另一个点满足约束条件。 让我们考虑下图:

现在,闭上你的眼睛,并尝试形象化这张图片。 您看到特定的东西了吗? 您可以回忆一下图像左半部分的内容吗? 并不是的! 这样做的原因是图像没有任何有趣的信息。 当我们的大脑看着这样的东西时,没有什么需要注意的,因此它会四处游荡! 让我们看一下下图:

现在,闭上你的眼睛,并尝试形象化这张图片。 您会看到回忆很生动,并且还记得有关此图像的许多细节。 原因是图像中有很多有趣的区域。 人眼对高频内容比低频内容更敏感。 这就是为什么我们倾向于比第一幅图像更好地收集第二幅图像的原因。 为了进一步说明这一点,让我们看一下下图:

如果您注意到,即使它不在图像中心,您的视线也会立即移到电视遥控器上。 我们会自动趋向于图像中的有趣区域,因为这是所有信息所在的位置。 这是我们的大脑需要存储的内容,以便以后进行重新收集。
在构建对象识别系统时,我们需要检测这些有趣的区域,以创建图像的签名。 这些有趣的区域以关键点为特征。 这就是为什么关键点检测在许多现代计算机视觉系统中至关重要的原因。
关键点是什么?
现在我们知道关键点是指图像中有趣的区域,下面让我们更深入地进行研究。 关键点是什么? 这些要点在哪里? 当我们说有趣时,表示该区域正在发生某些事情。 如果该区域是统一的,那就不是很有趣。 例如,角点很有趣,因为强度在两个不同方向上急剧变化。 每个角都是两个边相交的唯一点。 如果查看前面的图像,您会发现有趣的区域并没有完全由有趣的内容组成。 如果仔细观察,我们仍然可以看到繁忙区域中的*原区域。 例如,考虑以下图像:

如果您查看前面的对象,则有趣区域的内部部分不有趣:

因此,如果要表征该对象,则需要确保选择了有趣的点。 现在,我们如何定义有趣点? 我们可以说没有什么不有趣的事情可能是一个有趣的观点吗? 让我们考虑以下示例:

现在,我们可以看到该图像沿边缘有很多高频内容。 但是我们不能称整个边缘为有趣的。 重要的是要理解有趣的不一定涉及颜色或强度值。 只要是不同的,它可以是任何东西。 我们需要隔离它们附*唯一的点。 沿边缘的点相对于它们的邻居不是唯一的。 那么,既然我们知道我们在寻找什么,我们如何挑选一个有趣的观点?
桌子的一角呢? 那很有趣,对不对? 就其邻居而言,它是独一无二的,我们附*没有类似的东西。 现在,可以选择这一点作为我们的关键点之一。 我们利用这些关键点来表征特定的图像。
在进行图像分析时,我们需要先将图像转换为数字形式,然后才能得出结论。 这些关键点使用数字形式表示,然后使用这些关键点的组合来创建图像签名。 我们希望该图像签名以最好的方式表示给定的图像。
检测角点
由于我们知道角点很有趣,因此让我们看一下如何检测它们。 在计算机视觉中,有一种流行的角点检测技术,称为哈里斯角点检测器。 我们基本上基于灰度图像的偏导数构造一个2x2矩阵,然后分析获得的特征值。 特征值是一组特殊的标量,与一组线性方程组相关联,这些方程组通过属于一起的像素群集提供有关图像的分段信息。 在这种情况下,我们使用它们来检测角点。 这实际上是对实际算法的过度简化,但涵盖了要点。 因此,如果您想了解基本的数学细节,可以在这个页面上查看 Harris 和 Stephens 的原始论文。角点是两个特征值都将具有较大值的点。
让我们考虑下图:

如果在此图像上运行哈里斯角点探测器,您将看到类似以下内容:

如您所见,所有黑点均对应于图像中的角。 您可能会注意到未检测到盒子底部的角。 原因是角点不够尖锐。 您可以在角点检测器中调整阈值以识别这些角点。 执行此操作的代码如下:
import cv2
import numpy as np
img = cv2.imread(img/box.png')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
gray = np.float32(gray)
# To detect only sharp corners
dst = cv2.cornerHarris(gray, blockSize=4, ksize=5, k=0.04)
# Result is dilated for marking the corners
dst = cv2.dilate(dst, None)
# Threshold for an optimal value, it may vary depending on the image
img[dst > 0.01*dst.max()] = [0,0,0]
cv2.imshow('Harris Corners(only sharp)',img)
# to detect soft corners
dst = cv2.cornerHarris(gray, blockSize=14, ksize=5, k=0.04)
dst = cv2.dilate(dst, None)
img[dst > 0.01*dst.max()] = [0,0,0]
cv2.imshow('Harris Corners(also soft)',img)
cv2.waitKey()
良好的跟踪特征
在许多情况下,Harris Corner Detector 的表现都不错,但在某些方面却漏了。 在哈里斯和斯蒂芬斯撰写原始论文大约六年后,史和托马西想出了一个更好的角检测器。 您可以在这里阅读原始论文。 J. Shi 和 C.Tomasi 使用了不同的评分函数来提高整体质量。 使用这种方法,我们可以找到给定图像中的 N 个最强角。 当我们不想使用每个角来从图像中提取信息时,这非常有用。
如果将 Shi-Tomasi 角点检测器应用于之前显示的图像,则会看到类似以下内容:

以下是代码:
import cv2
import numpy as np
img = cv2.imread('images/box.png')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
corners = cv2.goodFeaturesToTrack(gray, maxCorners=7, qualityLevel=0.05, minDistance=25)
corners = np.float32(corners)
for item in corners:
x, y = item[0]
cv2.circle(img, (x,y), 5, 255, -1)
cv2.imshow("Top 'k' features", img)
cv2.waitKey()
尺度不变特征变换(SIFT)
即使边角特征很有趣,它们也不足以表征真正有趣的部分。 当我们谈论图像内容分析时,我们希望图像签名对于诸如比例尺,旋转和照明之类的东西不变。 人类在这些事情上非常擅长。 即使我给您看的苹果颠倒的图像变暗,您仍然会识别它。 如果我向您展示该图像的放大版本,您仍会识别它。 我们希望我们的图像识别系统能够做到这一点。
让我们考虑角点特征。 如果放大图像,角点可能不再是角点,如下所示:

在第二种情况下,检测器将不会拾取该角。 并且,由于它是在原始图像中拾取的,因此第二张图像将与第一张图像不匹配。 它基本上是相同的图像,但是基于角点特征的方法将完全错过它。 这意味着角点检测器并非完全不变。 这就是为什么我们需要一种更好的方法来表征图像的原因。
SIFT 是整个计算机视觉领域中最受欢迎的算法之一。 您可以在这个页面上阅读 David Lowe 的原始论文。 我们可以使用该算法提取关键点并构建相应的特征描述符。 在线上有很多好的文档,因此我们将简短地讨论。 为了确定潜在的关键点,SIFT 通过对图像进行下采样并获取高斯差来构建金字塔。 这意味着我们在每个级别上运行一个高斯过滤器,并利用差值在金字塔中构建连续的级别。 为了查看当前点是否为关键点,它会查看相邻点以及金字塔相邻级别中同一位置的像素。 如果是最大值,则将当前点作为关键点。 这样可以确保我们将关键点保持不变。
现在我们知道了 SIFT 如何实现尺度不变性,让我们看看它如何实现旋转不变性。 一旦我们确定了关键点,便为每个关键点分配了方向。 我们采用每个关键点附*的邻域,并计算梯度大小和方向。 这使我们对关键点的方向有所了解。 如果我们有此信息,即使旋转该关键点,也可以将其与另一个图像中的同一点进行匹配。 由于我们知道方向,因此我们可以在进行比较之前将这些关键点归一化。
一旦获得所有这些信息,我们如何量化它? 我们需要将其转换为一组数字,以便可以对其进行某种匹配。 为此,我们只需要围绕每个关键点设置16x16的邻域,并将其划分为 16 个大小为4x4的块。 对于每个块,我们使用八个面元计算方向直方图。 因此,我们有一个与每个块相关联的长度为 8 的向量,这意味着该邻域由大小为 128(8x16)的向量表示。 这是将要使用的最终关键点描述符。 如果从图像中提取N个关键点,则每个长度为 128 的N个描述符。N个描述符的数组表征了给定的图像。
考虑下图:

如果使用 SIFT 提取关键点位置,您将看到类似以下的内容,其中圆圈的大小指示关键点的强度,圆圈内的线指示方向:

在查看代码之前,重要的是要知道 SIFT 已获得专利,并且不能免费商业使用。 以下是执行此操作的代码:
import cv2
import numpy as np
input_image = cv2.imread('images/fishing_house.jpg')
gray_image = cv2.cvtColor(input_image, cv2.COLOR_BGR2GRAY)
# For version opencv < 3.0.0, use cv2.SIFT()
sift = cv2.xfeatures2d.SIFT_create()
keypoints = sift.detect(gray_image, None)
cv2.drawKeypoints(input_image, keypoints, input_image, \
flags = cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
cv2.imshow('SIFT features', input_image)
cv2.waitKey()
我们还可以计算描述符。 OpenCV 让我们分开进行操作,或者我们可以通过使用以下步骤在同一步骤中组合检测和计算部分:
keypoints, descriptors = sift.detectAndCompute(gray_image, None)
加速的强大特征(SURF)
即使 SIFT 很好且有用,但它的计算量很大。 这意味着它很慢,如果使用 SIFT,我们将很难实现实时系统。 我们需要一个快速且具有 SIFT 所有优点的系统。 如果您还记得,SIFT 使用高斯差分来构建金字塔,并且此过程很慢。 因此,为克服此问题,SURF 使用简单的盒式过滤器来*似高斯。 好消息是,这确实很容易计算,而且速度相当快。 SURF 上有很多在线文档。 因此,您可以遍历它以了解他们如何构造描述符。 您也可以在这个页面上参考原始论文。 重要的是要知道 SURF 也已获得专利,并且不能免费用于商业用途。
如果在较早的图像上运行 SURF 关键点检测器,您将看到类似以下内容:

这是代码:
import cv2
import numpy as np
input_image = cv2.imread('images/fishing_house.jpg')
gray_image = cv2.cvtColor(input_image, cv2.COLOR_BGR2GRAY)
# For version opencv < 3.0.0, use cv2.SURF()
surf = cv2.xfeatures2d.SURF_create()
# This threshold controls the number of keypoints
surf.setHessianThreshold(15000)
keypoints, descriptors = surf.detectAndCompute(gray_image, None)
cv2.drawKeypoints(input_image, keypoints, input_image, color=(0,255,0),\ flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
cv2.imshow('SURF features', input_image)
cv2.waitKey()
来自加速段测试的特征(FAST)
尽管 SURF 比 SIFT 快,但是对于实时系统而言,它还不够快,尤其是在存在资源限制的情况下。 当您在移动设备上构建实时应用时,您将无法享受使用 SURF 进行实时计算的奢华。 我们需要的是真正快速且计算便宜的东西。 因此,罗斯滕(Rosten)和德拉蒙德(Drummond)提出了 FAST。 顾名思义,它真的很快!
他们没有进行所有昂贵的计算,而是提出了一种高速测试来快速确定当前点是否是潜在的关键点。 我们需要注意,FAST 仅用于关键点检测。 一旦检测到关键点,就需要使用 SIFT 或 SURF 来计算描述符。 考虑下图:

如果我们在此图像上运行 FAST 关键点检测器,您将看到类似以下内容:

如果我们清理它并抑制不重要的关键点,它将看起来像这样:

以下是此代码:
import cv2
import numpy as np
input_image = cv2.imread('images/tool.png')
gray_image = cv2.cvtColor(input_image, cv2.COLOR_BGR2GRAY)
# Version under opencv 3.0.0 cv2.FastFeatureDetector()
fast = cv2.FastFeatureDetector_create()
# Detect keypoints
keypoints = fast.detect(gray_image, None)
print("Number of keypoints with non max suppression:", len(keypoints))
# Draw keypoints on top of the input image
img_keypoints_with_nonmax=input_image.copy()
cv2.drawKeypoints(input_image, keypoints, img_keypoints_with_nonmax, color=(0,255,0), \ flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
cv2.imshow('FAST keypoints - with non max suppression', img_keypoints_with_nonmax)
# Disable nonmaxSuppression
fast.setNonmaxSuppression(False)
# Detect keypoints again
keypoints = fast.detect(gray_image, None)
print("Total Keypoints without nonmaxSuppression:", len(keypoints))
# Draw keypoints on top of the input image
img_keypoints_without_nonmax=input_image.copy()
cv2.drawKeypoints(input_image, keypoints, img_keypoints_without_nonmax, color=(0,255,0), \ flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
cv2.imshow('FAST keypoints - without non max suppression', img_keypoints_without_nonmax)
cv2.waitKey()
二进制鲁棒的独立基本特征(BRIEF)
即使我们有 FAST 来快速检测关键点,我们仍然必须使用 SIFT 或 SURF 来计算描述符。 我们还需要一种快速计算描述符的方法。 这就是 BRIEF 出现的地方。 摘要是一种提取特征描述符的方法。 它不能单独检测关键点,因此我们需要将其与关键点检测器结合使用。 BRIEF 的好处是它紧凑且快速。
考虑下图:

BRIEF 获取输入关键点的列表并输出更新的列表。 因此,如果在此图像上运行“摘要”,您将看到类似以下内容:

以下是代码:
import cv2
import numpy as np
input_image = cv2.imread('images/house.jpg')
gray_image = cv2.cvtColor(input_image, cv2.COLOR_BGR2GRAY)
# Initiate FAST detector
fast = cv2.FastFeatureDetector_create()
# Initiate BRIEF extractor, before opencv 3.0.0 use cv2.DescriptorExtractor_create("BRIEF")
brief = cv2.xfeatures2d.BriefDescriptorExtractor_create()
# find the keypoints with STAR
keypoints = fast.detect(gray_image, None)
# compute the descriptors with BRIEF
keypoints, descriptors = brief.compute(gray_image, keypoints)
cv2.drawKeypoints(input_image, keypoints, input_image, color=(0,255,0))
cv2.imshow('BRIEF keypoints', input_image)
cv2.waitKey()
定向快速旋转 BRIEF(ORB)
因此,现在我们已经达到了到目前为止讨论的所有组合中的最佳组合。 该算法来自 OpenCV 实验室。 它是快速,强大且开源的! SIFT 和 SURF 算法均已获得专利,您不能将其用于商业目的; 这就是为什么 ORB 在许多方面都很好的原因。
如果在前面显示的图像之一上运行 ORB 关键点提取器,您将看到类似以下内容:

这是代码:
import cv2
import numpy as np
input_image = cv2.imread('images/fishing_house.jpg')
gray_image = cv2.cvtColor(input_image, cv2.COLOR_BGR2GRAY)
# Initiate ORB object, before opencv 3.0.0 use cv2.ORB()
orb = cv2.ORB_create()
# find the keypoints with ORB
keypoints = orb.detect(gray_image, None)
# compute the descriptors with ORB
keypoints, descriptors = orb.compute(gray_image, keypoints)
# draw only the location of the keypoints without size or orientation
cv2.drawKeypoints(input_image, keypoints, input_image, color=(0,255,0))
cv2.imshow('ORB keypoints', input_image)
cv2.waitKey()
总结
在本章中,我们了解了关键点的重要性以及为什么需要它们。 我们讨论了用于检测关键点和计算特征描述符的各种算法。 我们将在各种情况下的所有后续章节中使用这些算法。 关键点概念对于计算机视觉至关重要,并且在许多现代系统中都扮演着重要角色。
在下一章中,我们将讨论如何将同一场景的多个图像拼接在一起以创建全景图像。
六、接缝雕刻
在本章中,我们将学习有关内容感知的图像大小调整,这也称为接缝雕刻。 我们将讨论如何检测图像中有趣的部分,以及如何使用该信息调整给定图像的大小而不会降低这些有趣元素的质量。
在本章结束时,您将了解:
- 什么是内容感知
- 如何量化和识别图像中有趣的部分
- 如何使用动态规划进行图像内容分析
- 如何在保持高度不变的情况下增加和减小图像的宽度而又不使兴趣区域恶化
- 如何使对象从图像中消失
我们为什么要关心接缝雕刻?
在开始有关接缝雕刻的讨论之前,我们需要首先了解为什么需要接缝雕刻。 我们为什么要关心图像内容? 为什么我们不能只是调整给定的图像大小并继续生活呢? 好吧,要回答这些问题,让我们考虑下图:

现在,假设我们要减小图像的宽度,同时保持高度不变。 如果我们这样做,它将看起来像这样:

如您所见,图像中的鸭子看起来偏斜,并且图像的整体质量下降。 直观地说,可以说鸭子是图像中有趣的部分。 因此,当我们调整大小时,我们希望鸭子是完整的。 这是缝制雕刻出现的地方。 使用接缝雕刻,我们可以检测到这些有趣的区域,并确保它们不会退化。
它是如何工作的?
我们一直在讨论图像调整大小以及调整图像大小时应如何考虑图像的内容。 那么为什么在地球上将其称为接缝雕刻呢? 它应该只是称为内容感知图像调整大小,对吗? 嗯,有许多不同的术语可用来描述此过程,例如图像重新定向,液体缩放,接缝雕刻等。 由于我们调整图像大小的方式,因此将其称为“缝缝雕刻”。 该算法由 Shai Avidan 和 Ariel Shamir 提出。 您可以在这个页面上参考原始论文。
我们知道目标是调整给定图像的大小并保持完整有趣的内容。 因此,我们通过找到图像中最不重要的路径来做到这一点。 这些路径称为接缝。 找到这些接缝后,便从图像中删除或拉伸它们以获得重新缩放的图像。 移除,拉伸或雕刻的过程最终将导致图像调整大小。 这就是我们称其为接缝雕刻的原因。 考虑下图:

在上图中,我们可以看到如何将图像大致分为有趣和不有趣的部分。 我们需要确保我们的算法能够检测到这些无关紧要的部分并对其进行处理。 让我们考虑一下鸭子的形象和我们必须处理的约束。 我们要保持高度恒定并减小宽度。 这意味着我们需要在图像中找到垂直接缝并将其删除。 这些接缝从顶部开始,在底部结束(反之亦然)。 如果我们要处理垂直大小调整,则接缝将从左侧开始,到右侧结束。 垂直接缝只是从图像的第一行开始到最后一行结束的一连串相连像素。
我们如何定义有趣?
在开始计算接缝之前,我们需要找出用于计算接缝的度量标准。 我们需要一种将重要性分配给每个像素的方法,以便我们可以识别出最不重要的路径。 用计算机视觉术语来说,我们需要为每个像素分配一个能量值,以便我们找到最小能量的路径。 提出一种分配能量值的好方法非常重要,因为这会影响输出的质量。
我们可以使用的指标之一是每个点的导数值。 这是该社区活动水*的良好指标。 如果有活动,则像素值将快速变化,因此该点的导数值将很高。 另一方面,如果区域*淡无趣,那么像素值将不会迅速变化,因此灰度图像中该点的导数值将很低。
对于每个像素位置,我们通过累加该点的 x 和 y 导数来计算能量。 我们通过获取当前像素与其相邻像素之间的差来计算导数。 回想一下,在使用第 2 章,“检测边缘并应用图像过滤器”中的 sobel 过滤器进行边缘检测时,我们做了类似的操作。 一旦计算出这些值,便将它们存储在称为能量矩阵的矩阵中,该矩阵将用于定义接缝。
我们如何计算接缝?
现在我们有了能量矩阵,可以开始计算接缝了。 我们需要找到能量最少的图像路径。 计算所有可能的路径非常昂贵,因此我们需要找到一种更智能的方法来执行此操作。 这是动态规划出现的地方。 实际上,接缝雕刻是动态规划的直接应用。
我们需要从第一行中的每个像素开始,然后找到到达最后一行的方式。 为了找到能量最少的路径,我们计算并存储了到表中每个像素的最佳路径。 一旦我们构建了该表,就可以通过在该表中的行上回溯找到特定像素的路径。
对于当前行中的每个像素,我们计算下一行可以移动到的三个像素位置的能量; 即左下,右下和右下。 我们不断重复此过程,直到到达最低点。 一旦到达最低点,我们就会选择累积值最小的那根,然后回溯到最高点。 这将为我们提供最少的能量。 每次删除接缝时,图像的宽度都会减少一个像素。 因此,我们需要不断移除这些接缝,直到达到所需的图像尺寸为止。
首先,我们将提供一组函数来计算图像中的能量,定位其接缝并绘制它们。 这些函数将与前面的每个代码示例一起使用,并且可以作为库包含在您的任何自定义项中:
# Draw vertical seam on top of the image
def overlay_vertical_seam(img, seam):
img_seam_overlay = np.copy(img)
# Extract the list of points from the seam
x_coords, y_coords = np.transpose([(i,int(j)) for i,j in enumerate(seam)])
# Draw a green line on the image using the list of points
img_seam_overlay[x_coords, y_coords] = (0,255,0)
return img_seam_overlay
# Compute the energy matrix from the input image
def compute_energy_matrix(img):
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Compute X derivative of the image
sobel_x = cv2.Sobel(gray,cv2.CV_64F, 1, 0, ksize=3)
# Compute Y derivative of the image
sobel_y = cv2.Sobel(gray,cv2.CV_64F, 0, 1, ksize=3)
abs_sobel_x = cv2.convertScaleAbs(sobel_x)
abs_sobel_y = cv2.convertScaleAbs(sobel_y)
# Return weighted summation of the two images i.e. 0.5*X + 0.5*Y
return cv2.addWeighted(abs_sobel_x, 0.5, abs_sobel_y, 0.5, 0)
# Find vertical seam in the input image
def find_vertical_seam(img, energy):
rows, cols = img.shape[:2]
# Initialize the seam vector with 0 for each element
seam = np.zeros(img.shape[0])
# Initialize distance and edge matrices
dist_to = np.zeros(img.shape[:2]) + float('inf')
dist_to[0,:] = np.zeros(img.shape[1])
edge_to = np.zeros(img.shape[:2])
# Dynamic programming; iterate using double loop and compute the paths efficiently
for row in range(rows-1):
for col in range(cols):
if col != 0 and dist_to[row+1, col-1] > dist_to[row, col] + energy[row+1, col-1]:
dist_to[row+1, col-1] = dist_to[row, col] + energy[row+1, col-1]
edge_to[row+1, col-1] = 1
if dist_to[row+1, col] > dist_to[row, col] + energy[row+1, col]:
dist_to[row+1, col] = dist_to[row, col] + energy[row+1, col]
edge_to[row+1, col] = 0
if col != cols-1 and \
dist_to[row+1, col+1] > dist_to[row, col] + energy[row+1, col+1]:
dist_to[row+1, col+1] = dist_to[row, col] + energy[row+1, col+1]
edge_to[row+1, col+1] = -1
# Retracing the path
# Returns the indices of the minimum values along X axis.
seam[rows-1] = np.argmin(dist_to[rows-1, :])
for i in (x for x in reversed(range(rows)) if x > 0):
seam[i-1] = seam[i] + edge_to[i, int(seam[i])]
return seam
让我们再次考虑我们的鸭子形象。 如果我们计算前 30 个接缝,它将看起来像这样:

这些绿线表示最不重要的路径。 正如我们在这里看到的那样,它们会小心翼翼地绕过鸭子,以确保不会触碰到有趣的区域。 在图像的上半部分,接缝围绕着树枝缠绕,从而保持了质量。 从技术上讲,树枝也很有趣。 如果我们继续删除前 100 个接缝,它将看起来像这样:

现在,将其与简单调整大小的图像进行比较。 看起来不是更好吗? 鸭子在这个版本中看起来更好。
让我们看一下代码,看看如何做:
import sys
import cv2
import numpy as np
# Remove the input vertical seam from the image
def remove_vertical_seam(img, seam):
rows, cols = img.shape[:2]
# To delete a point, move every point after it one step towards the left
for row in range(rows):
for col in range(int(seam[row]), cols-1):
img[row, col] = img[row, col+1]
# Discard the last column to create the final output image
img = img[:, 0:cols-1]
return img
if __name__=='__main__':
# Make sure the size of the input image is reasonable.
# Large images take a lot of time to be processed.
# Recommended size is 640x480\.
img_input = cv2.imread(sys.argv[1])
# Use a small number to get started. Once you get an
# idea of the processing time, you can use a bigger number.
# To get started, you can set it to 20\.
num_seams = int(sys.argv[2])
img = np.copy(img_input)
img_overlay_seam = np.copy(img_input)
energy = compute_energy_matrix(img)
for i in range(num_seams):
seam = find_vertical_seam(img, energy)
img_overlay_seam = overlay_vertical_seam(img_overlay_seam, seam)
img = remove_vertical_seam(img, seam)
energy = compute_energy_matrix(img)
print('Number of seams removed = ', i+1)
cv2.imshow('Input', img_input)
cv2.imshow('Seams', img_overlay_seam)
cv2.imshow('Output', img)
cv2.waitKey()
我们使用remove_vertical_seam从原始图像中去除垂直接缝,从而减小了图像的宽度,但保留了有趣的部分。
我们可以扩大图像吗?
我们知道,我们可以使用接缝雕刻来减小图像的宽度,而不会降低有趣的区域。 因此,自然地,我们需要问自己是否可以扩展图像而不会破坏有趣的区域。 事实证明,我们可以使用相同的逻辑来做到这一点。 在计算接缝时,我们只需要添加一列而不是删除一列。
如果我们简单地扩大鸭子的形象,它将看起来像这样:

如果我们以更聪明的方式(即通过使用接缝雕刻)来进行操作,它将看起来像这样:

如您所见,图像的宽度增加了,鸭子看起来没有拉长。 以下是执行此操作的代码:
import sys
import cv2
import numpy as np
# Add a vertical seam to the image
def add_vertical_seam(img, seam, num_iter):
seam = seam + num_iter
rows, cols = img.shape[:2]
zero_col_mat = np.zeros((rows,1,3), dtype=np.uint8)
img_extended = np.hstack((img, zero_col_mat))
for row in range(rows):
for col in range(cols, int(seam[row]), -1):
img_extended[row, col] = img[row, col-1]
# To insert a value between two columns, take the average
# value of the neighbors. It looks smooth this way and we
# can avoid unwanted artifacts.
for i in range(3):
v1 = img_extended[row, int(seam[row])-1, i]
v2 = img_extended[row, int(seam[row])+1, i]
img_extended[row, int(seam[row]), i] = (int(v1)+int(v2))/2
return img_extended
if __name__=='__main__':
img_input = cv2.imread(sys.argv[1])
num_seams = int(sys.argv[2])
img = np.copy(img_input)
img_output = np.copy(img_input)
img_overlay_seam = np.copy(img_input)
energy = compute_energy_matrix(img) # Same than previous code sample
for i in range(num_seams):
seam = find_vertical_seam(img, energy) # Same than previous code sample
img_overlay_seam = overlay_vertical_seam(img_overlay_seam, seam)
img = remove_vertical_seam(img, seam) # Same than previous code sample
img_output = add_vertical_seam(img_output, seam, i)
energy = compute_energy_matrix(img)
print('Number of seams added =', i+1)
cv2.imshow('Input', img_input)
cv2.imshow('Seams', img_overlay_seam)
cv2.imshow('Output', img_output)
cv2.waitKey()
在这种情况下,我们在代码中添加了一个额外的函数add_vertical_seam。 我们使用它来添加垂直接缝,以使图像看起来自然,在不改变有趣区域原始比例的情况下,向图像添加接缝以增加其宽度。
我们可以完全去除对象吗?
这也许是接缝雕刻最有趣的应用。 我们可以使物体从图像中完全消失。 让我们考虑下图:

封闭要使用鼠标删除的区域:

卸下右侧的椅子后,它将看起来像这样:

好像椅子根本不存在! 在查看代码之前,重要的是要知道这需要一段时间才能运行。 因此,只需等待几分钟即可了解处理时间。 您可以相应地调整输入图像的尺寸! 让我们看一下代码:
import sys
import cv2
import numpy as np
# Draw rectangle on top of the input image
def draw_rectangle(event, x, y, flags, params):
global x_init, y_init, drawing, top_left_pt, bottom_right_pt, img_orig
# Detecting a mouse click
if event == cv2.EVENT_LBUTTONDOWN:
drawing = True
x_init, y_init = x, y
# Detecting mouse movement
elif event == cv2.EVENT_MOUSEMOVE:
if drawing:
top_left_pt, bottom_right_pt = (x_init,y_init), (x,y)
img[y_init:y, x_init:x] = 255 - img_orig[y_init:y, x_init:x]
cv2.rectangle(img, top_left_pt, bottom_right_pt, (0,255,0), 2)
# Detecting the mouse button up event
elif event == cv2.EVENT_LBUTTONUP:
drawing = False
top_left_pt, bottom_right_pt = (x_init,y_init), (x,y)
# Create the "negative" film effect for the selected
# region
img[y_init:y, x_init:x] = 255 - img[y_init:y, x_init:x]
# Draw rectangle around the selected region
cv2.rectangle(img, top_left_pt, bottom_right_pt, (0,255,0), 2)
rect_final = (x_init, y_init, x-x_init, y-y_init)
# Remove the object in the selected region
remove_object(img_orig, rect_final)
# Computing the energy matrix using modified algorithm
def compute_energy_matrix_modified(img, rect_roi):
# Compute weighted summation i.e. 0.5*X + 0.5*Y
energy_matrix = compute_energy_matrix(img)
x,y,w,h = rect_roi
# We want the seams to pass through this region, so make sure the energy values in this region are set to 0
energy_matrix[y:y+h, x:x+w] = 0
return energy_matrix
# Remove the object from the input region of interest
def remove_object(img, rect_roi):
num_seams = rect_roi[2] + 10
energy = compute_energy_matrix_modified(img, rect_roi)
# Start a loop and rsemove one seam at a time
for i in range(num_seams):
# Find the vertical seam that can be removed
seam = find_vertical_seam(img, energy)
# Remove that vertical seam
img = remove_vertical_seam(img, seam)
x,y,w,h = rect_roi
# Compute energy matrix after removing the seam
energy = compute_energy_matrix_modified(img, (x,y,w-i,h))
print('Number of seams removed =', i+1)
img_output = np.copy(img)
# Fill up the region with surrounding values so that the size
# of the image remains unchanged
for i in range(num_seams):
seam = find_vertical_seam(img, energy)
img = remove_vertical_seam(img, seam)
img_output = add_vertical_seam(img_output, seam, i)
energy = compute_energy_matrix(img)
print('Number of seams added =', i+1)
cv2.imshow('Input', img_input)
cv2.imshow('Output', img_output)
cv2.waitKey()
if __name__=='__main__':
img_input = cv2.imread(sys.argv[1])
drawing = False
img = np.copy(img_input)
img_orig = np.copy(img_input)
cv2.namedWindow('Input')
cv2.setMouseCallback('Input', draw_rectangle)
print('Draw a rectangle with the mouse over the object to be removed')
while True:
cv2.imshow('Input', img)
c = cv2.waitKey(10)
if c == 27:
break
cv2.destroyAllWindows()
我们是怎么做的?
这里的基本逻辑保持不变。 我们正在使用接缝雕刻来移除对象。 一旦选择了兴趣区域,我们就使所有接缝都穿过该区域。 我们通过在每次迭代后操纵能量矩阵来做到这一点。 我们添加了一个名为compute_energy_matrix_modified的新函数来实现此目的。 一旦我们计算了能量矩阵,便将零值分配给该兴趣区域。 这样,我们强制所有接缝穿过该区域。 删除与该区域相关的所有接缝后,我们将继续添加接缝,直到将图像扩展到其原始宽度为止。
总结
在本章中,我们了解了内容感知图像的大小调整。 我们讨论了如何量化图像中有趣和无趣的区域。 我们学习了如何计算图像中的接缝,以及如何使用动态规划有效地进行处理。 我们讨论了如何使用接缝雕刻来减小图像的宽度,以及如何使用相同的逻辑来扩展图像。 我们还学习了如何从图像中完全删除对象。
在下一章中,我们将讨论如何进行形状分析和图像分割。 我们将看到如何使用这些原理来找到图像中感兴趣对象的确切边界。
七、检测形状和分割图像
在本章中,我们将学习形状分析和图像分割。 我们将学习如何识别形状并估计确切边界。 我们将讨论如何使用各种方法将图像分割成其组成部分。 我们还将学习如何将前景与背景分开。
在本章结束时,您将了解:
- 什么是轮廓分析和形状匹配
- 如何匹配形状
- 什么是图像分割
- 如何将图像分割成其组成部分
- 如何将前景与背景分开
- 如何使用各种技术分割图像
轮廓分析和形状匹配
轮廓分析是计算机视觉领域中非常有用的工具。 我们处理现实世界中的许多形状,轮廓分析有助于使用各种算法分析这些形状。 当我们将图像转换为灰度并对其进行阈值处理时,我们会留下一堆线条和轮廓。 一旦了解了不同形状的属性,便可以从图像中提取详细信息。
假设我们要在下图中标识飞旋镖形状:

为此,我们首先需要了解常规回旋镖的外观:

现在,以前面的图像为参考,我们是否可以识别原始图像中与回旋镖相对应的形状? 如果您注意到,我们不能使用简单的基于相关性的方法,因为形状都会变形。 这意味着我们寻找精确匹配的方法几乎行不通! 我们需要了解形状的特征并匹配相应的特征以识别飞旋镖形状。 OpenCV 提供了几个形状匹配器工具,我们可以使用它们来实现此目的。 如果您想了解更多信息,请访问这里了解更多信息。
匹配基于胡矩的概念,而后者又与图像矩有关。 您可以参考以下论文以了解有关矩的更多信息。 图像矩的概念基本上是指形状内像素的加权和乘幂加和。

在上式中,p表示轮廓内的像素,w表示权重,N表示轮廓内的点数,k表示功率,I表示矩。 根据我们为w和k选择的值,我们可以从该轮廓提取不同的特征。
也许最简单的例子是计算轮廓的面积。 为此,我们需要计算该区域内的像素数。 因此,从数学上讲,在加权和加幂求和形式中,我们只需要将w设置为 1,将k设置为零。 这将为我们提供轮廓区域。 根据我们如何计算这些矩,它们将帮助我们理解这些不同的形状。 这也产生了一些有趣的属性,可以帮助我们确定形状相似度。
如果我们匹配形状,您将看到类似以下内容:

让我们看一下执行此操作的代码:
import cv2
import numpy as np
# Extract all the contours from the image
def get_all_contours(img):
ref_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(ref_gray, 127, 255, 0)
# Find all the contours in the thresholded image. The values
# for the second and third parameters are restricted to a
# certain number of possible values.
im2, contours, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_LIST, \
cv2.CHAIN_APPROX_SIMPLE )
return contours
# Extract reference contour from the image
def get_ref_contour(img):
contours = get_all_contours(img)
# Extract the relevant contour based on area ratio. We use the
# area ratio because the main image boundary contour is
# extracted as well and we don't want that. This area ratio
# threshold will ensure that we only take the contour inside the image.
for contour in contours:
area = cv2.contourArea(contour)
img_area = img.shape[0] * img.shape[1]
if 0.05 < area/float(img_area) < 0.8:
return contour
if __name__=='__main__':
# Boomerang reference image
img1 = cv2.imread(sys.argv[1])
# Input image containing all the different shapes
img2 = cv2.imread(sys.argv[2])
# Extract the reference contour
ref_contour = get_ref_contour(img1)
# Extract all the contours from the input image
input_contours = get_all_contours(img2)
closest_contour = None
min_dist = None
contour_img = img2.copy()
cv2.drawContours(contour_img, input_contours, -1, color=(0,0,0), thickness=3)
cv2.imshow('Contours', contour_img)
# Finding the closest contour
for contour in input_contours:
# Matching the shapes and taking the closest one using
# Comparison method CV_CONTOURS_MATCH_I3 (second argument)
ret = cv2.matchShapes(ref_contour, contour, 3, 0.0)
print("Contour %d matchs in %f" % (i, ret))
if min_dist is None or ret < min_dist:
min_dist = ret
closest_contour = contour
cv2.drawContours(img2, [closest_contour], 0 , color=(0,0,0), thickness=3)
cv2.imshow('Best Matching', img2)
cv2.waitKey()
matchShapes方法的使用可能与胡矩不变量(CV_CONTOUR_MATCH_I1,2,3)不同,后者由于轮廓的大小,方向或旋转而可能产生不同的最佳匹配形状。 要了解更多信息,可以在这个页面上查看官方文档。
*似轮廓
我们在现实生活中遇到的许多轮廓都很嘈杂。 这意味着轮廓看起来不*滑,因此我们的分析受到了打击。 那么,我们该如何处理呢? 一种解决方法是获取轮廓上的所有点,然后使用*滑多边形对其进行*似。
让我们再次考虑飞旋镖的形象。 如果使用各种阈值*似轮廓,则会看到轮廓改变其形状。 让我们从 0.05 开始:

如果减小此因子,轮廓将变得更*滑。 让我们使其为 0.01:

如果您将其缩小,例如说 0.00001,那么它将看起来像原始图像:

以下代码表示如何将这些轮廓转换为多边形的*似*滑化:
import sys
import cv2
import numpy as np
if __name__=='__main__':
# Input image containing all the different shapes
img1 = cv2.imread(sys.argv[1])
# Extract all the contours from the input image
input_contours = get_all_contours(img1)
contour_img = img1.copy()
smoothen_contours = []
factor = 0.05
# Finding the closest contour
for contour in input_contours:
epsilon = factor * cv2.arcLength(contour, True)
smoothen_contours.append(cv2.approxPolyDP(contour, epsilon, True))
cv2.drawContours(contour_img, smoothen_contours, -1, color=(0,0,0), thickness=3)
cv2.imshow('Contours', contour_img)
cv2.waitKey()
识别出一片突出的披萨
标题可能会引起误导,因为我们不会谈论披萨片。 但是,假设您所处的图像包含不同类型的不同形状的比萨饼。 现在,有人从其中一个比萨饼中切出一片。 我们如何自动识别这一点?
我们无法采用之前采用的方法,因为我们不知道形状是什么样子,因此我们没有任何模板。 我们甚至不确定我们要寻找的形状,因此我们无法基于任何先验信息构建模板。 我们所知道的是从一个比萨饼上切下一片的事实。 让我们考虑下图:

这不完全是真实的图像,但是您可以理解。 你知道我们在谈论什么形状。 由于我们不知道要寻找的是什么,因此需要使用这些形状的某些属性来识别切片的比萨饼。 如果您注意到,所有其他形状都很好地封闭了; 也就是说,您可以在这些形状中选取任意两个点并在它们之间画一条线,并且该线将始终位于该形状之内。 这些形状称为凸形。
如果您查看切片的比萨饼形状,我们可以选择两个点,使它们之间的线超出形状,如下图所示:

因此,我们要做的就是检测图像中的非凸形状,然后就可以完成。 让我们继续这样做:
import sys
import cv2
import numpy as np
if __name__=='__main__':
img = cv2.imread(sys.argv[1])
# Iterate over the extracted contours
# Using previous get_all_contours() method
for contour in get_all_contours(img):
# Extract convex hull from the contour
hull = cv2.convexHull(contour, returnPoints=False)
# Extract convexity defects from the above hull
# Being a convexity defect the cavities in the hull segments
defects = cv2.convexityDefects(contour, hull)
if defects is None:
continue
# Draw lines and circles to show the defects
for i in range(defects.shape[0]):
start_defect, end_defect, far_defect, _ = defects[i,0]
start = tuple(contour[start_defect][0])
end = tuple(contour[end_defect][0])
far = tuple(contour[far_defect][0])
cv2.circle(img, far, 5, [128,0,0], -1)
cv2.drawContours(img, [contour], -1, (0,0,0), 3)
cv2.imshow('Convexity defects',img)
cv2.waitKey(0)
cv2.destroyAllWindows()
要了解有关convexityDefects工作原理的更多信息,请访问这里。
如果运行前面的代码,您将看到类似以下内容:

等一下,这是怎么回事? 看起来很混乱。 我们做错了吗? 事实证明,曲线并不是很*滑。 如果仔细观察,曲线上到处都有细小的山脊。 因此,如果仅运行凸度检测器,它将无法正常工作。
这是轮廓*似非常有用的地方。 一旦检测到轮廓,就需要对其进行*滑处理,以免脊不影响它们。 让我们继续这样做:
factor = 0.01
epsilon = factor * cv2.arcLength(contour, True)
contour = cv2.approxPolyDP(contour, epsilon, True)
如果使用*滑轮廓运行前面的代码,则输出将如下所示:

如何检查形状?
假设您正在处理图像,并且想要遮挡特定的形状。 现在,您可能会说您将使用形状匹配来识别形状,然后将其屏蔽掉,对吗? 但是这里的问题是我们没有可用的模板。 那么,我们如何去做呢? 形状分析有多种形式,我们需要根据情况构建算法。 让我们考虑下图:

假设我们要识别所有回旋镖形状,然后不使用任何模板图像就将它们遮挡掉。 如您所见,该图像中还有其他各种怪异的形状,而飞旋镖形状并不是很*滑。 我们需要确定将飞旋镖形状与当前其他形状区分开的属性。 让我们考虑凸包。 如果采用每种形状的面积与凸包的面积之比,我们可以看到这可以作为区别指标。 该度量在形状分析中称为坚固性因子。 该度量标准对于回旋镖形状而言具有较低的值,因为将留出空白区域,如下图所示:

黑色边界代表凸包。 一旦为所有形状计算了这些值,我们如何将它们分开? 我们可以仅使用固定的阈值来检测回旋镖形状吗? 并不是的! 我们无法使用固定的阈值,因为您永远不知道以后会遇到哪种形状。 因此,更好的方法是使用 K 均值聚类。 K 均值是一种无监督的学习技术,可用于将输入数据分离为 K 类。 在继续之前,您可以在这里快速掌握 K 均值。
我们知道我们想将形状分为两组,即回旋镖形状和其他形状。 因此,我们知道 K 均值中的K是什么。 一旦使用该值并对值进行聚类,我们将选择具有最低实体因子的聚类,这将为我们提供回旋镖形状。 请记住,这种方法仅在特定情况下有效。 如果要处理其他类型的形状,则必须使用其他指标来确保形状检测有效。 正如我们前面所讨论的,这在很大程度上取决于情况。 如果检测到形状并将其屏蔽掉,它将看起来像这样:

以下是执行此操作的代码:
import sys
import cv2
import numpy as np
if __name__=='__main__':
# Input image containing all the shapes
img = cv2.imread(sys.argv[1])
img_orig = np.copy(img)
input_contours = get_all_contours(img)
solidity_values = []
# Compute solidity factors of all the contours
for contour in input_contours:
area_contour = cv2.contourArea(contour)
convex_hull = cv2.convexHull(contour)
area_hull = cv2.contourArea(convex_hull)
solidity = float(area_contour)/area_hull
solidity_values.append(solidity)
# Clustering using KMeans
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
flags = cv2.KMEANS_RANDOM_CENTERS
solidity_values = \ np.array(solidity_values).reshape((len(solidity_values),1)).astype('float32')
compactness, labels, centers = cv2.kmeans(solidity_values, 2, None, criteria, 10, flags)
closest_class = np.argmin(centers)
output_contours = []
for i in solidity_values[labels==closest_class]:
index = np.where(solidity_values==i)[0][0]
output_contours.append(input_contours[index])
cv2.drawContours(img, output_contours, -1, (0,0,0), 3)
cv2.imshow('Output', img)
# Censoring
for contour in output_contours:
rect = cv2.minAreaRect(contour)
box = cv2.boxPoints(rect)
box = np.int0(box)
cv2.drawContours(img_orig, [box], 0, (0,0,0), -1)
cv2.imshow('Censored', img_orig)
cv2.waitKey()
什么是图像分割?
图像分割是将图像分成其组成部分的过程。 这是现实世界中许多计算机视觉应用中的重要一步。 分割图像有很多不同的方法。 分割图像时,我们会根据各种指标(例如颜色,纹理,位置等)将区域分开。 每个区域内的所有像素都有一些共同点,具体取决于我们使用的指标。 让我们看看这里的一些流行方法。
首先,我们将研究一种称为 GrabCut 的技术。 这是基于称为图切割的更通用方法的图像分割方法。 在图切方法中,我们将整个图像视为一个图,然后根据该图边缘的强度对图进行分段。 我们通过将每个像素视为一个节点来构造图,并在节点之间构造边缘,其中边缘权重是这两个节点的像素值的函数。 只要有边界,像素值就会更高。 因此,边缘权重也将更高。 然后通过最小化图的吉布斯能量来对该图进行分段。 这类似于找到最大熵分割。 您可以在这个页面上参考原始论文以了解更多信息。
让我们考虑下图:

让我们选择兴趣区域:

分割图像后,它将看起来像这样:

以下是执行此操作的代码:
import sys
import cv2
import numpy as np
# Draw rectangle based on the input selection
def draw_rectangle(event, x, y, flags, params):
global x_init, y_init, drawing, top_left_pt, bottom_right_pt, img_orig
# Detecting mouse button down event
if event == cv2.EVENT_LBUTTONDOWN:
drawing = True
x_init, y_init = x, y
# Detecting mouse movement
elif event == cv2.EVENT_MOUSEMOVE:
if drawing:
top_left_pt, bottom_right_pt = (x_init,y_init), (x,y)
img[y_init:y, x_init:x] = 255 - img_orig[y_init:y, x_init:x]
cv2.rectangle(img, top_left_pt, bottom_right_pt, (0,255,0), 2)
# Detecting mouse button up event
elif event == cv2.EVENT_LBUTTONUP:
drawing = False
top_left_pt, bottom_right_pt = (x_init,y_init), (x,y)
img[y_init:y, x_init:x] = 255 - img[y_init:y, x_init:x]
cv2.rectangle(img, top_left_pt, bottom_right_pt, (0,255,0), 2)
rect_final = (x_init, y_init, x-x_init, y-y_init)
# Run Grabcut on the region of interest
run_grabcut(img_orig, rect_final)
# Grabcut algorithm
def run_grabcut(img_orig, rect_final):
# Initialize the mask
mask = np.zeros(img_orig.shape[:2],np.uint8)
# Extract the rectangle and set the region of
# interest in the above mask
x,y,w,h = rect_final
mask[y:y+h, x:x+w] = 1
# Initialize background and foreground models
bgdModel = np.zeros((1,65), np.float64)
fgdModel = np.zeros((1,65), np.float64)
# Run Grabcut algorithm
cv2.grabCut(img_orig, mask, rect_final, bgdModel, fgdModel, 5, cv2.GC_INIT_WITH_RECT)
# Extract new mask
mask2 = np.where((mask==2)|(mask==0),0,1).astype('uint8')
# Apply the above mask to the image
img_orig = img_orig*mask2[:,:,np.newaxis]
# Display the image
cv2.imshow('Output', img_orig)
if __name__=='__main__':
drawing = False
top_left_pt, bottom_right_pt = (-1,-1), (-1,-1)
# Read the input image
img_orig = cv2.imread(sys.argv[1])
img = img_orig.copy()
cv2.namedWindow('Input')
cv2.setMouseCallback('Input', draw_rectangle)
while True:
cv2.imshow('Input', img)
c = cv2.waitKey(1)
if c == 27:
break
cv2.destroyAllWindows()
它是如何工作的?
我们从用户指定的种子点开始。 这是我们有兴趣的对象的边界框。 该算法在表面之下估计对象和背景的颜色分布。 该算法将图像的颜色分布表示为高斯混合马尔可夫随机场(GMMRF)。 您可以在这个页面上参考详细的文章以了解有关 GMMRF 的更多信息。 我们需要对象和背景的颜色分布,因为我们将使用此知识来分离对象。 通过将最小割算法应用于 Markov 随机场,此信息可用于找到最大熵分割。 一旦有了这个,我们就可以使用图切割优化方法来推断标签。
分水岭算法
OpenCV 随附分水岭算法的默认实现,该理论认为,任何灰度图像都可以被视为地形表面,高强度表示山峰和丘陵,而低强度表示山谷。 该算法非常有名,并且有很多实现方式。
考虑下图:

让我们根据其地形表面选择区域:

如果对此运行分水岭算法,输出将如下所示:

可以在前面给出的链接上找到示例代码,以及分水岭算法的许多其他应用。
总结
在本章中,我们学习了轮廓分析和图像分割。 我们学习了如何根据模板匹配形状。 我们了解了形状的各种不同属性,以及如何使用它们识别不同种类的形状。 我们讨论了图像分割以及如何使用基于图的方法对图像中的区域进行分割。 我们还简要讨论了分水岭的改造。
在下一章中,我们将讨论如何在实时视频中跟踪对象。
八、对象跟踪
在本章中,我们将学习有关在实时视频中跟踪对象的信息。 我们将讨论可用于跟踪对象的不同特征。 我们还将学习有关对象跟踪的不同方法和技术。
在本章结束时,您将了解:
- 如何使用帧差异
- 如何使用色彩空间跟踪有色对象
- 如何构建交互式对象跟踪器
- 如何构建特征跟踪器
- 如何建立视频监控系统
帧差异
这可能是我们可以用来查看视频的哪些部分正在移动的最简单的技术。 当我们考虑实时视频流时,连续帧之间的差异为我们提供了很多信息。 这个概念非常简单! 我们只求连续帧之间的差异并显示差异。
如果我们从左向右快速移动,我们将看到类似以下内容:

从上一张图像可以看到,只有视频中的移动部分被突出显示。 这为我们提供了一个很好的起点,以了解视频中哪些区域正在移动。 这是执行此操作的代码:
import cv2
# Compute the frame difference
def frame_diff(prev_frame, cur_frame, next_frame):
# Absolute difference between current frame and next frame
diff_frames1 = cv2.absdiff(next_frame, cur_frame)
# Absolute difference between current frame and
# previous frame
diff_frames2 = cv2.absdiff(cur_frame, prev_frame)
# Return the result of bitwise 'AND' between the
# above two resultant images to obtain a mask where
# only the areas with white pixels are shown
return cv2.bitwise_and(diff_frames1, diff_frames2)
# Capture the frame from webcam
def get_frame(cap, scaling_factor):
# Capture the frame
ret, frame = cap.read()
# Resize the image
frame = cv2.resize(frame, None, fx=scaling_factor,
fy=scaling_factor, interpolation=cv2.INTER_AREA)
return frame
if __name__=='__main__':
cap = cv2.VideoCapture(0)
scaling_factor = 0.5
cur_frame, prev_frame, next_frame = None, None, None
while True:
frame = get_frame(cap, scaling_factor)
prev_frame = cur_frame
cur_frame = next_frame
# Convert frame to grayscale image
next_frame = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
if prev_frame is not None:
cv2.imshow("Object Movement", frame_diff(prev_frame, cur_frame, next_frame))
key = cv2.waitKey(delay=10)
if key == 27:
break
cv2.destroyAllWindows()
已使用 10 毫秒的延迟来使帧之间有足够的时间来产生实际的显着差异。
基于色彩空间的跟踪
帧差异为我们提供了一些有用的信息,但是我们不能使用它来构建有意义的任何东西。 为了构建一个好的对象跟踪器,我们需要了解可以使用哪些特征来使我们的跟踪功能强大而准确。 因此,让我们朝这个方向迈出一步,看看如何使用颜色空间提出一个好的跟踪器。 正如我们在前几章中讨论的那样,当涉及到人类感知时, HSV 颜色空间非常有用。 我们可以将图像转换为 HSV 空间,然后使用颜色空间阈值跟踪给定的对象。
考虑视频中的以下帧:

如果通过颜色空间过滤器运行它并跟踪对象,则会看到以下内容:

就像我们在这里看到的那样,我们的跟踪器会根据颜色特征识别视频中的特定对象。 为了使用此跟踪器,我们需要知道目标对象的颜色分布。 以下是代码:
import cv2
import numpy as np
if __name__=='__main__':
cap = cv2.VideoCapture(0)
scaling_factor = 0.5
# Define 'blue' range in HSV color space
lower = np.array([60,100,100])
upper = np.array([180,255,255])
while True:
frame = get_frame(cap, scaling_factor)
# Convert the HSV color space
hsv_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
# Threshold the HSV image to get only blue color
mask = cv2.inRange(hsv_frame, lower, upper)
# Bitwise-AND mask and original image
res = cv2.bitwise_and(frame, frame, mask=mask)
res = cv2.medianBlur(res, ksize=5)
cv2.imshow('Original image', frame)
cv2.imshow('Color Detector', res)
# Check if the user pressed ESC key
c = cv2.waitKey(delay=10)
if c == 27:
break
cv2.destroyAllWindows()
构建一个交互式对象跟踪器
基于颜色空间的跟踪器使我们可以自由跟踪有色对象,但是我们也只能使用预定义的颜色。 如果我们只想随机选择一个对象怎么办? 我们如何构建一个对象跟踪器,以了解所选对象的特征并自动跟踪它? 这就是 CAMShift 算法的意思,它代表连续自适应均值偏移。 它基本上是 Meanshift 算法的改进版本。
Meanshift 的概念实际上很简单。 假设我们选择了一个兴趣区域,并且希望我们的对象跟踪器跟踪该对象。 在该区域中,我们基于颜色直方图选择一堆点并计算质心。 如果质心位于该区域的中心,则说明该对象没有移动。 但是,如果质心不在此区域的中心,那么我们知道对象正在朝某个方向移动。 重心的移动控制对象移动的方向。 因此,我们将边界框移动到新位置,以便新质心成为此边界框的中心。 因此,此算法称为均值移位,因为均值(即质心)正在移动。 这样,我们就可以使用对象的当前位置进行更新。
但是,Meanshift 的问题在于不允许更改边界框的大小。 当您将物体移离相机时,人眼看起来物体会变小,但是 Meanshift 不会考虑这一点。 在整个跟踪会话中,边界框的大小将保持不变。 因此,我们需要使用 CAMShift。 CAMShift 的优点在于,它可以使边界框的大小适合对象的大小。 除此之外,它还可以跟踪对象的方向。
让我们考虑以下框架,其中对象以橙色突出显示(我手中的框):

现在我们已经选择了对象,该算法将计算直方图反投影并提取所有信息。 让我们移动对象,看看它是如何被跟踪的:

看起来该对象已被很好地跟踪。 让我们更改方向,看看是否保持跟踪:

如我们所见,边界椭圆改变了它的位置和方向。 让我们更改对象的视角,看看它是否仍然可以跟踪它:

我们还是很好! 边界椭圆更改了宽高比,以反映对象现在看起来偏斜的事实(由于透视变换)。
以下是代码:
import sys
import cv2
import numpy as np
class ObjectTracker():
def __init__(self):
# Initialize the video capture object
# 0 -> indicates that frame should be captured
# from webcam
self.cap = cv2.VideoCapture(0)
# Capture the frame from the webcam
ret, self.frame = self.cap.read()
# Downsampling factor for the input frame
self.scaling_factor = 0.8
self.frame = cv2.resize(self.frame, None, fx=self.scaling_factor, fy=self.scaling_factor, interpolation=cv2.INTER_AREA)
cv2.namedWindow('Object Tracker')
cv2.setMouseCallback('Object Tracker', self.mouse_event)
self.selection = None
self.drag_start = None
self.tracking_state = 0
# Method to track mouse events
def mouse_event(self, event, x, y, flags, param):
x, y = np.int16([x, y])
# Detecting the mouse button down event
if event == cv2.EVENT_LBUTTONDOWN:
self.drag_start = (x, y)
self.tracking_state = 0
if self.drag_start:
if event == cv2.EVENT_MOUSEMOVE:
h, w = self.frame.shape[:2]
xo, yo = self.drag_start
x0, y0 = np.maximum(0, np.minimum([xo, yo], [x, y]))
x1, y1 = np.minimum([w, h], np.maximum([xo, yo], [x, y]))
self.selection = None
if x1-x0 > 0 and y1-y0 > 0:
self.selection = (x0, y0, x1, y1)
elif event == cv2.EVENT_LBUTTONUP:
self.drag_start = None
if self.selection is not None:
self.tracking_state = 1
# Method to start tracking the object
def start_tracking(self):
# Iterate until the user presses the Esc key
while True:
# Capture the frame from webcam
ret, self.frame = self.cap.read()
# Resize the input frame
self.frame = cv2.resize(self.frame, None, fx=self.scaling_factor, fy=self.scaling_factor, interpolation=cv2.INTER_AREA)
vis = self.frame.copy()
# Convert to HSV color space
hsv = cv2.cvtColor(self.frame, cv2.COLOR_BGR2HSV)
# Create the mask based on predefined thresholds.
mask = cv2.inRange(hsv, np.array((0., 60., 32.)), np.array((180., 255., 255.)))
if self.selection:
x0, y0, x1, y1 = self.selection
self.track_window = (x0, y0, x1-x0, y1-y0)
hsv_roi = hsv[y0:y1, x0:x1]
mask_roi = mask[y0:y1, x0:x1]
# Compute the histogram
hist = cv2.calcHist( [hsv_roi], [0], mask_roi, [16], [0, 180] )
# Normalize and reshape the histogram
cv2.normalize(hist, hist, 0, 255, cv2.NORM_MINMAX);
self.hist = hist.reshape(-1)
vis_roi = vis[y0:y1, x0:x1]
cv2.bitwise_not(vis_roi, vis_roi)
vis[mask == 0] = 0
if self.tracking_state == 1:
self.selection = None
# Compute the histogram back projection
prob = cv2.calcBackProject([hsv], [0], self.hist, [0, 180], 1)
prob &= mask
term_crit = ( cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 1 )
# Apply CAMShift on 'prob'
track_box, self.track_window = cv2.CamShift(prob, self.track_window, term_crit)
# Draw an ellipse around the object
cv2.ellipse(vis, track_box, (0, 255, 0), 2)
cv2.imshow('Object Tracker', vis)
c = cv2.waitKey(delay=5)
if c == 27:
break
cv2.destroyAllWindows()
if __name__ == '__main__':
ObjectTracker().start_tracking()
基于特征的跟踪
基于特征的跟踪是指跟踪视频中连续帧中的各个特征点。 我们使用一种称为光流的技术来跟踪这些特征。 光流是计算机视觉中最流行的技术之一。 我们选择了一堆特征点并通过视频流对其进行跟踪。
当我们检测到特征点时,我们将计算位移向量并显示这些关键点在连续帧之间的运动。 这些向量称为运动向量。 有很多方法可以做到这一点,但是卢卡斯-卡纳德方法也许是所有这些方法中最流行的方法。 您可以在 OpenCV 官方文档中了解更多信息。
我们通过提取特征点开始该过程。 对于每个特征点,我们以特征点为中心创建3x3色块。 这里的假设是每个面片内的所有点都将具有相似的运动。 我们可以根据眼前的问题来调整此窗口的大小。
对于当前帧中的每个特征点,我们将周围的3x3面片作为参考点。 对于此补丁,我们在前一帧中查看其邻域以获得最佳匹配。 该邻域通常大于3x3,因为我们要获取最接*所考虑补丁的补丁。 现在,从前一帧中匹配的补丁的中心像素到当前帧中正在考虑的补丁的中心像素的路径将成为运动向量。 我们对所有特征点都这样做,并提取所有运动向量。
让我们考虑以下框架:

如果我沿水*方向移动,您将看到沿水*方向的运动向量:

如果我离开网络摄像头,您将看到以下内容:

首先,我们将实现一个函数,以从给定图像中提取特征点,以使用前一帧获取运动向量:
def compute_feature_points(tracking_paths, prev_img, current_img):
feature_points = [tp[-1] for tp in tracking_paths]
# Vector of 2D points for which the flow needs to be found
feature_points_0 = np.float32(feature_points).reshape(-1, 1, 2)
feature_points_1, status_1, err_1 = cv2.calcOpticalFlowPyrLK(prev_img, current_img, \
feature_points_0, None, **tracking_params)
feature_points_0_rev, status_2, err_2 = cv2.calcOpticalFlowPyrLK(current_img, prev_img, \
feature_points_1, None, **tracking_params)
# Compute the difference of the feature points
diff_feature_points = abs(feature_points_0-feature_points_0_rev).reshape(-1, 2).max(-1)
# threshold and keep only the good points
good_points = diff_feature_points < 1
return feature_points_1.reshape(-1, 2), good_points
现在,我们可以实现一种跟踪方法,其中给定获得的兴趣区域,并基于通过上述方法获得的特征点,我们可以显示运动向量(跟踪路径):
# Extract area of interest based on the tracking_paths
# In case there is none, entire frame is used
def calculate_region_of_interest(frame, tracking_paths):
mask = np.zeros_like(frame)
mask[:] = 255
for x, y in [np.int32(tp[-1]) for tp in tracking_paths]:
cv2.circle(mask, (x, y), 6, 0, -1)
return mask
def add_tracking_paths(frame, tracking_paths):
mask = calculate_region_of_interest(frame, tracking_paths)
# Extract good features to track. You can learn more
# about the parameters here: http://goo.gl/BI2Kml
feature_points = cv2.goodFeaturesToTrack(frame, mask = mask, maxCorners = 500, \
qualityLevel = 0.3, minDistance = 7, blockSize = 7)
if feature_points is not None:
for x, y in np.float32(feature_points).reshape(-1, 2):
tracking_paths.append([(x, y)])
def start_tracking(cap, scaling_factor, num_frames_to_track, num_frames_jump, tracking_params):
tracking_paths = []
frame_index = 0
# Iterate until the user presses the ESC key
while True:
# read the input frame
ret, frame = cap.read()
# downsample the input frame
frame = cv2.resize(frame, None, fx=scaling_factor, fy=scaling_factor, \ interpolation=cv2.INTER_AREA)
frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
output_img = frame.copy()
if len(tracking_paths) > 0:
prev_img, current_img = prev_gray, frame_gray
# Compute feature points using optical flow. You can
# refer to the documentation to learn more about the
# parameters here: http://goo.gl/t6P4SE
feature_points, good_points = compute_feature_points(tracking_paths, \
prev_img, current_img)
new_tracking_paths = []
for tp, (x, y), good_points_flag in \
zip(tracking_paths, feature_points, good_points):
if not good_points_flag: continue
tp.append((x, y))
# Using the queue structure i.e. first in, first out
if len(tp) > num_frames_to_track: del tp[0]
new_tracking_paths.append(tp)
# draw green circles on top of the output image
cv2.circle(output_img, (x, y), 3, (0, 255, 0), -1)
tracking_paths = new_tracking_paths
# draw green lines on top of the output image
point_paths = [np.int32(tp) for tp in tracking_paths]
cv2.polylines(output_img, point_paths, False, (0, 150, 0))
# 'if' condition to skip every 'n'th frame
if not frame_index % num_frames_jump:
add_tracking_paths(frame_gray, tracking_paths)
frame_index += 1
prev_gray = frame_gray
cv2.imshow('Optical Flow', output_img)
# Check if the user pressed the ESC key
c = cv2.waitKey(1)
if c == 27:
break
这是使用上面的代码执行基于光流的跟踪:
import cv2
import numpy as np
if __name__ == '__main__':
# Capture the input frame
cap = cv2.VideoCapture(1)
# Downsampling factor for the image
scaling_factor = 0.5
# Number of frames to keep in the buffer when you
# are tracking. If you increase this number,
# feature points will have more "inertia"
num_frames_to_track = 5
# Skip every 'n' frames. This is just to increase the speed.
num_frames_jump = 2
# 'winSize' refers to the size of each patch. These patches
# are the smallest blocks on which we operate and track
# the feature points. You can read more about the parameters
# here: http://goo.gl/ulwqLk
tracking_params = dict(winSize = (11, 11), maxLevel = 2, \
criteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03))
start_tracking(cap, scaling_factor, num_frames_to_track, \
num_frames_jump, tracking_params)
cv2.destroyAllWindows()
因此,如果您想玩转它,可以让用户在输入视频中选择兴趣区域(就像我们之前所做的那样)。 然后,您可以从该兴趣区域提取特征点,并通过绘制边界框来跟踪对象。 这将是一个有趣的练习!
背景减法
背景减法在视频监视中非常有用。 基本上,背景减法技术在必须检测静态场景中的运动物体的情况下表现非常好。 顾名思义,该算法通过检测背景并将其从当前帧中减去以获取前景(即运动对象)来工作。
为了检测运动物体,我们需要首先建立背景模型。 这与帧差异不同,因为我们实际上是在对背景建模并使用此模型来检测运动对象。 因此,这比简单的帧差分技术要好得多。 该技术尝试检测场景中的静态部分,然后将其包括在背景模型中。 因此,这是一种自适应技术,可以根据场景进行调整。
让我们考虑下图:

现在,当我们在该场景中收集更多帧时,图像的每个部分将逐渐成为背景模型的一部分。 这也是我们前面讨论的内容。 如果场景是静态的,则模型会进行自我调整以确保更新了背景模型。 一开始就是这样:

请注意,我的脸部如何已成为背景模型(变黑区域)的一部分。 以下屏幕截图显示了几秒钟后我们将看到的内容。 如果我们继续前进,一切最终都会成为背景模型的一部分:

现在,如果我们引入一个新的运动对象,则将清晰地检测到它,如下所示:

这是执行此操作的代码:
import cv2
import numpy as np
# Capture the input frame
def get_frame(cap, scaling_factor=0.5):
ret, frame = cap.read()
# Resize the frame
frame = cv2.resize(frame, None, fx=scaling_factor,
fy=scaling_factor, interpolation=cv2.INTER_AREA)
return frame
if __name__=='__main__':
# Initialize the video capture object
cap = cv2.VideoCapture(1)
# Create the background subtractor object
bgSubtractor = cv2.createBackgroundSubtractorMOG2()
# This factor controls the learning rate of the algorithm.
# The learning rate refers to the rate at which your model
# will learn about the background. Higher value for
# 'history' indicates a slower learning rate. You
# can play with this parameter to see how it affects
# the output.
history = 100
# Iterate until the user presses the ESC key
while True:
frame = get_frame(cap, 0.5)
# Apply the background subtraction model to the input frame
mask = bgSubtractor.apply(frame, learningRate=1.0/history)
# Convert from grayscale to 3-channel RGB
mask = cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR)
cv2.imshow('Input frame', frame)
cv2.imshow('Moving Objects MOG', mask & frame)
# Check if the user pressed the ESC key
c = cv2.waitKey(delay=30)
if c == 27:
break
cap.release()
cv2.destroyAllWindows()
在前面的示例中,我们使用了称为BackgroundSubtractorMOG的背景减法,这是一种基于高斯混合的背景/前景分割算法。 在该算法中,每个背景像素都放入一个矩阵中,并通过应用高斯分布进行混合。 每种颜色都会获得一个权重,以代表它们停留在场景中的时间; 这样,将保持静态的颜色用于定义背景:
if __name__=='__main__':
# Initialize the video capture object
cap = cv2.VideoCapture(1)
# Create the background subtractor object
bgSubtractor= cv2.bgsegm.createBackgroundSubtractorGMG()
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, ksize=(3,3))
# Iterate until the user presses the ESC key
while True:
frame = get_frame(cap, 0.5)
# Apply the background subtraction model to the input frame
mask = bgSubtractor.apply(frame)
# Removing noise from background
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
cv2.imshow('Input frame', frame)
cv2.imshow('Moving Objects', mask)
# Check if the user pressed the ESC key
c = cv2.waitKey(delay=30)
if c == 27:
break
cap.release()
cv2.destroyAllWindows()
还有其他选择可能会更好。 例如,去除图像噪声,这是BackgroundSubtractorGMG的情况。 如果您想进一步了解它们,请访问这个页面。
总结
在本章中,我们学习了对象跟踪。 我们学习了如何使用帧差分获得运动信息,以及当我们要跟踪不同类型的对象时如何限制运动信息。 我们了解了色彩空间阈值及其如何用于跟踪彩色对象的知识。 我们讨论了用于对象跟踪的聚类技术,以及如何使用 CAMShift 算法构建交互式的对象跟踪器。 我们讨论了如何跟踪视频中的特征,以及如何使用光流来实现相同功能。 我们了解了背景减法及其如何用于视频监控。
在下一章中,我们将讨论对象识别以及如何构建视觉搜索引擎。
九、对象识别
在本章中,我们将学习对象识别以及如何使用它来构建视觉搜索引擎。 我们将讨论特征检测,构建特征向量以及使用机器学习构建分类器。 我们将学习如何使用这些不同的块来构建对象识别系统。
在本章结束时,您将了解:
- 对象检测和对象识别之间的区别
- 什么是密集特征检测器
- 什么是视觉词典
- 如何建立特征向量
- 什么是监督和无监督学习
- 什么是支持向量机以及如何使用它们来构建分类器
- 如何识别未知图像中的对象
对象检测与对象识别
在继续之前,我们需要了解本章将要讨论的内容。 您必须经常听到术语“对象检测”和“对象识别”,并且它们经常被误认为是同一件事。 两者之间有非常明显的区别。
对象检测是指在给定场景中检测特定对象的存在。 我们不知道对象可能是什么。 例如,我们在第 4 章,“检测和跟踪不同身体部位”中讨论了面部检测。 在讨论过程中,我们仅检测到给定图像中是否存在面部。 我们不认识这个人! 我们之所以不认识这个人,是因为我们在讨论中并不在意。 我们的目标是找到给定图像中人脸的位置。 商业面部识别系统同时使用面部检测和面部识别来识别人。 首先,我们需要找到面部,然后在裁剪的面部上运行面部识别器。
对象识别是在给定图像中识别对象的过程。 例如,对象识别系统可以告诉您给定的图像是否包含衣服或鞋子。 实际上,我们可以训练对象识别系统来识别许多不同的对象。 问题在于对象识别是一个非常难以解决的问题。 数十年来,它一直使计算机视觉研究人员望而却步,并且已成为计算机视觉的圣杯。 人类可以很容易地识别出各种各样的物体。 我们每天都会这样做,而且我们会毫不费力地这样做,但是计算机无法做到这种准确率。
让我们考虑一下拿铁杯的图片:

对象检测器将为您提供以下信息:

现在,考虑下面的茶杯图像:

如果通过对象检测器运行它,将看到以下结果:

如您所见,对象检测器可以检测到茶杯的存在,仅此而已。 如果训练对象识别器,它将为您提供以下信息,如下图所示:

如果考虑第二张图像,它将为您提供以下信息:

如您所见,完美的对象识别器将为您提供与该对象关联的所有信息。 如果对象识别器知道对象位于何处,则其特征将更加准确。 如果您的图像很大,而杯子只是其中的一小部分,则对象识别器可能无法识别它。 因此,第一步是检测对象并获得边界框。 一旦有了这些,就可以运行对象识别器来提取更多信息。
什么是密集特征检测器?
为了从图像中提取有意义的信息,我们需要确保特征提取器从给定图像的所有部分中提取特征。 考虑下图:

如果像第 5 章,“从图像中提取特征”那样,使用特征提取器提取特征,它将看起来像这样:

不幸的是,如果您曾经使用cv2.FeaturetureDetector_create("Dense")检测器,则该检测器已从 OpenCV 3.2 以后的版本中删除,因此我们需要实现自己的一个遍历网格并获取关键点的方法:

我们也可以控制密度。 让我们使其稀疏:

通过这样做,我们可以确保处理图像中的每个部分。 这是执行此操作的代码:
import sys
import cv2
import numpy as np
class DenseDetector():
def __init__(self, step_size=20, feature_scale=20, img_bound=20):
# Create a dense feature detector
self.initXyStep = step_size
self.initFeatureScale = feature_scale
self.initImgBound = img_bound
def detect(self, img):
keypoints = []
rows, cols = img.shape[:2]
for x in range(self.initImgBound, rows, self.initFeatureScale):
for y in range(self.initImgBound, cols, self.initFeatureScale):
keypoints.append(cv2.KeyPoint(float(x), float(y), self.initXyStep))
return keypoints
class SIFTDetector():
def __init__(self):
self.detector = cv2.xfeatures2d.SIFT_create()
def detect(self, img):
# Convert to grayscale
gray_image = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Detect keypoints using SIFT
return self.detector.detect(gray_image, None)
if __name__=='__main__':
input_image = cv2.imread(sys.argv[1])
input_image_dense = np.copy(input_image)
input_image_sift = np.copy(input_image)
keypoints = DenseDetector(20,20,5).detect(input_image)
# Draw keypoints on top of the input image
input_image_dense = cv2.drawKeypoints(input_image_dense, keypoints, None,\
flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
# Display the output image
cv2.imshow('Dense feature detector', input_image_dense)
keypoints = SIFTDetector().detect(input_image)
# Draw SIFT keypoints on the input image
input_image_sift = cv2.drawKeypoints(input_image_sift, keypoints, None,\
flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
# Display the output image
cv2.imshow('SIFT detector', input_image_sift)
# Wait until user presses a key
cv2.waitKey()
这使我们可以严格控制要提取的信息量。 当我们使用 SIFT 检测器时,图像的某些部分被忽略了。 当我们要处理突出特征时,这种方法效果很好,但是当我们构建对象识别器时,我们需要评估图像的所有部分。 因此,我们使用一个密集的检测器,然后从那些关键点提取特征。
什么是视觉词典?
我们将使用词袋模型来构建我们的对象识别器。 每个图像表示为视觉单词的直方图。 这些视觉词基本上是使用从训练图像中提取的所有关键点构建的N重心。 管道如下图所示:

从每个训练图像中,我们检测一组关键点,并为每个关键点提取特征。 每个图像都会产生不同数量的关键点。 为了训练分类器,必须使用固定长度的特征向量来表示每个图像。 该特征向量仅是直方图,其中每个箱子对应一个视觉单词。
当从训练图像中的所有关键点提取所有特征时,我们执行K-均值聚类并提取N重心。N是给定图像的特征向量的长度。 现在将每个图像表示为一个直方图,其中每个箱子对应于N重心之一。 为简单起见,假设N设置为四个。 现在,在给定的图像中,我们提取K个关键点。 在这些K关键点中,其中一些将最接*第一个质心,其中一些将最接*第二质心,依此类推。 因此,我们基于最接*每个关键点的质心构建直方图。 该直方图成为我们的特征向量。 该过程称为向量量化。
为了理解向量量化,让我们考虑一个例子。 假设我们有一幅图像,并且已经从中提取了一定数量的特征点。 现在我们的目标是以特征向量的形式表示该图像。 考虑下图:

如您所见,我们有四个质心。 请记住,图中显示的点代表特征空间,而不是图像中这些特征点的实际几何位置。 上图中以这种方式显示了它,因此很容易可视化。 图像中许多不同几何位置的点在特征空间中可以彼此靠*。 我们的目标是将该图像表示为直方图,其中每个箱子对应于这些质心之一。 这样,无论我们从图像中提取多少个特征点,都将始终将其转换为固定长度的特征向量。 因此,我们将每个特征点四舍五入到其最*的质心,如下图所示:

如果您为此图像构建直方图,它将看起来像这样:

现在,如果您考虑具有不同特征点分布的其他图像,它将看起来像这样:

群集如下所示:

直方图如下所示:

如您所见,即使这些点似乎是随机分布的,两个图像的直方图也有很大不同。 这是一项非常强大的技术,已广泛用于计算机视觉和信号处理中。 有许多不同的方法可以做到这一点,而准确率取决于您希望它有多细。 如果增加质心的数量,将可以更好地表示图像,从而增加特征向量的唯一性。 话虽如此,重要的是要提到您不能仅仅无限期地增加质心的数量。 如果这样做,它将变得过于嘈杂并失去功能。
什么是监督和无监督学习?
如果您熟悉机器学习的基础知识,那么您一定会知道什么是监督学习和无监督学习。
为了让您快速入门,监督学习指的是基于标记的样本构建函数。 例如,如果我们要构建一个将服装图像与鞋类图像分离的系统,那么我们首先需要建立一个数据库并对其进行标签。 我们需要告诉我们的算法,哪些图像对应于衣服,哪些图像对应于鞋。 基于这些数据,该算法将学习如何识别衣服和鞋类,以便当出现未知图像时,它可以识别该图像中的内容。
无监督学习与我们刚刚讨论的相反。 这里没有标签数据。 假设我们有一堆图像,我们只想将它们分成三组。 我们不知道标准是什么。 因此,无监督学习算法将尝试以最佳可能的方式将给定的数据集分为三组。 之所以讨论这一点,是因为我们将结合有监督和无监督学习来构建对象识别系统。
什么是支持向量机?
支持向量机(SVM)是在机器学习领域中非常流行的监督学习模型。 SVM 确实擅长分析标记的数据和检测模式。 给定一堆数据点和相关的标签,SVM 将以最佳方式构建分离的超*面。
等一下,什么是超*面? 为了理解这一点,让我们考虑下图:

如您所见,这些点被与这些点等距的线边界分隔开。 这很容易在两个维度上可视化。 如果是三维尺寸,则分隔符将是*面。 当我们为图像构建特征时,特征向量的长度通常在六位数范围内。 因此,当我们进入如此高的维度空间时,线的等效物将是超*面。 制定超*面后,我们将使用数学模型根据未知数据在地图上的位置进行分类。
如果我们不能用简单的直线分开数据怎么办?
我们在 SVM 中使用了核技巧。 考虑下图:

如我们所见,我们不能画一条简单的直线将红色点和蓝色点分开。 提出一个可以满足所有要求的曲线优美的边界非常昂贵。 SVM 确实擅长绘制直线。 那么,我们在这里的答案是什么? 关于 SVM 的好处是它们可以绘制任意数量的这些直线。 因此,从技术上讲,如果将这些点投影到一个高维空间中,可以通过一个简单的超*面将它们分开,则 SVM 会提供一个确切的边界。 一旦有了该边界,就可以将其投影回原始空间。 该超*面在我们原始的较低维空间上的投影看起来是弯曲的,如下图所示:

SVM 的主题确实很深,我们将无法在此处进行详细讨论。 如果您真的有兴趣,可以在线找到大量材料。 您可以通过简单的教程来更好地理解它。 此外,OpenCV 官方文档还包含一些示例,可以使您更好地理解它们。
我们如何实际实现呢?
我们现在已经到达核心。 前面的介绍是必要的,因为它为您提供了构建对象检测和识别系统所需的背景。 现在,让我们构建一个对象识别器,该对象识别器可以识别给定的图像是否包含衣服,一双鞋子或一个包。 我们可以轻松扩展此系统以检测任意数量的项目。 我们从三个不同的项目开始,以便您以后可以开始进行试验。
在开始之前,我们需要确保我们具有一组训练图像。 在线有许多数据库,其中图像已经按组排列。 Caltech256 可能是最流行的对象识别数据库之一。 您可以从这里下载。 创建一个名为images的文件夹,并在其中创建三个子文件夹,即dress,footwear和bag。 在每个子文件夹中,添加与该项目相对应的 20 张图像。 您可以只从互联网下载这些图像,但要确保这些图像的背景干净。
例如,礼服图片将如下所示:

鞋类图片如下所示:

袋子图像如下所示:

现在我们有 60 张训练图像,我们准备开始。 附带说明一下,对象识别系统实际上需要成千上万张训练图像才能在现实世界中表现良好。 由于我们正在构建一个对象识别器来检测三种类型的对象,因此每个对象仅拍摄 20 张训练图像。 添加更多的训练图像将提高我们系统的准确率和鲁棒性。
这里的第一步是从所有训练图像中提取特征向量,并建立可视词典(也称为码本)。
首先,重用我们先前的DenseDetector类,再加上 SIFT 特征检测器:
class SIFTExtractor():
def __init__(self):
self.extractor = cv2.xfeatures2d.SIFT_create()
def compute(self, image, kps):
if image is None:
print "Not a valid image"
raise TypeError
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
kps, des = self.extractor.detectAndCompute(gray_image, None)
return kps, des
然后,我们的Quantizer类计算向量量化并构建特征向量:
from sklearn.cluster import KMeans
# Vector quantization
class Quantizer(object):
def __init__(self, num_clusters=32):
self.num_dims = 128
self.extractor = SIFTExtractor()
self.num_clusters = num_clusters
self.num_retries = 10
def quantize(self, datapoints):
# Create KMeans object
kmeans = KMeans(self.num_clusters,
n_init=max(self.num_retries, 1),
max_iter=10, tol=1.0)
# Run KMeans on the datapoints
res = kmeans.fit(datapoints)
# Extract the centroids of those clusters
centroids = res.cluster_centers_
return kmeans, centroids
def normalize(self, input_data):
sum_input = np.sum(input_data)
if sum_input > 0:
return input_data / sum_input
else:
return input_data
# Extract feature vector from the image
def get_feature_vector(self, img, kmeans, centroids):
kps = DenseDetector().detect(img)
kps, fvs = self.extractor.compute(img, kps)
labels = kmeans.predict(fvs)
fv = np.zeros(self.num_clusters)
for i, item in enumerate(fvs):
fv[labels[i]] += 1
fv_image = np.reshape(fv, ((1, fv.shape[0])))
return self.normalize(fv_image)
重用以前的实现,另一个需要的类是FeatureExtractor类,该类旨在提取每个图像的质心:
class FeatureExtractor(object):
def extract_image_features(self, img):
# Dense feature detector
kps = DenseDetector().detect(img)
# SIFT feature extractor
kps, fvs = SIFTExtractor().compute(img, kps)
return fvs
# Extract the centroids from the feature points
def get_centroids(self, input_map, num_samples_to_fit=10):
kps_all = []
count = 0
cur_label = ''
for item in input_map:
if count >= num_samples_to_fit:
if cur_label != item['label']:
count = 0
else:
continue
count += 1
if count == num_samples_to_fit:
print("Built centroids for", item['label'])
cur_label = item['label']
img = cv2.imread(item['image'])
img = resize_to_size(img, 150)
num_dims = 128
fvs = self.extract_image_features(img)
kps_all.extend(fvs)
kmeans, centroids = Quantizer().quantize(kps_all)
return kmeans, centroids
def get_feature_vector(self, img, kmeans, centroids):
return Quantizer().get_feature_vector(img, kmeans, centroids)
以下脚本将为我们提供特征字典,以对未来的图像进行分类:
########################
# create_features.py
########################
import os
import sys
import argparse
import json
import cv2
import numpy as np
import cPickle as pickle
# In case of Python 2.7 use:
# import cPickle as pickle
def build_arg_parser():
parser = argparse.ArgumentParser(description='Creates features for given images')
parser.add_argument("--samples", dest="cls", nargs="+", action="append", required=True,\
help="Folders containing the training images.\nThe first element needs to be the class label.")
parser.add_argument("--codebook-file", dest='codebook_file', required=True,
help="Base file name to store the codebook")
parser.add_argument("--feature-map-file", dest='feature_map_file', required=True,\
help="Base file name to store the feature map")
return parser
# Loading the images from the input folder
def load_input_map(label, input_folder):
combined_data = []
if not os.path.isdir(input_folder):
raise IOError("The folder " + input_folder + " doesn't exist")
# Parse the input folder and assign the labels
for root, dirs, files in os.walk(input_folder):
for filename in (x for x in files if x.endswith('.jpg')):
combined_data.append({'label': label, 'image':
os.path.join(root, filename)})
return combined_data
def extract_feature_map(input_map, kmeans, centroids):
feature_map = []
for item in input_map:
temp_dict = {}
temp_dict['label'] = item['label']
print("Extracting features for", item['image'])
img = cv2.imread(item['image'])
img = resize_to_size(img, 150)
temp_dict['feature_vector'] = FeatureExtractor().get_feature_vector(img, kmeans, centroids)
if temp_dict['feature_vector'] is not None:
feature_map.append(temp_dict)
return feature_map
# Resize the shorter dimension to 'new_size'
# while maintaining the aspect ratio
def resize_to_size(input_image, new_size=150):
h, w = input_image.shape[0], input_image.shape[1]
ds_factor = new_size / float(h)
if w < h:
ds_factor = new_size / float(w)
new_size = (int(w * ds_factor), int(h * ds_factor))
return cv2.resize(input_image, new_size)
if __name__=='__main__':
args = build_arg_parser().parse_args()
input_map = []
for cls in args.cls:
assert len(cls) >= 2, "Format for classes is `<label> file`"
label = cls[0]
input_map += load_input_map(label, cls[1])
# Building the codebook
print("===== Building codebook =====")
kmeans, centroids = FeatureExtractor().get_centroids(input_map)
if args.codebook_file:
with open(args.codebook_file, 'wb') as f:
print('kmeans', kmeans)
print('centroids', centroids)
pickle.dump((kmeans, centroids), f)
# Input data and labels
print("===== Building feature map =====")
feature_map = extract_feature_map(input_map, kmeans,
centroids)
if args.feature_map_file:
with open(args.feature_map_file, 'wb') as f:
pickle.dump(feature_map, f)
代码内部发生了什么?
我们需要做的第一件事是提取质心。 这就是我们要构建可视词典的方式。 FeatureExtractor类中的get_centroids方法旨在实现此目的。 我们会不断收集从关键点提取的图像特征,直到有足够数量的特征为止。 由于我们使用的是密集探测器,因此 10 张图像就足够了。 我们只拍摄 10 张图像的原因是因为它们会产生大量特征。 即使添加更多特征点,质心也不会改变太多。
提取质心后,就可以继续进行特征提取的下一步了。 形心集是我们的视觉词典。 函数extract_feature_map将从每个图像中提取特征向量,并将其与相应的标签关联。 这样做的原因是因为我们需要此映射来训练我们的分类器。 我们需要一组关键点,并且每个关键点都应与一个标签关联。 因此,我们从图像开始,提取特征向量,然后将其与相应的标签(例如包,衣服或鞋类)相关联。
Quantizer类旨在实现向量量化并构建特征向量。 对于从图像中提取的每个关键点,get_feature_vector方法会在我们的词典中找到最接*的视觉单词。 通过这样做,我们最终基于可视词典构建了直方图。 现在,将每个图像表示为一组视觉单词的组合。 因此,名称为词袋。
下一步是使用这些特征训练分类器。 为此,我们实现了另一个类:
from sklearn.multiclass import OneVsOneClassifier
from sklearn.svm import LinearSVC
from sklearn import preprocessing
# To train the classifier
class ClassifierTrainer(object):
def __init__(self, X, label_words):
# Encoding the labels (words to numbers)
self.le = preprocessing.LabelEncoder()
# Initialize One versus One Classifier using a linear kernel
self.clf = OneVsOneClassifier(LinearSVC(random_state=0))
y = self._encodeLabels(label_words)
X = np.asarray(X)
self.clf.fit(X, y)
# Predict the output class for the input datapoint
def _fit(self, X):
X = np.asarray(X)
return self.clf.predict(X)
# Encode the labels (convert words to numbers)
def _encodeLabels(self, labels_words):
self.le.fit(labels_words)
return np.array(self.le.transform(labels_words),
dtype=np.float32)
# Classify the input datapoint
def classify(self, X):
labels_nums = self._fit(X)
labels_words = self.le.inverse_transform([int(x) for x in
labels_nums])
return labels_words
现在,基于先前的特征字典,我们生成了 SVM 文件:
###############
# training.py
###############
import os
import sys
import argparse
import _pickle as pickle
import numpy as np
def build_arg_parser():
parser = argparse.ArgumentParser(description='Trains the classifier models')
parser.add_argument("--feature-map-file", dest="feature_map_file", required=True,\
help="Input pickle file containing the feature map")
parser.add_argument("--svm-file", dest="svm_file", required=False,\
help="Output file where the pickled SVM model will be stored")
return parser
if __name__=='__main__':
args = build_arg_parser().parse_args()
feature_map_file = args.feature_map_file
svm_file = args.svm_file
# Load the feature map
with open(feature_map_file, 'rb') as f:
feature_map = pickle.load(f)
# Extract feature vectors and the labels
labels_words = [x['label'] for x in feature_map]
# Here, 0 refers to the first element in the
# feature_map, and 1 refers to the second
# element in the shape vector of that element
# (which gives us the size)
dim_size = feature_map[0]['feature_vector'].shape[1]
X = [np.reshape(x['feature_vector'], (dim_size,)) for x in feature_map]
# Train the SVM
svm = ClassifierTrainer(X, labels_words)
if args.svm_file:
with open(args.svm_file, 'wb') as f:
pickle.dump(svm, f)
请注意,我们正在以二进制模式进行写入/读取,这就是打开文件时使用rb和wb模式的原因。
我们如何建立训练器?
我们使用scikit-learn包来构建 SVM 模型,并使用scipy来建立数学优化工具。 您可以按以下方式安装它:
$ pip install scikit-learn scipy
我们从标记的数据开始,并将其提供给OneVsOneClassifier方法。 我们有一个classify方法,该方法可以对输入图像进行分类并为其关联标签。
让我们试一下吧? 确保有一个名为images的文件夹,其中有这三个类的训练图像。 创建一个名为models的文件夹,该文件夹将存储学习模型。 在终端上运行以下命令以创建特征并训练分类器:
$ python create_features.py --samples bag images/bag/ --samples dress
images/dress/ --samples footwear images/footwear/ --codebook-file
models/codebook.pkl --feature-map-file models/feature_map.pkl
$ python training.py --feature-map-file models/feature_map.pkl
--svm-file models/svm.pkl
现在已经对分类器进行了训练,我们只需要一个模块即可对输入图像进行分类并检测其中的对象:
import create_features as cf
from training import ClassifierTrainer
# Classifying an image
class ImageClassifier(object):
def __init__(self, svm_file, codebook_file):
# Load the SVM classifier
with open(svm_file, 'rb') as f:
self.svm = pickle.load(f)
# Load the codebook
with open(codebook_file, 'rb') as f:
self.kmeans, self.centroids = pickle.load(f)
# Method to get the output image tag
def getImageTag(self, img):
# Resize the input image
img = cf.resize_to_size(img)
# Extract the feature vector
feature_vector = cf.FeatureExtractor().get_feature_vector(img, self.kmeans, \
self.centroids)
# Classify the feature vector and get the output tag
image_tag = self.svm.classify(feature_vector)
return image_tag
以下是对数据进行分类的脚本,该脚本可以根据我们之前的训练过程为图像加标签:
###############
# classify_data.py
###############
import os
import sys
import argparse
import _pickle as pickle
import cv2
import numpy as np
def build_arg_parser():
parser = argparse.ArgumentParser(description='Extracts features from each line and classifies the data')
parser.add_argument("--input-image", dest="input_image", required=True,\
help="Input image to be classified")
parser.add_argument("--svm-file", dest="svm_file", required=True,\
help="File containing the trained SVM model")
parser.add_argument("--codebook-file", dest="codebook_file", required=True,\
help="File containing the codebook")
return parser
if __name__=='__main__':
args = build_arg_parser().parse_args()
svm_file = args.svm_file
codebook_file = args.codebook_file
input_image = cv2.imread(args.input_image)
tag = ImageClassifier(svm_file, codebook_file).getImageTag(input_image)
print("Output class:", tag)
我们都准备好了! 我们只是从输入图像中提取feature向量,并将其用作分类器的输入参数。 让我们继续看看是否可行。 从互联网上下载随机的鞋类图片,并确保其背景干净。 通过用正确的文件名替换new_image.jpg,运行以下命令:
$ python classify_data.py --input-image new_image.jpg --svm-file
models/svm.pkl --codebook-file models/codebook.pkl
我们可以使用相同的技术来构建视觉搜索引擎。 视觉搜索引擎查看输入图像并显示一堆与其相似的图像。 我们可以重用对象识别框架来构建它。 从输入图像中提取特征向量,并将其与训练数据集中的所有特征向量进行比较。 选择最热门的比赛并显示结果。 这是一种简单的做事方式!
在现实世界中,我们必须处理数十亿张图像。 因此,在显示输出之前,您无力搜索每个图像。 有很多算法可用于确保在现实世界中高效而快速。 深度学习已在该领域广泛使用,并且在最*几年中显示出很大的希望。 这是机器学习的一个分支,专注于学习数据的最佳表示,因此机器可以更轻松地学习新任务。 您可以在以下网址了解更多信息。
总结
在本章中,我们学习了如何构建对象识别系统。 详细讨论了对象检测和对象识别之间的区别。 我们了解了密集特征检测器,可视词典,向量量化,以及如何使用这些概念来构建特征向量。 讨论了有监督和无监督学习的概念。 我们讨论了支持向量机以及如何使用它们构建分类器。 我们学习了如何识别未知图像中的物体,以及如何扩展该概念以构建视觉搜索引擎。
在下一章中,我们将讨论立体成像和 3D 重建。 我们将讨论如何构建深度图并从给定场景中提取 3D 信息。
十、增强现实
在本章中,您将学习增强现实以及如何使用它来构建出色的应用。 我们将讨论姿势估计和*面跟踪。 您将学习如何将坐标从 3D 映射到 2D,以及如何在实时视频的顶部叠加图形。
在本章结束时,您将了解:
- 增强现实的前提
- 什么是姿势估计
- 如何追踪*面对象
- 如何将坐标从 3D 映射到 2D
- 如何在视频上实时叠加图形
增强现实的前提是什么?
在介绍所有有趣的东西之前,让我们了解增强现实的含义。 您可能已经看到了增强现实一词在各种环境中使用。 因此,在开始讨论实现细节之前,我们应该了解增强现实的前提。 增强现实是指将计算机生成的输入(例如图像,声音,图形和文本)叠加在现实世界之上。
增强现实试图通过无缝地合并信息并增强我们所看到和感觉到的东西来模糊真实的东西和计算机生成的东西之间的界限。 实际上,它与称为中介现实的概念密切相关,在中介中,计算机可以修改我们对现实的看法。 结果,该技术通过增强我们当前对现实的感知而起作用。 现在,这里的挑战是使它对用户看起来无缝。 只需在输入视频的顶部覆盖一些内容即可,但是我们需要使其看起来像是视频的一部分。 用户应该感觉到计算机生成的输入紧密反映了现实世界。 这是我们构建增强现实系统时想要实现的目标。
在这种情况下,计算机视觉研究探索了如何将计算机生成的图像应用于实时视频流,以便我们可以增强对现实世界的感知。 增强现实技术具有广泛的应用,包括但不限于头戴式显示器,汽车,数据可视化,游戏,建筑等。 现在,我们拥有功能强大的智能手机和更智能的机器,我们可以轻松构建高端增强现实应用。
增强现实系统是什么样的?
让我们考虑下图:

正如我们在此处看到的那样,摄像机会捕获现实世界的视频以获取参考点。 图形系统生成需要覆盖在视频顶部的虚拟对象。 现在,视频合并块是所有魔术发生的地方。 该块应该足够聪明,以了解如何以最佳方式将虚拟对象叠加在现实世界的顶部。
增强现实的几何变换
增强现实的结果是惊人的,但是在下面却发生了许多数学事情。 增强现实利用了大量的几何变换和相关的数学函数来确保一切看起来都*滑。 在谈论增强现实的实时视频时,我们需要在现实世界的顶部精确地注册虚拟对象。 为了更好地理解这一点,让我们将其视为两个摄像机的对准:通过它可以看到世界的真实摄像机,以及投射计算机生成的图形对象的虚拟摄像机。
为了构建增强现实系统,需要建立以下几何变换:
- 对象到场景:此转换是指转换虚拟对象的 3D 坐标,并在我们真实世界场景的坐标框中表达它们。 这样可以确保我们将虚拟对象放置在正确的位置。
- 场景到摄像机:此转换是指现实世界中摄像机的姿势。 通过姿势,我们表示摄像机的方向和位置。 我们需要估计摄像机的视点,以便我们知道如何覆盖虚拟对象。
- 摄像机到图像:这是指摄像机的校准参数。 这定义了我们如何将 3D 对象投影到 2D 图像*面上。 这是我们最终将实际看到的图像。
考虑下图:

正如我们在这里看到的那样,这辆车正试图适应场景,但看起来非常虚假。 如果我们没有以正确的方式转换坐标,汽车将看起来不自然。 这就是我们所说的对象到场景转换! 将虚拟对象的 3D 坐标转换为现实世界的坐标框架后,我们需要估计相机的姿态:

我们需要了解相机的位置和旋转,因为这是用户会看到的。 一旦估计了相机的姿势,就可以将 3D 场景放置在 2D 图像上了:

一旦进行了这些转换,就可以构建完整的系统。
什么是姿势估计?
在继续之前,我们需要了解如何估计相机的姿势。 这是增强现实系统中非常关键的一步,如果我们想要无缝的体验,我们需要正确处理。 在增强现实世界中,我们实时将图形叠加在对象之上。 为此,我们需要知道相机的位置和方向,并且需要快速进行操作。 这是姿势估计变得非常重要的地方。 如果未正确跟踪姿势,则叠加的图形将看起来不自然。
考虑下图:

箭头表示表面是法线。 假设对象改变了方向:

现在,即使位置相同,方向也已更改。 我们需要掌握这些信息,以便叠加的图形看起来自然。 我们需要确保图形与此方向和位置对齐。
如何追踪*面对象
既然您已经了解了什么是姿态估计,那么让我们看看如何使用它来跟踪*面对象。 让我们考虑以下*面对象:

现在,如果我们从这张图片中提取特征点,我们将看到如下内容:

让我们倾斜纸箱:

如我们所见,纸板箱在此图像中倾斜。 现在,如果要确保我们的虚拟对象覆盖在该表面的顶部,则需要收集此*面倾斜信息。 一种方法是使用特征点的相对位置。 如果我们从前面的图像中提取特征点,它将看起来像这样:

如您所见,特征点在*面的远端与*端的特征点在水*方向上更加接*:

因此,我们可以利用此信息从图像中提取方向信息。 如果您还记得的话,我们在讨论几何变换和全景成像时会详细讨论透视变换。 我们需要做的就是使用这两组点并提取单应性矩阵。 该单应性矩阵将告诉我们纸板箱如何旋转。
考虑下图:

首先,我们将使用ROISelector类选择兴趣区域,然后,将这些坐标传递给PoseEstimator:
class ROISelector(object):
def __init__(self, win_name, init_frame, callback_func):
self.callback_func = callback_func
self.selected_rect = None
self.drag_start = None
self.tracking_state = 0
event_params = {"frame": init_frame}
cv2.namedWindow(win_name)
cv2.setMouseCallback(win_name, self.mouse_event, event_params)
def mouse_event(self, event, x, y, flags, param):
x, y = np.int16([x, y])
# Detecting the mouse button down event
if event == cv2.EVENT_LBUTTONDOWN:
self.drag_start = (x, y)
self.tracking_state = 0
if self.drag_start:
if event == cv2.EVENT_MOUSEMOVE:
h, w = param["frame"].shape[:2]
xo, yo = self.drag_start
x0, y0 = np.maximum(0, np.minimum([xo, yo], [x, y]))
x1, y1 = np.minimum([w, h], np.maximum([xo, yo], [x, y]))
self.selected_rect = None
if x1-x0 > 0 and y1-y0 > 0:
self.selected_rect = (x0, y0, x1, y1)
elif event == cv2.EVENT_LBUTTONUP:
self.drag_start = None
if self.selected_rect is not None:
self.callback_func(self.selected_rect)
self.selected_rect = None
self.tracking_state = 1
def draw_rect(self, img, rect):
if not rect: return False
x_start, y_start, x_end, y_end = rect
cv2.rectangle(img, (x_start, y_start), (x_end, y_end), (0, 255, 0), 2)
return True
在下图中,兴趣区域为绿色矩形:

然后,我们从该兴趣区域提取特征点。 由于我们跟踪的是*面物体,因此该算法假定此关注区域为*面。 这是显而易见的,但是最好明确声明! 因此,选择此兴趣区域时,请确保手中有一个纸板箱。 另外,如果纸板箱上有一堆图案和独特点会更好,这样可以很容易地检测和跟踪其特征点。
PoseEstimator类将从其方法add_target()中获得兴趣区域,并从它们中提取这些特征点,这将使我们能够跟踪物体的运动:
class PoseEstimator(object):
def __init__(self):
# Use locality sensitive hashing algorithm
flann_params = dict(algorithm = 6, table_number = 6, key_size = 12, multi_probe_level = 1)
self.min_matches = 10
self.cur_target = namedtuple('Current', 'image, rect, keypoints, descriptors, data')
self.tracked_target = namedtuple('Tracked', 'target, points_prev, points_cur, H, quad')
self.feature_detector = cv2.ORB_create()
self.feature_detector.setMaxFeatures(1000)
self.feature_matcher = cv2.FlannBasedMatcher(flann_params, {})
self.tracking_targets = []
# Function to add a new target for tracking
def add_target(self, image, rect, data=None):
x_start, y_start, x_end, y_end = rect
keypoints, descriptors = [], []
for keypoint, descriptor in zip(*self.detect_features(image)):
x, y = keypoint.pt
if x_start <= x <= x_end and y_start <= y <= y_end:
keypoints.append(keypoint)
descriptors.append(descriptor)
descriptors = np.array(descriptors, dtype='uint8')
self.feature_matcher.add([descriptors])
target = self.cur_target(image=image, rect=rect, keypoints=keypoints, descriptors=descriptors, data=None)
self.tracking_targets.append(target)
# To get a list of detected objects
def track_target(self, frame):
self.cur_keypoints, self.cur_descriptors = self.detect_features(frame)
if len(self.cur_keypoints) < self.min_matches: return []
try: matches = self.feature_matcher.knnMatch(self.cur_descriptors, k=2)
except Exception as e:
print('Invalid target, please select another with features to extract')
return []
matches = [match[0] for match in matches if len(match) == 2 and match[0].distance < match[1].distance * 0.75]
if len(matches) < self.min_matches: return []
matches_using_index = [[] for _ in range(len(self.tracking_targets))]
for match in matches:
matches_using_index[match.imgIdx].append(match)
tracked = []
for image_index, matches in enumerate(matches_using_index):
if len(matches) < self.min_matches: continue
target = self.tracking_targets[image_index]
points_prev = [target.keypoints[m.trainIdx].pt for m in matches]
points_cur = [self.cur_keypoints[m.queryIdx].pt for m in matches]
points_prev, points_cur = np.float32((points_prev, points_cur))
H, status = cv2.findHomography(points_prev, points_cur, cv2.RANSAC, 3.0)
status = status.ravel() != 0
if status.sum() < self.min_matches: continue
points_prev, points_cur = points_prev[status], points_cur[status]
x_start, y_start, x_end, y_end = target.rect
quad = np.float32([[x_start, y_start], [x_end, y_start], [x_end, y_end], [x_start, y_end]])
quad = cv2.perspectiveTransform(quad.reshape(1, -1, 2), H).reshape(-1, 2)
track = self.tracked_target(target=target, points_prev=points_prev, points_cur=points_cur, H=H, quad=quad)
tracked.append(track)
tracked.sort(key = lambda x: len(x.points_prev), reverse=True)
return tracked
# Detect features in the selected ROIs and return the keypoints and descriptors
def detect_features(self, frame):
keypoints, descriptors = self.feature_detector.detectAndCompute(frame, None)
if descriptors is None: descriptors = []
return keypoints, descriptors
# Function to clear all the existing targets
def clear_targets(self):
self.feature_matcher.clear()
self.tracking_targets = []
让跟踪开始! 我们将移动纸箱看看会发生什么:

如您所见,特征点正在关注区域内跟踪。 让我们将其倾斜一下,看看会发生什么:

似乎正在正确跟踪特征点。 我们可以看到,覆盖的矩形根据纸板箱的表面改变其方向。
这是执行此操作的代码:
import sys
from collections import namedtuple
import cv2
import numpy as np
class VideoHandler(object):
def __init__(self, capId, scaling_factor, win_name):
self.cap = cv2.VideoCapture(capId)
self.pose_tracker = PoseEstimator()
self.win_name = win_name
self.scaling_factor = scaling_factor
ret, frame = self.cap.read()
self.rect = None
self.frame = cv2.resize(frame, None, fx=scaling_factor, fy=scaling_factor, interpolation=cv2.INTER_AREA)
self.roi_selector = ROISelector(win_name, self.frame, self.set_rect)
def set_rect(self, rect):
self.rect = rect
self.pose_tracker.add_target(self.frame, rect)
def start(self):
paused = False
while True:
if not paused or self.frame is None:
ret, frame = self.cap.read()
scaling_factor = self.scaling_factor
frame = cv2.resize(frame, None, fx=scaling_factor, fy=scaling_factor, interpolation=cv2.INTER_AREA)
if not ret: break
self.frame = frame.copy()
img = self.frame.copy()
if not paused and self.rect is not None:
tracked = self.pose_tracker.track_target(self.frame)
for item in tracked:
cv2.polylines(img, [np.int32(item.quad)], True, (255, 255, 255), 2)
for (x, y) in np.int32(item.points_cur):
cv2.circle(img, (x, y), 2, (255, 255, 255))
self.roi_selector.draw_rect(img, self.rect)
cv2.imshow(self.win_name, img)
ch = cv2.waitKey(1)
if ch == ord(' '): paused = not paused
if ch == ord('c'): self.pose_tracker.clear_targets()
if ch == 27: break
if __name__ == '__main__':
VideoHandler(0, 0.8, 'Tracker').start()
代码内部发生了什么?
首先,我们有一个PoseEstimator类,在这里进行所有繁重的工作。 我们需要一些东西来检测图像中的特征,并需要一些东西来匹配连续图像之间的特征。 因此,我们使用 ORB 特征检测器和 Flann 特征匹配器在提取的特征中进行快速最*邻搜索。 如您所见,我们在构造器中使用这些参数初始化类。
每当我们选择兴趣区域时,我们都会调用add_target方法将其添加到我们的跟踪目标列表中。 此方法只是从兴趣区域提取特征并将其存储在一个类变量中。 现在我们有了目标,我们已经准备好追踪它!
track_target方法处理所有跟踪。 我们采用当前框架并提取所有关键点。 但是,我们对视频当前帧中的所有关键点并不真正感兴趣。 我们只想要属于我们目标对象的关键点。 因此,现在我们的工作是在当前帧中找到最接*的关键点。
现在,我们在当前帧中有一组关键点,而在上一帧中还有来自目标对象的另一组关键点。 下一步是从这些匹配点中提取单应性矩阵。 这个单应性矩阵告诉我们如何变换覆盖的矩形,使其与纸板箱的表面对齐。 我们只需要获取此单应性矩阵并将其应用于覆盖的矩形即可获得所有纸板箱点的新位置。
如何增强我们的现实
现在我们知道了如何跟踪*面对象,让我们看看如何将 3D 对象叠加在现实世界的顶部。 对象是 3D,但屏幕上的视频是 2D。 因此,这里的第一步是了解如何将那些 3D 对象映射到 2D 曲面,以使它们看起来逼真。 我们只需要将这些 3D 点投影到*面上即可。
将坐标从 3D 映射到 2D
估计姿势后,我们会将点从 3D 投影到 2D。 考虑下图:

正如我们在此处看到的那样,电视遥控器是 3D 对象,但我们在 2D *面上看到它。 现在,如果我们四处移动,它将看起来像这样:

该 3D 对象仍在 2D *面上。 对象已移动到其他位置,并且距相机的距离也已更改。 我们如何计算这些坐标? 我们需要一种将该 3D 对象映射到 2D 曲面的机制。 这是 3D 到 2D 投影真正重要的地方。
我们只需要估计摄像机的初始姿势即可。 现在,假设摄像机的固有参数是已知的。 因此,我们可以仅使用 OpenCV 中的solvePnP函数来估计摄像机的姿势。 此函数用于使用一组点来估计对象的姿势,如以下代码所示。 您可以在这个页面上阅读有关的更多信息:
solvePnP(InputArray objectPoints, InputArray imagePoints, InputArray cameraMatrix, InputArray distCoeffs, OutputArray rvec, OutputArray tvec, bool useExtrinsicGuess, int flags)
完成此操作后,我们需要将这些点投影到 2D *面上。 我们使用 OpenCV projectPoints函数来执行此操作。 此函数计算这些 3D 点在 2D *面上的投影。
如何在视频上叠加 3D 对象
现在我们有了所有不同的模块,我们已经准备好构建最终系统。 假设我们要在纸板箱顶部覆盖一个金字塔,如下所示:

让我们倾斜纸箱看看会发生什么:

看起来像金字塔在跟随表面。 让我们添加第二个目标:

您可以继续添加更多目标,所有这些金字塔都会得到很好的跟踪。 让我们看看如何使用 OpenCV Python 做到这一点。 确保将以前的文件另存为pose_estimation.py,因为我们将从那里导入几个类:
import cv2
import numpy as np
from pose_estimation import PoseEstimator, ROISelector
class Tracker(object):
def __init__(self, capId, scaling_factor, win_name):
self.cap = cv2.VideoCapture(capId)
self.rect = None
self.win_name = win_name
self.scaling_factor = scaling_factor
self.tracker = PoseEstimator()
ret, frame = self.cap.read()
self.rect = None
self.frame = cv2.resize(frame, None, fx=scaling_factor, fy=scaling_factor, interpolation=cv2.INTER_AREA)
self.roi_selector = ROISelector(win_name, self.frame, self.set_rect)
self.overlay_vertices = np.float32([[0, 0, 0], [0, 1, 0], [1, 1, 0], [1, 0, 0], [0.5, 0.5, 4]])
self.overlay_edges = [(0, 1), (1, 2), (2, 3), (3, 0), (0,4), (1,4), (2,4), (3,4)]
self.color_base = (0, 255, 0)
self.color_lines = (0, 0, 0)
def set_rect(self, rect):
self.rect = rect
self.tracker.add_target(self.frame, rect)
def start(self):
paused = False
while True:
if not paused or self.frame is None:
ret, frame = self.cap.read()
scaling_factor = self.scaling_factor
frame = cv2.resize(frame, None, fx=scaling_factor, fy=scaling_factor,\
interpolation=cv2.INTER_AREA)
if not ret: break
self.frame = frame.copy()
img = self.frame.copy()
if not paused:
tracked = self.tracker.track_target(self.frame)
for item in tracked:
cv2.polylines(img, [np.int32(item.quad)],
True, self.color_lines, 2)
for (x, y) in np.int32(item.points_cur):
cv2.circle(img, (x, y), 2,
self.color_lines)
self.overlay_graphics(img, item)
self.roi_selector.draw_rect(img, self.rect)
cv2.imshow(self.win_name, img)
ch = cv2.waitKey(1)
if ch == ord(' '): self.paused = not self.paused
if ch == ord('c'): self.tracker.clear_targets()
if ch == 27: break
def overlay_graphics(self, img, tracked):
x_start, y_start, x_end, y_end = tracked.target.rect
quad_3d = np.float32([[x_start, y_start, 0], [x_end,
y_start, 0],
[x_end, y_end, 0], [x_start, y_end, 0]])
h, w = img.shape[:2]
K = np.float64([[w, 0, 0.5*(w-1)],
[0, w, 0.5*(h-1)],
[0, 0, 1.0]])
dist_coef = np.zeros(4)
ret, rvec, tvec = cv2.solvePnP(objectPoints=quad_3d, imagePoints=tracked.quad,
cameraMatrix=K, distCoeffs=dist_coef)
verts = self.overlay_vertices * \
[(x_end-x_start), (y_end-y_start), -(x_end-x_start)*0.3] + (x_start, y_start, 0)
verts = cv2.projectPoints(verts, rvec, tvec, cameraMatrix=K, distCoeffs=dist_coef)[0].reshape(-1, 2)
verts_floor = np.int32(verts).reshape(-1,2)
cv2.drawContours(img, contours=[verts_floor[:4]], contourIdx=-1, color=self.color_base, thickness=-3)
cv2.drawContours(img, contours=[np.vstack((verts_floor[:2], verts_floor[4:5]))], contourIdx=-1, color=(0,255,0), thickness=-3)
cv2.drawContours(img, contours=[np.vstack((verts_floor[1:3], verts_floor[4:5]))], contourIdx=-1, color=(255,0,0), thickness=-3)
cv2.drawContours(img, contours=[np.vstack((verts_floor[2:4], verts_floor[4:5]))], contourIdx=-1, color=(0,0,150), thickness=-3)
cv2.drawContours(img, contours=[np.vstack((verts_floor[3:4], verts_floor[0:1], verts_floor[4:5]))], contourIdx=-1, color=(255,255,0), thickness=-3)
for i, j in self.overlay_edges:
(x_start, y_start), (x_end, y_end) = verts[i], verts[j]
cv2.line(img, (int(x_start), int(y_start)), (int(x_end), int(y_end)), self.color_lines, 2)
if __name__ == '__main__':
Tracker(0, 0.8, 'Augmented Reality').start()
让我们看一下代码
Tracker类用于执行此处的所有计算。 我们使用通过边和顶点定义的金字塔结构初始化该类。 我们用来跟踪表面的逻辑与我们之前讨论的相同,因为我们使用的是同一类。 我们只需要使用solvePnP和projectPoints将 3D 金字塔映射到 2D 曲面即可。
让我们添加一些动作
现在我们知道如何添加虚拟金字塔,让我们看看是否可以添加一些移动。 让我们看看如何动态更改金字塔的高度。 当您开始时,金字塔将如下所示:

如果您等待一段时间,金字塔会更高,看起来像这样:

让我们看看如何在 OpenCV Python 中做到这一点。 在我们刚刚讨论的增强现实代码中,在Tracker类的__init__方法的末尾添加以下代码段:
self.overlay_vertices = np.float32([[0, 0, 0], [0, 1, 0], [1, 1, 0], [1, 0, 0], [0.5, 0.5, 4]])
self.overlay_edges = [(0, 1), (1, 2), (2, 3), (3, 0),
(0,4), (1,4), (2,4), (3,4)]
self.color_base = (0, 255, 0)
self.color_lines = (0, 0, 0)
self.graphics_counter = 0
self.time_counter = 0
现在我们有了结构,我们需要添加代码以动态更改高度。 用以下方法替换overlay_graphics()方法:
def overlay_graphics(self, img, tracked):
x_start, y_start, x_end, y_end = tracked.target.rect
quad_3d = np.float32([[x_start, y_start, 0], [x_end,
y_start, 0],
[x_end, y_end, 0], [x_start, y_end, 0]])
h, w = img.shape[:2]
K = np.float64([[w, 0, 0.5*(w-1)],
[0, w, 0.5*(h-1)],
[0, 0, 1.0]])
dist_coef = np.zeros(4)
ret, rvec, tvec = cv2.solvePnP(objectPoints=quad_3d, imagePoints=tracked.quad,
cameraMatrix=K, distCoeffs=dist_coef)
verts = self.overlay_vertices * \
[(x_end-x_start), (y_end-y_start), -(x_end-x_start)*0.3] + (x_start, y_start, 0)
verts = cv2.projectPoints(verts, rvec, tvec, cameraMatrix=K,
distCoeffs=dist_coef)[0].reshape(-1, 2)
verts_floor = np.int32(verts).reshape(-1,2)
cv2.drawContours(img, contours=[verts_floor[:4]],
contourIdx=-1, color=self.color_base, thickness=-3)
cv2.drawContours(img, contours=[np.vstack((verts_floor[:2],
verts_floor[4:5]))], contourIdx=-1, color=(0,255,0), thickness=-3)
cv2.drawContours(img, contours=[np.vstack((verts_floor[1:3],
verts_floor[4:5]))], contourIdx=-1, color=(255,0,0), thickness=-3)
cv2.drawContours(img, contours=[np.vstack((verts_floor[2:4],
verts_floor[4:5]))], contourIdx=-1, color=(0,0,150), thickness=-3)
cv2.drawContours(img, contours=[np.vstack((verts_floor[3:4],
verts_floor[0:1], verts_floor[4:5]))], contourIdx=-1, color=(255,255,0),thickness=-3)
for i, j in self.overlay_edges:
(x_start, y_start), (x_end, y_end) = verts[i], verts[j]
cv2.line(img, (int(x_start), int(y_start)), (int(x_end), int(y_end)),
self.color_lines, 2)
现在我们知道了如何改变高度,让我们继续为我们做金字塔舞。 我们可以使金字塔的尖端周期性地振荡。 因此,当您开始时,它将如下所示:

如果您等待一段时间,它将如下所示:

您可以在augmented_reality_motion.py中查看实现的详细信息。
在下一个实验中,我们将使整个金字塔在兴趣区域内移动。 我们可以使它以我们想要的任何方式移动。 让我们开始添加围绕选定兴趣区域的线性对角线运动。 当您开始时,它将如下所示:

一段时间后,它将如下所示:

请参阅augmented_reality_dancing.py以了解如何更改overlay_graphics()方法使其跳舞。 让我们看看是否可以使金字塔绕我们兴趣区域旋转。 当您开始时,它将如下所示:

一段时间后,它将移至新位置:

您可以参考augmented_reality_circular_motion.py来了解如何实现。 您可以使其做任何您想做的事情。 您只需要提出正确的数学公式,金字塔就会如您所愿地跳动! 您还可以尝试其他虚拟对象,以了解如何使用它。 您可以使用许多不同的对象执行很多操作。 这些示例提供了很好的参考点,您可以在这些参考点上构建许多有趣的增强现实应用。
总结
在本章中,您了解了增强现实的前提,并了解了增强现实系统的外观。 我们讨论了增强现实所需的几何变换。 您还学习了如何使用这些转换来估计相机的姿势,并学习了如何跟踪*面对象。 我们讨论了如何在现实世界的顶部添加虚拟对象。 您学习了如何以不同方式修改虚拟对象以添加炫酷效果。
在下一章中,我们将学习如何将机器学习技术与人工神经网络一起应用,这将有助于我们增强第 9 章,“对象识别”中已经获得的知识。
十一、通过人工神经网络的机器学习
在本章中,您将学习如何构建 ANN 并对其进行训练以执行图像分类和对象识别。 人工神经网络是机器学习的子集之一,我们将特别讨论 MLP 网络,它是模式识别范围内最常见的神经网络类型。
在本章的最后,我们将介绍以下内容:
- 机器学习(ML)和人工神经网络(ANN)之间的区别
- 多层感知器(MLP)网络
- 如何定义和实现 MLP 网络
- 评估和改善我们的人工神经网络
- 如何使用经过训练的 ANN 识别图像中的对象
机器学习(ML)与人工神经网络(ANN)
如前所述,ANN 是 ML 的子集。 人工神经网络的灵感来自人类的理解; 它们像大脑一样工作,由不同的相互连接的神经元层组成,每个神经元从上一层接收信息,对其进行处理,然后将其发送到下一层,直到接收到最终输出。 在监督学习的情况下,此输出可以来自标记的输出,在监督学习的情况下,此输出可以来自某些匹配的条件。
人工神经网络的特点是什么? 机器学习被定义为计算机科学领域,专注于尝试在数据集中查找模式,而 ANN 则更侧重于模拟人脑如何连接以完成这项工作,将模式检测划分为多个层(称为节点) 神经元。
同时,其他的机器学习算法,例如支持向量机(SVM),在对象模式识别和分类上也越来越流行。 SVM 具有机器学习算法中最好的准确率之一。 ANN 具有更多的应用集,能够检测大多数类型的数据结构上的模式(SVM 主要与特征向量一起使用),并且可以进行更多参数化以在同一实现中实现不同的目标。
此外,与其他 ML 策略(例如 SVM)相比,ANN 的另一个优势是 ANN 是一种概率分类器,允许进行多类分类。 这意味着它可以检测图像中的多个物体。 另一方面,SVM 是一种非概率二分类器。
ANN 什么时候有用? 想象一下,我们已经实现了一个对象识别器,该对象识别器经过训练可以识别背包和鞋子,然后得到以下图像:

我们在其上运行特征检测器,并获得如下结果:

从上一张图像可以看到,我们的特征检测器算法从女孩的背包和鞋子中获得了特征向量。 因此,如果我们在此图像上运行第 9 章,“对象识别”的 SVM 分类器,由于采用了线性分类器,它宁愿只检测背包 即使图像中也包含鞋类。
SVM 还可以使用称为核技巧的东西执行非线性分类,并将输入隐式映射到高维特征空间。
ANN 如何工作?
在本节中,我们将看到哪些元素参与了 ANN-MLP。 首先,我们将代表一个常规的 ANN-MLP 形状,每层的输入,输出和隐藏以及信息如何在它们之间流动:

MLP 网络至少由三层组成:
- 输入层:每个 MLP 始终具有这些层之一。 它是一个被动层,这意味着它不会修改数据。 它从外界接收信息,并将其发送到网络。 该层中节点(神经元)的数量将取决于我们要从图像中提取的特征或描述性信息的数量。 例如,在使用特征向量的情况下,向量中的每一列将有一个节点。
- 隐藏层:所有基础工作都在此层进行。 它将输入转换为输出层或另一个隐藏层可以使用的东西(可以有多个)。 该层充当黑匣子,在接收到的输入中检测模式并评估每个输入的权重。 其行为将由其激活函数提供的方程式定义。
- 输出层:此层也将始终存在,但是在这种情况下,节点数将由所选的神经网络定义。 该层可能具有三个神经元。 输出层可以由单个节点构建(线性回归),也就是说,我们想知道图像是否带有背包。 但是在进行多类分类的情况下,该层将包含几个可以识别的节点,每个对象一个。 默认情况下,每个节点都会产生一个值,该值的范围为
[-1,1],该值定义对象是否存在的可能性,并允许在单个输入图像上进行多类检测。
假设我们要构建一个三层神经网络,每个神经网络之一:输入,隐藏和输出。 输入层中的节点数将由我们数据的维数决定。 输出层中的节点数将由我们拥有的模型数来定义。 关于隐藏层,节点或什至层的数量将取决于问题的复杂性和我们要添加到网络中的准确率。 高维数将提高结果的准确率,但也会增加计算成本。 对于隐藏层要采取的另一个决定是使用激活函数,该函数使我们能够拟合非线性假设并根据所提供的数据获得更好的模式检测。 激活函数的常见选择是Sigmoid函数,默认情况下使用该函数,其中会根据概率评估输出,但还有其他选择,例如 tanh 或 ReLU 。
更深入地研究具有隐藏层的每个神经元,我们可以说它们所有的行为都类似。 从上一层(输入节点)中检索值,并与某些权重(每个神经元各自)加偏差项相加。 使用激活函数f转换总和,该函数对于不同的神经元也可能有所不同,如下图所示:

如何定义多层感知器(MLP)
MLP 是 ANN 的一个分支,由于其能够在嘈杂或意外环境中识别模式,因此广泛用于模式识别。 MLP 可用于实现有监督和无监督的学习(在第 9 章,“对象识别”中都对它们进行了讨论)。 除此之外,MLP 还可以用于实现另一种学习,例如受行为心理学启发的强化学习,其中使用奖励/惩罚行为来调整网络学习。
定义 ANN-MLP 包括确定组成我们的网络的层的结构以及每个层中有多少个节点。 首先,我们需要确定我们网络的目标是什么。 例如,我们可以实现一个对象识别器,在这种情况下,属于输出层的节点数量将与我们要识别的不同对象的数量相同。 模拟第 9 章中的示例,对象识别,在识别手袋,鞋类和衣服的情况下,输出层将具有三个节点,其值将映射为概率元组而不是固定的元组值,例如[1,0,0],[0,1,0]和[0,0,1]。 因此,有可能在同一幅图像中识别一个以上的类,例如,一个背包穿拖鞋的女孩。
一旦确定了网络的结果,就应该定义可以将每个物体要识别的有意义的信息插入到我们的网络中,从而能够将对象识别为未知图像。 有几种方法作为图像的特征描述符。 我们可以使用定向直方图(HOG)来统计图像局部区域中梯度方向的出现,或使用彩色直方图来表示图像中的颜色分布,或者我们也可以使用具有 SIFT 或 SURF 算法的密集特征检测器提取图像特征。 由于插入输入层的每个图像的描述符数量必须相同,因此我们将使用词袋策略,将所有描述符集收集到单个视觉词直方图中,就像我们在第 9 章,“对象识别”,供使用 SVM 识别器。 直方图如下所示,其中每个条形值都将链接到输入层中的一个节点:

最后,我们进入隐藏层。 该层没有严格定义的结构,因此将是一个复杂的决定。 关于如何确定隐藏层的数量以及其中的节点数量,不同的研究人员之间进行了大量讨论。 它们全都依靠问题的复杂性来解决,并在性能和准确率之间找到*衡—更多的节点/层将具有更高的准确率,但性能却很差。 同样,可能会导致大量节点,并且网络过度安装不仅会导致性能降低,而且还会导致精度降低。 对于只有三个模型的简单对象识别器,它不需要一个以上的隐藏层,对于其中的节点数,我们可以采用 Heaton 研究,它设置了以下规则:
- 隐藏神经元的数量应在输入层的大小和输出层的大小之间
- 隐藏神经元的数量应为输入层大小的三分之二加上输出层大小的三分之二
- 隐藏神经元的数量应小于输入层大小的两倍
如何实现 ANN-MLP 分类器?
在对如何实现人工神经网络进行了所有理论解释之后,我们将自己实现。 为此,就像我们在 SVM 分类器中所做的一样,我们将从相同的源下载训练图像。 。 我们将从几个可以轻松扩展到其他项目的项目开始,创建一个文件夹images,为我们要分类的每个类别创建一个子文件夹:dresses,footwear和bagpack。 我们将为它们分别拍摄一堆图像; 大约 20 到 25 张图像应该足以进行训练,最重要的是,我们将包括另一组样本图像,我们将使用它们来评估训练后网络的准确率。
如前所述,我们需要使用词袋(BOW)对齐每个图像的描述符数量。 为此,我们将首先使用密集特征检测器为每个图像馈送的关键点提取每个图像的特征向量,然后将向量转发到 K 均值聚类以提取质心,这将帮助我们最终获得 BOW 。

从上一张图像可以看出,这与我们在 SVM 分类器中实现的过程相同。 为了节省时间和代码,我们将利用先前创建的create_features.py文件来提取所有将用作 MLP 网络输入的特征描述符。
通过运行以下命令,我们将获得下一步所需的每个映射文件:
$ python create_features.py --samples bag images/bagpack/ --samples dress images/dress/ --samples footwear images/footwear/ --codebook-file models/codebook.pkl --feature-map-file models/feature_map.pkl
在feature_map.pkl文件中,我们拥有训练期间将参与的每个图像的特征向量。 首先,我们将为 ANN 分类器创建一个类,在其中设置网络层的大小:
from sklearn import preprocessing
import numpy as np
import cv2
import random
class ClassifierANN(object):
def __init__(self, feature_vector_size, label_words):
self.ann = cv2.ml.ANN_MLP_create()
self.label_words = label_words
# Number of centroids used to build the feature vectors
input_size = feature_vector_size
# Number of models to recongnize
output_size = len(label_words)
# Applying Heaton rules
hidden_size = (input_size * (2/3)) + output_size
nn_config = np.array([input_size, hidden_size, output_size], dtype=np.uint8)
self.ann.setLayerSizes(np.array(nn_config))
# Symmetrical Sigmoid as activation function
self.ann.setActivationFunction(cv2.ml.ANN_MLP_SIGMOID_SYM, 2, 1)
# Map models as tuples of probabilities
self.le = preprocessing.LabelBinarizer()
self.le.fit(label_words) # Label words are ['dress', 'footwear', 'backpack']
作为输出,我们决定用二进制数[0,0,1],[0,1,0],[1,0,0]实现一个概率元组,目的是通过这种方式获得多类检测。 作为激活函数对称 Sigmoid(NN_MLP_SIGMOID_SYM),这是 MLP 的默认选择,其中输出将在[-1,1]范围内。 这样,我们的网络生成的输出将定义概率而不是分类结果,从而能够识别同一样本图像中的两个或三个对象。
在训练过程中,我们将数据集分为两个不同的集:训练和测试。 我们将为其定义一个比率(通常,大多数示例建议使用 75% 作为训练集,但可以对其进行调整,直到获得最佳准确率为止),并随机选择项目以防止偏差。 这是如何运作的?
class ClassifierANN(object):
...
def train(self, training_set):
label_words = [ item['label'] for item in training_set]
dim_size = training_set[0]['feature_vector'].shape[1]
train_samples = np.asarray(
[np.reshape(x['feature_vector'], (dim_size,)) for x in training_set]
)
# Convert item labels into encoded binary tuples
train_response = np.array(self.le.transform(label_words), dtype=np.float32)
self.ann.train(np.array(train_samples,
dtype=np.float32),cv2.ml.ROW_SAMPLE,
np.array(train_response, dtype=np.float32)
)
在这种情况下,我们对输入层的每个节点使用了相同的权重(默认行为),但是我们可以指定它们为特征向量中的列提供更多权重,并提供更多重要信息。
评估训练后的网络
为了评估我们训练有素的 MLP 网络的鲁棒性和准确率,我们将计算混淆矩阵(也称为误差矩阵)。 该矩阵将描述我们分类模型的表现。 混淆矩阵的每一行代表预测类中的实例,而每一列代表实际类中的实例(反之亦然)。 为了填充矩阵,我们将使用测试集对其进行评估:
from collections import OrderedDict
def init_confusion_matrix(self, label_words):
confusion_matrix = OrderedDict()
for label in label_words:
confusion_matrix[label] = OrderedDict()
for label2 in label_words: confusion_matrix[label][label2] = 0
return confusion_matrix
# Chooses the class with the greatest value, only one, in the tuple(encoded_word)
def classify(self, encoded_word, threshold=0.5):
models = self.le.inverse_transform(np.asarray([encoded_word]), threshold)
return models[0]
# Calculate the confusion matrix from given testing data set
def get_confusion_matrix(self, testing_set):
label_words = [item['label'] for item in testing_set]
dim_size = testing_set[0]['feature_vector'].shape[1]
test_samples = np.asarray(
[np.reshape(x['feature_vector'], (dim_size,)) for x in testing_set]
)
expected_outputs = np.array(self.le.transform(label_words), dtype=np.float32)
confusion_matrix = self._init_confusion_matrix(label_words)
retval, test_outputs = self.ann.predict(test_samples)
for expected_output, test_output in zip(expected_outputs, test_outputs):
expected_model = self.classify(expected_output)
predicted_model = self.classify(test_output)
confusion_matrix[expected_model][predicted_model] += 1
return confusion_matrix
作为样本混淆矩阵,并考虑一个包含 30 个元素的测试集,我们可能获得以下结果:
| 鞋子 | 背包 | 连衣裙 | |
|---|---|---|---|
| 鞋子 | 8 | 2 | 0 |
| 背包 | 2 | 7 | 1 |
| 连衣裙 | 2 | 2 | 6 |
考虑到先前的矩阵,我们可以通过以下公式计算训练网络的准确率:

在此公式中,我们表示真正例(TP),真负例(TN),假正例(FP)和假负例(FN)。 就鞋类而言,我们可以说其准确率为 80%。

上式的实现代码如下:
def calculate_accuracy(confusion_matrix):
acc_models = OrderedDict()
for model in confusion_matrix.keys():
acc_models[model] = {'TP':0, 'TN':0, 'FP':0, 'FN': 0}
for expected_model, predicted_models in confusion_matrix.items():
for predicted_model, value in predicted_models.items():
if predicted_model == expected_model:
acc_models[expected_model]['TP'] += value
acc_models[predicted_model]['TN'] += value
else:
acc_models[expected_model]['FN'] += value
acc_models[predicted_model]['FP'] += value
for model, rep in acc_models.items():
acc = (rep['TP']+rep['TN'])/(rep['TP']+rep['TN']+rep['FN']+rep['FP'])
print('%s \t %f' % (model,acc))
收集本节中的每个代码块,我们已经实现了ClassifierANN类以供使用:
###############
# training.py
###############
import pickle
def build_arg_parser():
parser = argparse.ArgumentParser(description='Creates features for given images')
parser.add_argument("--feature-map-file", dest="feature_map_file", required=True,
help="Input pickle file containing the feature map")
parser.add_argument("--training-set", dest="training_set", required=True,
help="Percentage taken for training. ie 0.75")
parser.add_argument("--ann-file", dest="ann_file", required=False,
help="Output file where ANN will be stored")
parser.add_argument("--le-file", dest="le_file", required=False,
help="Output file where LabelEncoder class will be stored")
if __name__ == '__main__':
args = build_arg_parser().parse_args()
# Load the Feature Map
with open(args.feature_map_file, 'rb') as f:
feature_map = pickle.load(f)
training_set, testing_set = split_feature_map(feature_map, float(args.training_set))
label_words = np.unique([item['label'] for item in training_set])
cnn = ClassifierANN(len(feature_map[0]['feature_vector'][0]), label_words)
cnn.train(training_set)
print("===== Confusion Matrix =====")
confusion_matrix = cnn.get_confusion_matrix(testing_set)
print(confusion_matrix)
print("===== ANN Accuracy =====")
print_accuracy(confusion_matrix)
if 'ann_file' in args and 'le_file' in args:
print("===== Saving ANN =====")
with open(args.ann_file, 'wb') as f:
cnn.ann.save(args.ann_file)
with open(args.le_file, 'wb') as f:
pickle.dump(cnn.le, f)
print('Saved in: ', args.ann_file)
您可能已经注意到,我们已经将 ANN 保存到两个单独的文件中,因为ANN_MLP类具有自己的保存和加载方法。 我们需要保存用于训练网络的label_words。 Pickle 为我们提供了对对象结构进行序列化和反序列化以及从磁盘保存和加载它们的功能,除了ann这样的结构有自己的实现。
运行以下命令以获取模型文件。 混淆矩阵和准确率概率将与其一起显示:
$ python training.py --feature-map-file models/feature_map.pkl --training-set 0.8 --ann-file models/ann.yaml --le-file models/le.pkl
为了获得训练有素的网络,我们可以根据需要重复执行多次,直到获得良好的精度结果为止。 发生这种情况是因为训练和测试集是随机抽取的,因此我们应该保留结果更好的那个。
图片分类
要实现我们的 ANN 分类器,我们将需要重用第 9 章,“对象识别”中create_feature.py文件中FeatureExtractor类的方法,这将使我们能够计算我们要评估的图像中的特征向量:
class FeatureExtractor(object):
def get_feature_vector(self, img, kmeans, centroids):
return Quantizer().get_feature_vector(img, kmeans, centroids)
考虑将create_feature文件包含在同一文件夹中。 现在,我们准备实现分类器:
###############
# classify_data.py
###############
import argparse
import _pickle as pickle
import cv2
import numpy as np
import create_features as cf
# Classifying an image
class ImageClassifier(object):
def __init__(self, ann_file, le_file, codebook_file):
with open(ann_file, 'rb') as f:
self.ann = cv2.ml.ANN_MLP_load(ann_file)
with open(le_file, 'rb') as f:
self.le = pickle.load(f)
# Load the codebook
with open(codebook_file, 'rb') as f:
self.kmeans, self.centroids = pickle.load(f)
def classify(self, encoded_word, threshold=None):
models = self.le.inverse_transform(np.asarray(encoded_word), threshold)
return models[0]
# Method to get the output image tag
def getImageTag(self, img):
# Resize the input image
img = cf.resize_to_size(img)
# Extract the feature vector
feature_vector = cf.FeatureExtractor().get_feature_vector(img, self.kmeans, self.centroids)
# Classify the feature vector and get the output tag
retval, image_tag = self.ann.predict(feature_vector)
return self.classify(image_tag)
def build_arg_parser():
parser = argparse.ArgumentParser(
description='Extracts features from each line and classifies the data')
parser.add_argument("--input-image", dest="input_image", required=True,
help="Input image to be classified")
parser.add_argument("--codebook-file", dest="codebook_file", required=True,
help="File containing the codebook")
parser.add_argument("--ann-file", dest="ann_file", required=True,
help="File containing trained ANN")
parser.add_argument("--le-file", dest="le_file", required=True,
help="File containing LabelEncoder class")
return parser
if __name__=='__main__':
args = build_arg_parser().parse_args()
codebook_file = args.codebook_file
input_image = cv2.imread(args.input_image)
tag = ImageClassifier(args.ann_file, args.le_file, codebook_file).getImageTag(input_image)
print("Output class:", tag)
运行以下命令对图像进行分类:
$ python classify_data.py --codebook-file models/codebook.pkl --ann-file models/ann.yaml --le-file models/le.pkl --input-imageimg/test.png
总结
在本章中,您学习了 ANN 的概念。 您还了解到,它在对象识别领域的用途之一是 MLP 的实现,包括 MLP 相对于其他机器学习策略(例如 SVM)的优缺点。 关于 ANN-MLP,您了解了哪些层形成其结构,以及如何定义和实现它们以构建图像分类器,然后学习了如何评估 MLP,训练其鲁棒性和准确率。 在上一节中,我们实现了一个 MLP 的示例来检测未知图像中的物体。
请记住,计算机视觉世界充满了无限的可能性! 本书旨在教您入门各种项目所需的技能。 现在,由您和您的想象力来使用您在这里获得的技能来构建一些独特而有趣的东西。


浙公网安备 33010602011771号