精通-Python-OpenCV4-全-
精通 Python OpenCV4(全)
零、前言
简而言之,这本书是有关使用 OpenCV 的计算机视觉的,它是计算机视觉(也是机器学习)库和 Python 编程语言。 您可能想知道为什么使用 OpenCV 和 Python? 这确实是一个好问题,我们将在本书的第一章中解决。 总而言之,OpenCV 是最好的开源计算机视觉库(BSD 许可证,它对学术和商业都是免费的),提供了 2500 多种优化算法,包括最新的计算机视觉算法,并且还具有机器学习和深度学习支持。 OpenCV 用优化的 C/C++ 编写,但是提供了 Python 包装器。 因此,该库可以在您的 Python 程序中使用。 从这个意义上讲,Python 被认为是科学计算的理想语言,因为它可以刺激快速原型开发,并为计算机视觉项目的各个方面提供了许多预构建的库。
如前一段所述,您可以在项目中使用许多预构建的库。 确实,在本书中,我们使用了很多库,向您展示了安装和使用新库确实非常容易。 本书中将使用 Matplotlib,scikit-image,SciPy,Dlib,人脸识别,Pillow,Cvlib,Keras,TensorFlow 和 Flask 之类的库来向您展示 Python 生态系统的潜力。 如果您是第一次阅读这些库,请不要担心,因为我们为几乎所有这些库引入了 hello world 示例。
这本书是使用 Python 和 OpenCV 使用各种技术(例如面部识别,目标跟踪,增强现实,对象检测和分类等)创建高级应用的完整资源。 此外,本书
探索了使用 Python 生态系统在机器视觉应用中机器学习和深度学习技术的潜力。
现在该深入了解本书的内容了。 我们将向您介绍本书的内容,包括一段简短的段落,介绍本书的每一章。 所以,让我们开始吧!
这本书是给谁的
本书非常适合具有 Python 基本编程知识的学生,研究人员和开发人员,这些知识对于计算机视觉来说是新手,他们想更深入地研究这个世界。 假定读者以前有使用 Python 的经验。 对图像数据(例如,像素和颜色通道)的基本理解也将有所帮助,但不是必需的,因为本书中涵盖了这些概念。 最后,需要标准的数学技能。
本书涵盖的内容
第 1 章,“设置 OpenCV”显示了如何安装开始使用 Python 和 OpenCV 进行编程所需的所有内容。 还将向您介绍一般的术语和概念,以使您将学到的内容相关联,并使用 OpenCV 建立和设置与计算机视觉主要概念相关的基础。
第 2 章,“OpenCV 中的图像基础”演示了如何开始编写您的第一个脚本,以向您介绍 OpenCV 库。
第 3 章,“处理文件和图像”向您展示如何处理文件和图像,这是构建计算机视觉应用所必需的。
第 4 章,“在 OpenCV 中构建基本形状”涵盖了如何使用 OpenCV 库绘制形状-从基本形状到更高级的形状。
第 5 章,“图像处理技术”介绍了计算机视觉项目所需的大多数常见图像处理技术。
第 6 章,“构造和构建直方图”展示了如何创建和理解直方图,这是了解图像内容的强大工具。
第 7 章,“阈值处理技术”介绍了计算机视觉应用所需的主要阈值处理技术,这些技术是图像分割的关键过程。
第 8 章,“轮廓检测,滤波和绘图”显示了如何处理轮廓,这些轮廓用于形状分析以及对象检测和识别。
第 9 章,“增强现实”,教您如何构建第一个增强现实应用。
第 10 章,“使用 OpenCV 的机器学习”向您介绍机器学习的世界。 您将看到如何在计算机视觉项目中使用机器学习。
第 11 章,“人脸检测,跟踪和识别”展示了如何使用最新算法,结合人脸检测,跟踪和识别来创建人脸处理项目 。
第 12 章,“深度学习简介”向您介绍使用 OpenCV 进行深度学习的世界,以及一些深度学习 Python 库(TensorFlow 和 Keras)。
第 13 章,“使用 Python 和 OpenCV 的移动和 Web 计算机视觉”展示了如何使用 Flask 创建计算机视觉和深度学习 Web 应用。
充分利用这本书
为了充分利用本书,您必须考虑两个简单但重要的考虑因素:
- 由于本书中的所有脚本和示例均使用 Python,因此假定您具备一些 Python 编程的基本知识。
- NumPy 和 OpenCV-Python 包高度互连(您将在本书中了解原因)。 尽管已经充分说明了 NumPy 的示例,但是如果在开始本书之前获得了一些 NumPy 的知识,则可以缓解学习曲线。
下载示例代码文件
您可以从 www.packt.com 的帐户中下载本书的示例代码文件。 如果您在其他地方购买了此书,则可以访问 www.packt.com/support 并注册以将文件直接通过电子邮件发送给您。
您可以按照以下步骤下载代码文件:
- 登录或注册 www.packt.com 。
- 选择支持选项卡。
- 单击代码下载和勘误。
- 在搜索框中输入书籍的名称,然后按照屏幕上的说明进行操作。
下载文件后,请确保使用以下最新版本解压缩或解压缩文件夹:
- Windows 的 WinRAR/7-Zip
- Mac 的 Zipeg/iZip/UnRarX
- Linux 的 7-Zip/PeaZip
本书的代码包也托管在 GitHub 上。 如果代码有更新,它将在现有 GitHub 存储库上进行更新。
我们还从这里提供了丰富的书籍和视频目录中的其他代码包。 去看一下!
下载彩色图像
我们还提供了 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。 您可以在此处下载。
使用约定
本书中使用了许多文本约定。
CodeInText
:指示文本,数据库表名称,文件夹名称,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄中的代码字。 这是一个示例:“接下来提供build_sample_image()
的代码。”
代码块设置如下:
channels = cv2.split(img)
eq_channels = []
for ch in channels:
eq_channels.append(cv2.equalizeHist(ch))
当我们希望引起您对代码块特定部分的注意时,相关行或项目以粗体显示:
Hu moments (original): '[ 1.92801772e-01 1.01173781e-02 5.70258405e-05 1.96536742e-06 2.46949980e-12 -1.88337981e-07 2.06595472e-11]'
Hu moments (rotation): '[ 1.92801772e-01 1.01173781e-02 5.70258405e-05 1.96536742e-06 2.46949980e-12 -1.88337981e-07 2.06595472e-11]'
Hu moments (reflection): '[ 1.92801772e-01 1.01173781e-02 5.70258405e-05 1.96536742e-06 2.46949980e-12 -1.88337981e-07 -2.06595472e-11]'
任何命令行输入或输出的编写方式如下:
$ mkdir opencv-project
$ cd opencv-project
粗体:表示新术语,重要单词或您在屏幕上看到的单词。 例如,菜单或对话框中的单词会出现在这样的文本中。 这是一个示例:“从管理面板中选择系统信息。”
警告或重要提示如下所示。
提示和技巧如下所示。
一、设置 OpenCV
使用 Python 精通 OpenCV 4 将为您提供有关构建涉及开源计算机视觉库(OpenCV)和 Python 的项目的知识。 将介绍这两种技术(第一种是编程语言,第二种是计算机视觉和机器学习库)。 另外,您还将了解为什么将 OpenCV 和 Python 结合使用具有构建各种计算机应用的潜力。 最后,将介绍与本书内容有关的主要概念。
在本章中,将逐步指导您安装开始使用 Python 和 OpenCV 进行编程所需的一切。 第一章很长,但是不用担心,因为它被分为容易理解的部分,从一般的术语和概念开始,假定读者是新手。 在本章的最后,您将能够构建第一个涉及 Python 和 OpenCV 的项目。
本章将涵盖以下主题:
- OpenCV 库的理论介绍
- 安装 Python OpenCV 和其他包
- 运行示例,文档,帮助和更新
- Python 和 OpenCV 项目结构
- 第一个 Python 和 OpenCV 项目
技术要求
本章及后续各章重点讨论与计算机视觉,机器学习和深度学习技术(以及其他技术)相关的 Python(一种编程语言)和 OpenCV(一个计算机视觉库)概念。 因此,应该在计算机上安装 Python 和 OpenCV。 此外,还应该安装一些与科学计算和数据科学有关的 Python 包(例如 NumPy 或 Matplotlib。
此外,建议您安装集成开发环境(IDE)包,因为它有助于计算机程序员进行软件开发。 从这个意义上讲,建议使用 Python 特定的 IDE。 实际上是 Python IDE 是 PyCharm,可以从这里下载。
最后,为了促进 GitHub 活动(例如,克隆存储库),您应该安装 Git 客户端。 从这个意义上讲,GitHub 提供了包括最常见的存储库操作的桌面客户端。 有关 Git 命令的介绍,请查看这里,其中总结了常用的 Git 命令行说明。 此外,还包括在操作系统上安装 Git 客户端的说明。
可从这里访问本书的 GitHub 存储库,其中包含从本书第一章到最后一章所需的所有支持项目文件。
最后,应该注意的是,使用 Python 精通 OpenCV 的 GitHub 存储库的 README 文件包括以下内容,出于完整性考虑,此文件也附在此处:
- 代码测试规范
- 硬件规格
- 相关书籍和产品
代码测试规范
使用 Python 精通 OpenCV 4 需要一些已安装的包,您可以在这里查看:
- 第 1 章,“设置 OpenCV”:
opencv-contrib-python
- 第 2 章,“OpenCV 中的图像基础”:
opencv-contrib-python
和matplotlib
- 第 3 章,“处理文件和图像”:
opencv-contrib-python
和matplotlib
- 第 4 章,“在 OpenCV 中构建基本形状”:
opencv-contrib-python
和matplotlib
- 第 5 章,“图像处理技术”:
opencv-contrib-python
和matplotlib
- 第 6 章,“直方图的构建”:
opencv-contrib-python
和matplotlib
- 第 7 章,“阈值处理技术”:
opencv-contrib-python
,matplotlib
,scikit-image
和scipy
- 第 8 章,“轮廓检测,过滤和绘制”:
opencv-contrib-python
和matplotlib
- 第 9 章,“增强现实”:
opencv-contrib-python
和matplotlib
- 第 10 章,“使用 OpenCV 的机器学习”:
opencv-contrib-python
和matplotlib
- 第 11 章,“人脸检测,跟踪和识别”:
opencv-contrib-python
,matplotlib
,dlib
,face-recognition
,cvlib
,requests
,progressbar
,keras
和tensorflow
- 第 12 章,“深度学习简介”:
opencv-contrib-python
,matplotlib
,tensorflow
和keras
- 第 13 章,“使用 Python 和 OpenCV 的移动和 Web 计算机视觉”:
opencv-contrib-python
,matplotlib
,flask
,tensorflow
,keras
,requests
和pillow
确保已安装包的版本号等于或大于此处指定的版本,以确保代码示例正确运行。
如果要安装本书经过测试的确切版本,请从pip
安装时包括该版本,如下所示。
运行以下命令以安装主要模块和贡献模块:
- 安装
opencv-contrib-python
:
pip install opencv-contrib-python==4.0.0.21
应该注意的是,OpenCV 需要numpy
。 安装opencv-contrib-python==4.0.0.21
时已安装numpy-1.16.1
。
运行以下命令以安装 Matplotlib 库:
- 安装
matplotlib
:
pip install matplotlib==3.0.2
应当注意,matplotlib
需要kiwisolver
,pyparsing
,six
,cycler
和python-dateutil
。
安装matplotlib==3.0.2
时,已经安装了cycler-0.10.0
,kiwisolver-1.0.1
,pyparsing-2.3.1
,python-dateutil-2.8.0
和six-1.12.0
。
运行以下命令以安装库,该库包含用于图像处理的算法的集合:
- 安装
scikit-image
:
pip install scikit-image==0.14.2
应当注意,scikit-image
需要cloudpickle
,decorator
,networkx
,numpy
,toolz
,dask
,pillow
,PyWavelets
和six
。
安装scikit-image==0.14.2
时,已安装PyWavelets-1.0.1
,cloudpickle-0.8.0
,dask-1.1.1
,decorator-4.3.2
,networkx-2.2
,numpy-1.16.1
,pillow-5.4.1
,six-1.12.0
和toolz-0.9.0
。
如果需要 SciPy,可以使用以下命令进行安装:
- 安装
scipy
:
pip install scipy==1.2.1
应当注意,scipy
需要numpy
。
安装scipy==1.2.1
时已安装numpy-1.16.1
。
运行以下命令以安装dlib
库:
- 安装
dlib
:
pip install dlib==19.8.1
要安装面部识别库,请运行以下命令:
- 安装
face-recognition
:
pip install face-recognition==1.2.3
应当注意,face-recognition
需要dlib
,Click
,numpy
,face-recognition-models
和pillow
。
安装face-recognition==1.2.3
时,已经安装了dlib-19.8.1
,Click-7.0
,face-recognition-models-0.3.0
和pillow-5.4.1
。
运行以下命令以安装开源计算机视觉库:
- 安装
cvlib
:
pip install cvlib==0.1.8
要安装请求库,请运行以下命令:
- 安装
requests
:
pip install requests==2.21.0
应当注意,requests
需要urllib3
,chardet
,certifi
和idna
。
安装requests==2.21.0
时,已经安装了urllib3-1.24.1
,chardet-3.0.4
,certifi-2018.11.29
和idna-2.8
。
运行以下命令以安装文本进度栏库:
- 安装
progressbar
:
pip install progressbar==2.5
运行以下命令以安装 Keras 库以进行深度学习:
- 安装
keras
:
pip install keras==2.2.4
应当注意,keras
需要numpy
,six
,h5py
,keras-applications
,scipy
,keras-preprocessing
和pyyaml
。
安装keras==2.2.4
时,已经安装了h5py-2.9.0
,keras-applications-1.0.7
,keras-preprocessing-1.0.9
,numpy-1.16.1 pyyaml-3.13
和scipy-1.2.1 six-1.12.0
。
运行以下命令以安装 TensorFlow 库:
- 安装
tensorflow
:
pip install tensorflow==1.12.0
应该注意的是 TensorFlow 需要termcolor
,numpy
,wheel
,gast
,six
,setuptools
,protobuf
,markdown
,grpcio
,werkzeug
,tensorboard
,absl-py
,h5py
,keras-applications
,keras-preprocessing
和astor
。
termcolor-1.1.0
,numpy-1.16.1
,wheel-0.33.1
,gast-0.2.2
,six-1.12.0, setuptools-40.8.0
,protobuf-3.6.1
,markdown-3.0.1
,grpcio-1.18.0
,werkzeug-0.14.1
,tensorboard-1.12.2
,absl-py-0.7.0
,h5py-2.9.0
,keras-applications-1.0.7
,keras-preprocessing-1.0.9
和astor-0.7.1
已在安装tensorflow==1.12.0
时安装。
运行以下命令以安装 Flask 库:
- 安装
flask
:
pip install flask==1.0.2
应当注意,flask
需要Werkzeug
,click
,itsdangerous
和MarkupSafe Jinja2
。
安装flask==1.0.2
时,已经安装了Jinja2-2.10
,MarkupSafe-1.1.1
,Werkzeug-0.14.1
,click-7.0
和itsdangerous-1.1.0
。
硬件规格
硬件规格如下:
- 32 位或 64 位架构
- 2+ GHz CPU
- 4 GB 内存
- 至少 10 GB 的可用硬盘空间
了解 Python
Python 是具有动态类型系统和自动内存管理的一种解释型高级通用编程语言。 Python 编程语言的官方主页是这里。 在过去的十年中,Python 的普及率稳步上升。 这是因为 Python 是当今一些最令人兴奋和最具挑战性的技术中非常重要的编程语言。 人工智能(AI),机器学习,神经网络,深度学习,物联网(IoT)和机器人技术(以及其他)依靠 Python。
这是 Python 的一些优点:
- Python 被认为是科学计算的理想语言,主要有以下四个原因:
- 这很容易理解。
- 它具有(通过包)科学计算的支持。
- 它消除了其他编程语言所具有的许多复杂性。
- 它具有简单且一致的语法。
- Python 可以促进快速原型设计,因为它有助于轻松编写和执行代码。 的确,与其他编程语言相比,Python 可以用最少五分之一的代码来实现相同的逻辑。
- Python 有许多预建的库(NumPy,SciPy,scikit-learn)可满足您 AI 项目的各种需求。 Python 受益于丰富的科学计算库生态系统。
- 它是一个独立的平台,使开发人员可以节省在不同平台上进行测试的时间。
- Python 提供了一些工具,例如 Jupyter 笔记本,可用于以轻松舒适的方式共享脚本。 这在科学计算中是完美的,因为它可以激发交互式计算环境中的协作。
介绍 OpenCV
OpenCV 是具有实时功能的 C++ 编程库。 由于它是用优化的 C/C++ 编写的,因此该库可以从多核处理中受益。 下一节将对 OpenCV 库进行理论上的介绍。
与 OpenCV 库有关,以下是其受欢迎的一些原因:
- 开源计算机视觉库
- OpenCV(BSD 许可证)是免费的
- 特定的图像处理库
- 它拥有 2500 多种优化算法,包括最新的计算机视觉算法
- 机器学习和深度学习支持
- 该库针对性能进行了优化
- 有大量的开发人员使用和支持 OpenCV
- 它具有 C++,Python,Java 和 MATLAB 接口
- 该库支持 Windows,Linux,Android 和 macOS
- 快速定期更新(现在每六个月发布一次正式发布)
使读者具有上下文
为了使读者具有上下文关系,有必要建立和设置与本书主题相关的主要概念的基础。 最近几年,人们对 AI 和机器学习产生了浓厚的兴趣,特别是在深度学习领域。 这些术语可以互换使用,并且经常相互混淆。 为了完整和清楚起见,下面将简要描述这些术语。
人工智能是指使机器(计算机或机器人系统)能够以与人类相同的方式处理信息的一组技术。
术语“AI”通常用作机器技术的保护伞,以提供涵盖多种方法和算法的智能。 机器学习是对计算机进行编程以从历史数据中学习以对新数据进行预测的过程。 机器学习是 AI 的子学科,是指机器根据学习到的相互关系使用的统计技术。 根据收集或收集的数据,计算机可以独立学习算法。 这些算法和方法包括支持向量机,决策树,随机森林,逻辑回归,贝叶斯网络和神经网络。
神经网络是用于机器学习的计算机模型,该模型基于生物大脑的结构和功能。 人工神经元处理多个输入信号,然后,当输入信号的总和超过某个阈值时,将向其他相邻神经元发送信号。 深度学习是机器学习的子集,它对大量非结构化数据(例如人类语音,文本和图像)进行操作。 深度学习模型是一种人工神经网络,其中包括对数据进行的多层数学计算,其中一层的结果作为输入输入到下一层,以对输入数据进行分类和/或进行预测。
因此,这些概念在层次结构上是相互依存的,AI 是最广义的术语,而深度学习是最具体的术语。 下图可以看到这种结构:
计算机视觉是人工智能的一个跨学科领域,旨在使具有计算能力的计算机和其他设备从数字图像和视频中获得高层次的理解,包括获取,处理的功能 ,并分析数字图像。 这就是为什么计算机视觉在某种程度上是人工智能的另一个子领域的原因,该领域严重依赖于机器学习和深度学习算法来构建计算机视觉应用。 此外,计算机视觉由多种技术共同作用-计算机图形学,图像处理,信号处理,传感器技术 ,数学甚至是物理。
因此,可以完成前面的图来介绍计算机视觉学科:
OpenCV 库的理论介绍
OpenCV 是一个具有实时计算机视觉功能的编程库,它对于学术和商业用途都是免费的(BSD 许可证)。 在本节中,将介绍有关 OpenCV 库的信息,包括其主要模块以及与该库有关的其他有用信息。
OpenCV 模块
OpenCV(从版本 2 开始)分为几个模块,每个模块通常可以理解为专用于一组计算机视觉问题。 在下图中可以看到这种划分,其中显示了主要模块:
OpenCV 模块在此处简短描述:
- core:核心功能。 核心功能是一个定义基本数据结构的模块,也是库中所有其他模块使用的基本功能。
- imgproc:图像处理。 图像处理模块,包括图像过滤,几何图像转换,色彩空间转换和直方图。
- imgcodecs:图像编解码器。 图像文件读写。
- videoio:视频 I/O。 视频捕获和视频编解码器的接口。
- highgui:高级 GUI。 UI 功能的接口。 它提供了一个界面,可以轻松地执行以下操作:
- 创建和操作可显示图像的窗口
- 将跟踪栏添加到窗口,键盘命令并处理鼠标事件
- video:视频分析。 一个视频分析模块,包括背景扣除,运动估计和对象跟踪算法。
- calib3d:相机校准和 3D 重建。 相机校准和 3D 重建涵盖基本的多视图几何算法,立体对应算法,对象姿态估计,单相机和立体相机校准以及 3D 重建。
- features2d:2D 特征框架。 该模块包括特征检测器,描述符和描述符匹配器。
- objdetect:对象检测。 检测对象和预定义类的实例(例如,面部,眼睛,人和汽车)。
- dnn:深度神经网络(DNN)模块。 该模块包含以下内容:
- 用于创建新层的 API
- 一组有用的层
- 从层构建和修改神经网络的 API
- 从不同的深度学习框架加载序列化网络模型的功能
- ml:机器学习。 机器学习库(MLL)是可用于分类,回归和聚类目的的一组类和方法。
- flann:在多维空间中进行聚类和搜索。 用于近似最近邻的快速库(FLANN)是非常适合于快速最近邻搜索的算法集合。
- photo:计算摄影。 该模块提供了一些用于计算摄影的功能。
- stitching:图像拼接。 该模块实现了执行自动全景图像拼接的拼接管线。
- shape:形状距离和匹配。 形状距离和匹配模块,可用于形状匹配,检索或比较。
- superres:超分辨率。 此模块包含一组可用于增强分辨率的类和方法。
- videostab:视频稳定。 此模块包含一组用于视频稳定的类和方法。
- viz:3D 可视化器。 此模块用于显示小部件,这些小部件提供了几种与场景和小部件进行交互的方法。
OpenCV 用户
无论您是专业的软件开发人员还是新手程序员,OpenCV 库都将对图像处理和计算机视觉领域的研究生,研究人员和计算机程序员很感兴趣。 该库已在科学家和学者中广受欢迎,因为该库提供了许多最新的计算机视觉算法。
此外,它通常用作计算机视觉和机器学习的教学工具。 应该考虑到 OpenCV 足够强大以支持实际应用。 因此,OpenCV 可以用于非商业和商业产品。 例如,它被 Google,Microsoft,Intel,IBM,Sony 和 Honda 等公司使用。 MIT,CMU 或 Stanford 等一流大学的研究所为库提供支持。 OpenCV 已被世界各地采用。 它的下载量超过 1400 万,社区中的人口超过 47,000。
OpenCV 应用
OpenCV 正在广泛的应用中使用:
- 2D 和 3D 特征工具包
- 街景图像拼接
- 自我估计
- 面部识别系统
- 手势识别
- 人机交互
- 移动机器人
- 运动理解
- 对象识别
- 自动化检查和监视
- 分割与识别
- 立体视觉 – 两台摄像机的深度感知
- 医学图像分析
- 运动结构
- 运动追踪
- 增强现实
- 视频/图像搜索和检索
- 机器人和无人驾驶汽车的导航和控制
- 驾驶员嗜睡和注意力分散检测
为什么在您的研究工作中引用 OpenCV
如果您在研究中使用 OpenCV,建议您引用 OpenCV 库。 这样,其他研究人员可以更好地理解您提出的算法并重现您的结果,从而获得更好的信誉。 此外,OpenCV 将增加反响,从而产生更好的计算机视觉库。 以下代码显示了引用 OpenCV 的 BibTex 条目:
@article{opencv_library,
author = {Bradski, G.},
citeulike-article-id = {2236121},
journal = {Dr. Dobb's Journal of Software Tools},
keywords = {bibtex-import},
posted-at = {2008-01-15 19:21:54},
priority = {4},
title = {{The OpenCV Library}},
year = {2000}
}
安装 OpenCV,Python 和其他包
OpenCV,Python 和 AI 相关的包可以安装在大多数操作系统上。 我们将看到如何通过不同的方法来安装这些包。
在选择最适合您需要的安装选项之前,请确保检查出不同的安装选项。
另外,由于这些文档的普及,在本章的最后对 Jupyter 笔记本进行了介绍,可以运行 Jupyter 笔记本进行数据分析。
全局安装 Python,OpenCV 和其他包
在本节中,您将看到如何全局安装 Python,OpenCV 和任何其他包。 给出了针对 Linux 和 Windows 操作系统的特定说明。
安装 Python
我们将看到如何在 Linux 和 Windows 操作系统上全局安装 Python。
在 Linux 上安装 Python
在 Debian 衍生产品(例如 Ubuntu)上,使用 APT 安装 Python。 之后,建议升级 pip 版本。 PIP 是 PyPA 推荐的安装 Python 包的工具:
$ sudo apt-get install python3.7 $ sudo pip install --upgrade pip
要验证 Python 是否已正确安装,请打开命令提示符或 shell 并运行以下命令:
$ python3 --version
Python 3.7.0
在 Windows 上安装 Python
转到这里。 Python Windows 的默认安装程序是 32 位。 启动安装程序。 选择自定义安装:
在下一个屏幕上,应检查所有可选功能:
最后,在下一个屏幕上,确保选中将 Python 添加到环境变量和预编译标准库。 (可选)您可以自定义安装位置,例如C:\Python37
:
按下“安装”按钮,几分钟后,安装就准备就绪。 在安装程序的最后一页,您还应该按禁用路径长度限制:
要检查 Python 是否已正确安装,请按住Shift
键,然后在桌面上的鼠标右键单击。 在此处选择“打开命令窗口”。 或者,在 Windows 10 上,使用左下方的搜索框搜索cmd
。 现在,在命令窗口中写入python
,然后按Enter
键。 您应该会看到以下内容:
您还应该升级点子:
$ python -m pip install --upgrade pip
安装 OpenCV
现在,我们将在 Linux 和 Windows 操作系统上安装 OpenCV。 首先,我们将了解如何在 Linux 上安装 OpenCV,然后如何在 Windows 上安装 OpenCV。
在 Linux 上安装 OpenCV
确保已安装 NumPy。 要安装 NumPy,请输入以下内容:
$ pip3 install numpy
然后安装 OpenCV:
$ pip3 install opencv-contrib-python
此外,我们可以安装 Matplotlib,这是一个生成高质量图形的 Python 图形库:
$ pip3 install matplotlib
在 Windows 上安装 OpenCV
确保已安装 NumPy。 要安装 NumPy,请输入以下内容:
$ pip install numpy
然后安装 OpenCV:
$ pip install opencv-contrib-python
此外,我们可以安装 Matplotlib:
$ pip install matplotlib
测试安装
一种测试安装的方法是执行 OpenCV Python 脚本。 为此,在特定的文件夹中应该有两个文件logo.png
和test_opencv_installation.py
:
打开一个 cmd 并转到这两个文件所在的路径。 接下来,我们可以通过键入以下内容来检查安装:
python test_opencv_installation.py
您应该同时看到 OpenCV RGB 徽标和 OpenCV 灰度徽标:
在这种情况下,安装成功。
使用 Virtualenv 安装 Python,OpenCV 和其他包
virtualenv
是一种非常流行的工具,可为 Python 库创建隔离的 Python 环境。 virtualenv
允许多个具有不同(有时是相互冲突)要求的 Python 项目。 从技术上讲,virtualenv
通过在目录下安装一些文件来工作(例如env/
)。
另外,virtualenv
修改PATH
环境变量以在其前面添加自定义二进制目录(例如env/bin/
)。 此外,Python 或 Python3 二进制文件的精确副本位于此目录中。 激活此虚拟环境后,您可以使用 PIP 在虚拟环境中安装包。 PyPA 也推荐virtualenv
。 因此,我们将看到如何使用虚拟环境安装 OpenCV 或任何其他包。
通常,pip
和virtualenv
是仅需要全局安装的两个包。 这是因为,一旦安装了两个包,就可以在虚拟环境中完成所有工作。 实际上,virtualenv
实际上就是您所需要的,因为此包提供了pip
的副本,该副本被复制到您创建的每个新环境中。
现在,我们将看到如何安装,激活,使用和停用虚拟环境。 现在为 Linux 和 Windows 操作系统提供了特定的命令。 我们不会为每个操作系统添加一个特定的部分,因为每个过程都非常相似。 让我们开始安装virtualenv
:
$ pip install virtualenv
在此目录(env
)中,创建了一些文件和文件夹,其中包含运行 python 应用所需的全部内容。 例如,新的 python 可执行文件将位于/env/scripts/python.exe
。 下一步是创建一个新的虚拟环境。 首先,将目录更改为项目目录的根目录。 第二步是使用virtualenv
命令行工具创建环境:
$ virtualenv env
在这里,env
是您要在其中创建虚拟环境的目录的名称。 通常的惯例是在env
中调用要创建虚拟环境的目录,并将其放入项目目录中。 这样,如果将代码保留在~/code/myproject/
,则环境将在~/code/myproject/env/
。
下一步是使用命令行工具激活刚刚创建的env
环境,以执行activate
脚本,该脚本位于以下位置:
~/code/myprojectname/env/bin/activate
(Linux)~/code/myprojectname/env/Scripts/activate
(Windows)
例如,在 Windows 下,您应该键入以下内容:
$ ~/code/myprojectname/env/Scripts/activate
(env) $
现在,您只能为此激活的环境安装所需的包。 例如,如果要安装使用 Python 编写的 Django(这是一个免费的开放源 Web 框架),则应输入以下内容:
(env)$ pip install Django
请记住,此包仅会为myprojectname
项目安装。
您还可以通过执行以下操作来停用环境:
$ deactivate $
您应该看到已经返回到正常提示,表明您不再处于任何virtualenv
中。 最后,如果要删除环境,只需键入以下内容:
$ rmvirtualenv test
使用 Python IDE 和 Virtualenv 创建虚拟环境
在下一节中,我们将使用 PyCharm(一个 Python IDE)创建虚拟环境。 但是在此之前,我们将讨论 IDE。 IDE 是一种软件应用,可帮助计算机程序员进行软件开发。 IDE 提供了一个程序,可以完成所有开发。 与 Python IDE 结合,可以找到两种方法:
- 具有 Python 支持的常规编辑器和 IDE
- 特定于 Python 的编辑器和 IDE
在第一类(通用 IDE)中,应突出一些示例:
- Eclipse + PyDev
- Visual Studio + 适用于 Visual Studio 的 Python 工具
- Atom + Python 扩展
在第二类中,这是一些特定于 Python 的 IDE:
- PyCharm:Python 最好的全功能,专用 IDE 之一。 PyCharm 可在 Windows,MacOS 和 Linux 平台上快速轻松地安装。 它实际上是 Python IDE 环境。
- Spyder:Anaconda 包管理器发行版附带的 Spyder 是一种开源 Python IDE,非常适合数据科学工作流程。
- Thonny:Thonny 旨在成为初学者的 IDE。 它适用于所有主要平台(Windows,macOS,Linux),并在网站上提供了安装说明。
在这种情况下,我们将安装 PyCharm(实际上是 Python IDE 环境)社区版。 之后,我们将看到如何使用此 IDE 创建虚拟环境。 可以从这里下载 PyCharm。 PyCharm 可以安装在 Windows,MacOS 和 Linux 上:
安装 PyCharm 之后,我们就可以使用它了。 使用 PyCharm,我们可以以非常简单直观的方式创建虚拟环境。
通过 PyCharm,可以使用virtualenv
工具创建特定于项目的隔离虚拟环境。 此外,virtualenv
工具与 PyCharm 捆绑在一起,因此用户不需要安装它。
打开 Pycharm 后,您可以单击“创建新项目”。 如果要创建新环境,则应单击Project Interpreter: New Virtualenv
环境。 然后单击使用 Virtualenv 的新环境。 在下一个屏幕截图中可以看到:
您应注意,虚拟环境的名称(默认为 PyCharm)为venv
,位于项目文件夹下。 在这种情况下,项目名为test-env-pycharm
,虚拟环境venv
位于test-env-pycharm/venv
。 此外,您可以看到venv
名称可以根据您的喜好进行更改。
当您单击创建按钮时,PyCharm 会加载项目并创建虚拟环境。 您应该会看到以下内容:
创建项目后,只需单击几下就可以安装包。 单击文件,然后单击设置...(Ctrl + Alt + S
)。 将出现一个新窗口,显示如下内容:
现在,单击Project:
,然后选择Project Interpreter
。 在此屏幕的右侧,显示已安装的包以及所选的项目解释器。 您可以在此屏幕顶部进行更改。 选择适当的解释器(以及项目的环境)后,您可以安装新的包。 为此,您可以在左上角的输入框中搜索。 在下一个屏幕截图中,您可以看到一个搜索numpy
包的示例:
您可以通过单击“安装包”来安装包(默认为最新版本)。 您还可以指定一个具体版本,如上一个屏幕截图所示:
安装该包之后,我们可以看到我们现在在虚拟环境中已经安装了三个包。 此外,在环境之间进行更改非常容易。 您应该转到运行/调试配置,然后单击 Python 解释器以在环境之间进行切换。 下一个屏幕截图中可以看到此功能:
最后,您可能已经注意到,在第一步中,使用 PyCharm 创建虚拟环境时,可以使用virtualenv
以外的其他选项。 PyCharm 使您能够使用 Virtualenv,Pipenv 和 Conda 创建虚拟环境:
先前我们介绍了 Virtualenv,以及如何使用此工具为 Python 库创建隔离的 Python 环境。
Pyenv 用于隔离 Python 版本。 例如,您可能想针对 Python 2.6、2.7、3.3、3.4 和 3.5 测试代码,因此您将需要一种在它们之间切换的方法。
Conda 是在 Windows,MacOS 和 Linux 上运行的开源包管理和环境管理系统(提供虚拟环境功能)。 Conda 包含在 Anaconda 和 Miniconda 的所有版本中。
由于读者可能会对与 Anaconda/Miniconda 和 Conda 的合作感兴趣,因此在下一节中将进行快速介绍,但是不必运行本书中包含的代码示例。
Anaconda/Miniconda 发行版和 Conda 包以及环境管理系统
Conda 是一个开源的包管理和环境管理系统(提供虚拟环境功能),可在许多操作系统(例如 Windows,macOS 和 Linux)上运行。 Conda 安装,运行和更新包及其依赖项。 Conda 可以创建,保存,加载和在环境之间切换。
由于 Conda 包含在 Anaconda 和 Miniconda 的所有版本中,因此您应该已经安装了 Anaconda 或 Miniconda。
Anaconda 是可下载,免费,开源的高性能 Python 和 R 发行版。 Anaconda 随附 Conda,Conda 构建,Python 和 100 多个开源科学包及其依赖项。 使用conda install
命令,您可以轻松地从 Anaconda 存储库安装用于数据科学的流行开源包。 Miniconda 是 Anaconda 的小型版本,仅包含 Conda,Python,它们依赖的包以及少量其他有用的包。
安装 Anaconda 或 Miniconda 很容易。 为了简单起见,我们将重点放在 Anaconda 上。 要安装 Anaconda,请检查操作系统的 Acadonda 安装程序。 Anaconda 5.2 可以在 Windows,MacOS 和 Linux 上的 Python 3.6 和 Python 2.7 版本中安装:
完成安装后,为了测试安装,请在终端或 Anaconda 提示符中运行以下命令:
$ conda list
为了成功安装,将显示已安装包的列表。 如前所述,Anaconda(和 Miniconda)附带了 Conda,它是一个简单的包管理器,类似于 Linux 上的apt-get
。 这样,我们可以使用以下命令在终端中安装新包:
$ conda install packagename
在这里,packagename
是我们要安装的包的实际名称。 可以使用以下命令更新现有包:
$ conda update packagename
我们还可以使用以下命令搜索包:
$ anaconda search –t conda packagename
这将显示单个用户可以使用的包的完整列表。
然后可以如下安装来自名为username
的用户的名为packagename
的包:
$ conda install -c username packagename
此外,Conda 可用于创建和管理虚拟环境。 例如,创建test
环境并安装 NumPy 1.7 版就像输入下一个命令一样简单:
$ conda create --name test numpy=1.7
与使用virtualenv
的方式类似,可以激活和停用环境。 要在 MacOS 和 Linux 上执行此操作,只需运行以下命令:
$ source activate test
$ python
...
$ source deactivate
在 Windows 上,运行以下命令:
$ activate test
$ python
...
$ deactivate
有关使用 Conda 的最重要信息的单页摘要,请参见 Conda 备忘单 PDF(1 MB)。
最后,应该指出的是,我们可以在 PyCharm IDE 下使用 Conda,就像virtualenv
一样创建和管理虚拟环境,因为 PyCharm 可以同时使用这两种工具。
科学计算,数据科学,机器学习,深度学习和计算机视觉的包
到目前为止,我们已经了解了如何从头开始安装 Python,OpenCV 和其他一些包(numpy
和matplotlib
),或使用 Anaconda 发行版,其中包括许多流行的数据科学包。 这样,有关科学计算,数据科学,机器学习和计算机视觉的主要包的一些知识是关键点,因为它们提供了强大的计算工具。 在本书中,将使用许多 Python 包。 并非本节中所有引用的包都将提供,但是为了完整起见,提供了一个完整的列表,以显示 Python 在与本书内容相关的主题中的潜力:
- NumPy 支持大型多维数组。 NumPy 是计算机视觉中的关键库,因为图像可以表示为多维数组。 将图像表示为 NumPy 数组具有许多优点。
- OpenCV 是一个开源计算机视觉库。
- Scikit-Imnage 是图像处理算法的集合。 scikit-image 操纵的图像只是 NumPy 数组。
- Python 图像库(PIL)是一种图像处理库,它提供了强大的图像处理和图形功能。
- Pillow 是 Alex Clark 及其贡献者友好的 PIL 叉子。 PIL 为您的 Python 解释器添加了图像处理功能。
- SimpleCV 是计算机视觉的框架,提供了用于处理图像处理的关键功能。
- Mahotas 是 Python 中用于图像处理和计算机视觉的一组功能。 它最初是为生物图像信息学设计的。 但是,它在其他领域也很有用。 它完全基于 numpy 数组作为其数据类型。
- Ilastik 是一种用于交互式图像分割,分类和分析的用户友好型简单工具。
- Scikit-learn)是一种机器学习库,具有各种分类,回归和聚类算法。
- SciPy 是一个用于科学和技术计算的库。
- NLTK 是一组用于处理人类语言数据的库和程序。
- spaCy 是一个用于在 Python 中进行高级自然语言处理的开源软件库。
- LibROSA 是一个用于音乐和音频处理的库。
- Pandas 是一个库(基于 NumPy 构建),提供了高级数据计算工具和易于使用的数据结构。
- Matplotlib 是一个绘图库,可产生多种格式的具有出版物质量的图形。
- Seaborn 是基于 Matplotlib 构建的图形库。
- Orange 是面向新手和专家的开源机器学习和数据可视化工具包。
- PyBrain是一种机器学习库,提供了易于使用的最新机器学习算法。
- Milk 是一种机器学习工具包,专注于带有多个分类器的监督分类。
- TensorFlow 是一个开源机器学习和深度学习库。
- PyTorch 是一个开放源代码的机器学习和深度学习库。
- Theano 是一个用于快速数学表达式,求值和计算的库,已编译为可在 CPU 和 CPU 上运行。 GPU 架构(深度学习的关键点)。
- Keras 是一个高级深度学习库,可以在 TensorFlow,CNTK,Theano 或 Microsoft 认知工具包之上运行。
- Django 是基于 Python 的免费和开源 Web 框架,鼓励快速开发和简洁实用的设计。
- Flask 是一个基于 Werkzeug 和 Jinja 2 用 Python 编写的微型 Web 框架。
所有这些包都可以根据其主要目的进行组织:
- 处理图像:NumPy,OpenCV,Scikit-image,PIL 枕头,SimpleCV,Mahotas,ilastik
- 处理文本:NLTK,spaCy,NumPy,scikit-learn,PyTorch
- 处理音频:LibROSA
- 解决机器学习问题:Pandas,Scikit-learn,Orange,PyBrain,牛奶
- 清楚地查看数据:Matplotlib,Seaborn,scikit-learn,Orange
- 深度学习:TensorFlow,Pytorch,Theano,Keras
- 科学计算:SciPy
- 集成 Web 应用:Django,Flask
可以在这个页面上找到用于 AI 和机器学习的其他 Python 库和包。
Jupyter 笔记本
Jupyter 笔记本是一个开源 Web 应用,允许您通过 Web 浏览器编辑和运行文档。 这些文档称为笔记本文档(或笔记本),包含代码(支持 40 多种编程语言,包括 Python)和富文本元素(段落,方程式,图形)。 Jupyter 笔记本可以在本地计算机上执行,也可以安装在远程服务器上。 您可以从笔记本开始,在线尝试它们,也可以安装 Jupyter 笔记本。
在线尝试 Jupiter 笔记本
首先,转到这里。 您将看到如下内容:
要在线尝试使用 Python 的 Jupyter,请单击 Python 选项,或将此 URL 粘贴到 Web 浏览器中:https://mybinder.org/v2/gh/ipython/ipython-in-depth/master?filepath=binder/Index.ipynb
。 页面加载后,即可开始编码/加载笔记本。
安装 Jupyter 笔记本
要安装 Jupyter,您可以按照这个页面上的主要步骤进行操作。 Jupyter 笔记本的安装也可以使用 Anaconda 或使用 Python 的包管理器 PIP 完成。
使用 Anaconda 安装 Jupyter
强烈建议您使用 Anaconda 发行版安装 Python 和 Jupyter,该发行版包括 Python,Jupyter 笔记本和其他用于科学计算和数据科学的常用包。 要使用 Anaconda 安装 Jupyter,请下载 Anaconda 并进行安装。 这样,您已经安装了 Jupyter 笔记本。 要运行笔记本,请在命令提示符(Windows)或终端(macOS/Linux)中运行以下命令:
$ jupyter notebook
使用 PIP 安装 Jupyter
您还可以通过运行以下命令,使用 Python 的包管理器 PIP 安装 Jupyter:
$ python -m pip install --upgrade pip
$ python -m pip install jupyter
此时,您可以通过运行以下命令来启动笔记本服务器:
$ jupyter notebook
上一个命令将向您显示与笔记本服务器有关的一些关键信息,包括 Web 应用的 URL(默认为http://localhost:8888
)。 然后它将打开您的默认 Web 浏览器到该 URL。 要启动特定的笔记本,应使用以下命令:
$ jupyter notebook notebook.ipynb
这是笔记本的快速介绍。 在下一章中,我们将创建一些笔记本,因此您将有机会使用它们,并充分了解此有用的工具。
OpenCV 和 Python 项目结构
项目结构是组织文件夹中所有文件的方式,以使项目最好地实现目标。 我们将从一个.py
脚本(sampleproject.py
)开始,该脚本应与其他文件一起使用,以完成有关此脚本的信息-依赖关系,许可证,如何安装或如何对其进行测试。 构建此基本项目的常用方法如下:
sampleproject/
│
├── .gitignore
├── sampleproject.py
├── LICENSE
├── README.rst
├── requirements.txt
├── setup.py
└── tests.py
sampleproject.py
-如果您的项目只是一个 Python 源文件,则将其放入目录中并为其命名与您的项目相关的名称。
README
(.rst
或.md
扩展名)用于注册项目的主要属性,至少应包括以下内容:
- 您的项目做什么
- 如何安装
- 用法示例
- 如何建立开发环境
- 如何发布 ISSUE
- 变更记录
- 许可证和作者信息
可以从以下 GitHub 存储库下载可以使用的模板。 有关更多信息,请参见这里。
LICENSE.md
文档包含适用的许可证。 除了源代码本身之外,这可以说是存储库中最重要的部分。 完整的许可证文本和版权声明应存在于此文件中。 如果您要分发代码,最好有一个。 通常, GNU 通用公共许可证(GPL)或 MIT 许可证在开源项目中使用。 如果您不确定应将哪个许可证应用于您的项目,可以访问这里。
应将requirements.txt
PIP 要求文件放在存储库的根目录中,用于指定项目所需的依赖关系。 可以使用以下方法生成requirements.txt
文件:
$ pip freeze > requirements.txt
要安装这些要求,可以使用以下命令:
$ pip install -r requirements.txt
setup.py
文件使您可以创建可以重新分发的包。 该脚本旨在将包安装在最终用户的系统上,而不是像pip install -r < requirements.txt
那样准备开发环境。 这是一个关键文件,因为它定义了包的信息(例如版本,包要求和项目描述)。
tests.py
脚本包含测试。
.gitignore
文件告诉 Git 忽略什么类型的文件,例如 IDE 混乱或本地配置文件。 您可以在这个页面上找到 Python 项目的示例.gitignore
文件。
我们的第一个 Python 和 OpenCV 项目
基于上一节中显示的最小项目结构,我们将创建我们的第一个 Python 和 OpenCV 项目。 该项目具有以下结构:
helloopencv/
│
├── images/
│
├── .gitignore
├── helloopencv.py
├── LICENSE
├── README.rst
├── requirements.txt
├── setup.py
└── helloopencvtests.py
README.rst
(.rst
扩展名)遵循基本结构,如上一节所示。 Python 和 ReStructuredText(RST)紧密相连-RST 是 docutils 和狮身人面像的格式(实际上是用于记录 python 代码的标准)。 RST 既用于通过文档字符串来记录对象,又用于编写其他文档。 如果您访问 Python 的官方文档,则可以查看每个页面的 RST 源。 对README.rst
使用 RST 使其与整个文档设置直接兼容。 实际上,README.rst
通常是项目文档的封面。
有一些 RST 编辑器可用来帮助您编写README.rst
。 您也可以使用一些在线编辑器。 例如,在线 Sphinx 编辑器是一个不错的选择。
.gitignore
文件指定 Git 应该忽略的故意未跟踪的文件。 .gitignore
告诉git
Git 应该忽略哪些文件(或模式)。 通常用于避免从您的工作目录中提交对其他协作者无用的临时文件,例如 IDE 创建的编译产品和临时文件。 打开这个页面以查看可以包含在 Python 项目中的.gitignore
文件。
setup.py
(有关详细说明,请参见上一节),它是 Python 文件,通常随库或程序一起提供,也使用 Python 编写。 其目的是正确安装软件。 可以在这个页面上看到此文件的非常完整的示例,其中包含许多注释,可帮助您了解如何适配它来满足您的需求。 此文件由 Python 包装规范(PyPa)提出。 一个关键点是与包的选项相关,正如我们可以在上述setup.py
文件中看到的那样。
如果您的项目很简单,则可以在此处手动指定包目录。 或者,您可以使用find_packages()
。 另外,如果您只想分发一个 Python 文件,请改为使用py_modules
参数,如下所示,这将期望存在一个名为my_module.py
的文件
py_modules=["my_module"]
。
因此,在我们的情况下,使用py_modules =["helloopencv"]
。
此外,setup.py
允许您轻松安装 Python 包。 通常,编写以下内容就足够了:
$ python setup.py install
因此,如果要安装此简单包,可以在helloopencv
文件夹中编写上一个命令python setup.py install
。 例如,在 Windows 中,运行以下命令:
C:\...\helloopencv>python setup.py install
您应该会看到以下内容:
running install
...
...
Installed c:\python37\lib\site-packages\helloopencv-0.1-py3.7.egg
Processing dependencies for helloopencv==0.1
...
...
Finished processing dependencies for helloopencv==0.1
完成后,helloopencv
已安装在我们的系统中(与其他任何 Python 包一样)。 您还可以在helloopencv
文件夹内使用pip install
安装helloopencv
。 例如,在 Windows 中,运行以下命令:
C:\...\helloopencv>pip install .
您应该会看到以下内容:
Processing c:\...\helloopencv
...
...
Successfully installed helloopencv-0.1
这表示helloopencv
已成功安装。 要使用此包,我们可以编写一个 Python 文件并导入helloopencv
包。 另外,我们可以通过直接从 Python 解释器导入来快速使用此包。 按照第二种方法,您可以打开命令提示符,导入包并使用它。 首先,打开命令提示符,然后键入python
以运行解释器:
C:\...\helloopencv>python
Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 27 2018, 04:06:47) [MSC v.1914 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>>
加载解释器后,我们可以导入包:
>>> import helloopencv
helloopencv.py is being imported into another module
>>>
helloopencv.py is being imported into another module
输出是来自helloopencv
包(特别是来自helloopencv.py
文件)的消息,指示已导入此文件。 因此,此消息表明模块已成功导入。 导入后,我们就可以使用它。 例如,我们可以调用show_message
方法:
>>> helloopencv.show_message()
'this function returns a message'
>>>
我们可以看到,调用此方法的结果是一条消息显示在屏幕上。 此方法是一种简单的方法,它知道所有内容均已正确安装,因为它涉及到安装,导入和使用包中的函数。 此外,我们可以调用helloopencv
包中包含的更有用的方法。 例如,您可以调用load_image
方法从磁盘加载图像,然后,可以使用show_image
方法显示它:
>>> image = helloopencv.load_image("C:/.img/logo.png")
>>> helloopencv.show_image(image)
此处,load_image
函数的参数是计算机图像的路径。 在这种情况下,将加载logo.png
图像。 调用show_image
方法后,应显示图像。 要关闭窗口,必须按下一个键。 然后,您应该能够再次在解释器中编写。 要查看helloopencv
包中可用的所有方法,可以使用喜欢的编辑器或 IDE 打开helloopencv.py
文件并进行查看。 在此 Python 文件中,您可以看到一些符合我们第一个 Python 项目的方法:
show_message()
:此函数打印this function returns a message
消息。load_image()
:此函数从其路径加载图像。show_image()
:加载图像后,此函数会显示图像。convert_to_grayscale()
:此函数在加载图像后将其转换为灰度。write_image_to_disk()
:此函数将图像保存在磁盘上。
所有这些方法都执行简单且基本的操作。 它们中的大多数都使用 OpenCV 库,该库在此文件(import cv2
)的开头导入。 不必担心此文件中包含的 Python 代码,因为仅执行基本操作和对 OpenCV 库的调用。
您无需安装包即可执行helloopencv.py
脚本。 要执行此文件,应在打开命令提示符后运行python helloopencv.py
命令:
C:\...\helloopencv>python helloopencv.py
helloopencv.py is being run directly
执行完该文件后,将显示helloopencv.py is being run directly
消息,这意味着该文件将直接执行,而不是从其他模块或包(或 Python 解释器)导入。 您还可以看到已加载并显示图像。 您可以按任意键继续执行。 再次显示徽标的灰度版本,应再次按下任何键以结束执行。 将灰度图像保存到磁盘后,执行结束。
最后,helloopencvtests.py
文件可用于单元测试。 测试应用已成为任何合格开发人员的标准技能。 Python 社区支持测试,Python 标准库具有良好的内置工具来支持测试。
在 Python 生态系统中,有很多测试工具。 [用于测试的两个最常见的包是`nose](https://pypi.org/project/nose/)和[`pytest`](https://pypi.org/project/pytest/)。 在第一个 Python 项目中,我们将使用`pytest`进行单元测试。
要执行测试,请在打开命令提示符后运行py.test -s -v helloopencvtests.py
命令:
C:\...\helloopencv>py.test -s -v helloopencvtests.py
============================= test session starts =============================
platform win32 -- Python 3.7.0, pytest-3.8.0, py-1.6.0, pluggy-0.7.1 -- c:\python37\python.exe
cachedir: .pytest_cache
collected 4 items
helloopencvtests.py::test_show_message testing show_message
PASSED
helloopencvtests.py::test_load_image testing load_image
PASSED
helloopencvtests.py::test_write_image_to_disk testing
write_image_to_disk
PASSED
helloopencvtests.py::test_convert_to_grayscale testing test_convert_to_grayscale
PASSED
========================== 4 passed in 0.57 seconds ===========================
执行测试后,您可以看到执行了四个测试。 PASSED
消息表示测试已成功执行。 这是 Python 单元测试的快速介绍。 不过,完整的pytest
文档可在这个页面中找到。
总结
在第一章中,我们介绍了设置 OpenCV 和 Python 以构建您的计算机视觉项目的主要步骤。 在本章开始时,我们快速浏览了本书的主要概念-人工智能,机器学习,神经网络和深度学习。 然后,我们探索了 OpenCV 库,包括该库的历史及其主要模块。 由于 OpenCV 和其他包可以在许多操作系统中以不同的方式安装,因此我们介绍了主要方法。
具体来说,我们看到了如何在全局或虚拟环境中安装 Python,OpenCV 和其他包。 在安装包时,我们介绍了 Anaconda/Miniconda 和 Conda,因为我们还可以创建和管理虚拟环境。 此外,Anaconda/Miniconda 附带了许多开源科学包,包括 SciPy 和 NumPy。
我们探索了用于科学计算,数据科学,机器学习和计算机视觉的主要包,因为它们提供了强大的计算工具。 然后,我们讨论了 Python 特定的 IDE,包括 PyCharm(实际上是 Python IDE 环境)。 PyCharm(和其他 IDE)可以帮助我们以非常直观的方式创建虚拟环境。 我们还研究了 Jupyter 笔记本,因为它可能是本书读者的一个很好的工具。 在下一章中,将创建更多的 Jupyter 笔记本,以使您更好地了解此有用的工具。 最后,我们探索了 OpenCV 和 Python 项目结构,涵盖了应包含的主要文件。 然后,我们构建了第一个 Python 和 OpenCV 示例项目,在其中我们看到了构建,运行和测试该项目的命令。
在下一章中,您将开始熟悉 OpenCV 库,从而开始编写第一个脚本。 您将看到开始对计算机视觉项目进行编码的一些基本概念(例如,了解主要图像概念,OpenCV 中的坐标系以及 OpenCV 中的访问和操纵像素)。
问题
- 什么是虚拟环境?
- PIP,Virtualenv,Pipenv,Anaconda 和 Conda 之间有什么联系?
- 什么是 Jupyter 笔记本?
- 在 Python 中使用计算机视觉的主要包是什么?
pip install -r requirements.txt
有什么作用?- 什么是 IDE?为什么在开发项目时使用 IDE?
- OpenCV 以什么协议发布?
进一步阅读
以下参考资料将帮助您更深入地了解本章中介绍的概念:
-
Python 机器学习:
-
Python 深度学习:
查看这些参考资料(主要是书籍),以获取有关概念的更多信息,这些概念将在本书的后续章节中介绍。 保持此清单方便; 这将非常有帮助:
- 《使用 Python 和 OpenCV 的计算机视觉》
- 《OpenCV:使用 Python 的计算机视觉项目》
- 《面向开发人员的增强现实》
- 《使用 Python 和 OpenCV 的深度学习》
- 《使用 Keras 的深度学习》
- 《TensorFlow 入门》
- 《精通 Flask Web 开发:第二版》
二、OpenCV 中的图像基础
图像是计算机视觉项目中的关键组成部分,因为在许多情况下,它们提供了要使用的输入。 因此,了解主要的图像概念是开始编写计算机视觉项目所需的基本知识。 此外,还将介绍一些 OpenCV 库的特性,例如坐标系或 BGR 顺序(而不是 RGB)。
在本章中,您将学习如何开始编写第一个脚本,这将向您介绍 OpenCV 库。 在本章的最后,您将有足够的知识来开始使用 OpenCV 和 Python 编写您的第一个计算机视觉项目。
在本章中,我们将介绍以下主题:
- 图像基础的理论介绍
- 像素,颜色,通道,图像和色彩空间的概念
- OpenCV 中的坐标系
- 在 OpenCV 中访问和操作不同颜色空间中的像素(获取和设置)
- OpenCV 中的 BGR 顺序(而不是 RGB)
技术要求
本章的技术要求如下:
- Python 和 OpenCV
- 特定于 Python 的 IDE
- NumPy 和 Matplotlib 包
- Jupyter 笔记本
- Git 客户端
有关如何安装这些要求的更多详细信息,请参见第 1 章,“设置 OpenCV”。 可以在这个页面上访问用于通过 Python 精通 OpenCV 的 GitHub 存储库,其中包含从第一章到最后一章都需要完成本书的所有支持项目文件。
图像基础的理论介绍
本部分的主要目的是为图像基础知识提供理论上的介绍-这些将在下一部分中详细说明。 首先,将快速介绍一下在计算机视觉项目中开发图像处理集时遇到的一些困难的重要性,然后再介绍一些与图像有关的简单公式。
图像处理中的主要问题
引入的第一个概念与图像有关,可以将其视为 3D 世界的二维(2D)视图。 数字图像是 2D 图像的数字表示形式,通常是二进制的有限数字值集,称为像素(像素的概念将在“像素,颜色,通道,图像和色彩空间概念”部分中详细说明)。 因此,计算机视觉的目标是将 2D 数据转换为以下内容:
- 新的表示形式(例如,新的图像)
- 决策(例如,执行具体任务)
- 新结果(例如,图像的正确分类)
- 一些有用的信息提取(例如,对象检测)
在处理图像处理技术时,计算机视觉可能会解决常见的问题(或难题):
- 含糊不清的图像,因为它们会受到透视的影响,这可能会导致图像的视觉外观发生变化。 例如,从不同角度观看的同一对象可能会产生不同的图像。
- 通常受许多因素影响的图像,例如照明,天气,反射和运动。
- 图像中的物体也可能被其他物体遮挡,从而难以检测或分类被遮挡的物体。 根据遮挡的级别,所需的任务(例如,将图像分类为一些预定义的类别)可能确实具有挑战性。
为了将所有这些困难放在一起,假设您想开发一个面部检测系统。 该系统应足够坚固以应对照明或天气条件的变化。 另外,该系统应该处理头部的运动,甚至可以处理用户可以离相机更远或更近的事实。 它应该能够在每个轴(偏航,横摇和俯仰)上旋转一定程度来检测用户的头部。 例如,当头部靠近额头时,许多面部检测算法都具有良好的表现。 但是,如果面部不是正面的话,他们将无法检测到(例如,个人资料中的面部)。 此外,即使用户戴着眼镜或太阳镜,也可能希望检测到脸部,这会在眼睛区域产生遮挡。 在开发计算机视觉项目时,必须考虑所有这些因素。 一个很好的近似值是通过合并一些困难来使用许多测试图像来验证您的算法。 您还可以根据要轻松检测算法弱点的主要困难对测试图像进行分类。
图像处理步骤
图像处理包括以下三个步骤:
- 获取要使用的图像。 此过程通常涉及一些函数,以便您可以从不同的来源(摄像机,视频流,磁盘,在线资源)读取图像。
- 通过应用图像处理技术来处理图像以实现所需的功能(例如,检测图像中的猫)。
- 显示处理步骤的结果(例如,在图像中绘制边框,然后将其保存到磁盘)。
此外,第二步可以分为三个处理级别:
- 低级流程
- 中级流程
- 高级流程
低级过程通常将图像作为输入,然后输出另一个图像。 可以在此步骤中应用的示例过程包括:
- 噪音消除
- 图像锐化
- 光照归一化
- 透视校正
结合面部检测示例,输出图像可以是光照归一化图像以处理由太阳反射引起的变化。
中级过程提取预处理后的图像,以输出该图像的某种表示形式。 将其视为数字的集合(例如,包含 100 个数字的向量),该集合汇总了要用于进一步处理的图像的主要信息。 关于面部检测示例,输出可以是由点(x, y)
,包含检测到的面部的宽度和高度定义的矩形。
高级过程提取此数字向量(通常称为属性)并输出最终结果。 例如,输入可以是检测到的面部,输出可以是以下内容:
- 人脸识别
- 情感识别
- 睡意和注意力分散
- 面部远程心率测量
图像创建
图像可以描述为 2D 函数f(x, y)
,其中(x, y)
是空间坐标和f
的值。 在任何时候,(x, y)
与图像的亮度或灰度级成正比。 另外,当(x, y)
和f
的亮度值都是有限离散量时,该图像被称为数字图像。 因此,f(x, y)
采用以下值:
x ∈ [0, h-1]
,其中h
是图像的高度y ∈ [0, w-1]
,其中w
是图像的宽度f(x, y) ∈ [0, L-1]
,其中L = 256
(对于 8 位图像)
彩色图像可以用相同的方式表示,但是我们需要定义三个函数分别表示红色,绿色和蓝色值。 这三个函数的每一个都遵循与为灰度图像定义的f(x, y)
函数相同的公式。 我们将针对三种秘籍(对于彩色图像)将这三个函数的子索引 R,G 和 B 表示为fR(x, y)
,fG(x, y)
和fB(x, y)
。
黑白图像遵循相同的近似方式,只需要一个函数即可表示图像。 但是,一个关键点是f(x, y)
只能取两个值。 通常,这些值为0
(黑色)和255
(白色)。
这三种类型的图像通常用于计算机视觉,因此请记住它们的格式。
以下屏幕截图显示了三种类型的图像(彩色图像,灰度图像和黑白图像):
请记住,由于f(x, y)
值是有限的离散量,因此可以将数字图像视为真实场景的近似值。 此外,灰度图像和黑白图像每点只有一个样本(仅需要一个函数),而彩色图像每点只有三个样本(需要三项函数,对应于图像的红色,绿色和蓝色分量) 。
像素,颜色,通道,图像和色彩空间的概念
有几种不同的颜色模型,但最常见的一种是红色,绿色,蓝色(RGB)模型,这些模型将用于解释有关数字图像的一些关键概念。
在第 5 章,“图像处理技术”中,将详细说明主要颜色模型(也称为颜色空间)。
RGB 模型是一种加色模型,其中将原色(R, G, B)
*混合在一起以再现各种颜色。 如前所述,在 RGB 模型中,原色是红色,绿色和蓝色。
每个原色(R, G, B)
,通常称为通道,通常表示为[0, 255]
范围内的整数。 因此,每个通道总共产生 256 个离散值,这些离散值对应于用于表示颜色通道值的总位数(2^8 = 256
)。 此外,由于存在三个不同的通道,因此称为 24 位色深:
在上图中,您可以看到 RGB 颜色空间的加色属性:
- 将红色添加到绿色将获得黄色
- 将红色添加到蓝色会产生洋红色
- 将绿色添加到蓝色会生成青色
- 将所有三种原色相加会产生白色
如前所述,结合 RGB 颜色模型,特定颜色由红色,绿色和蓝色值表示,将像素值表示为 RGB 三元组(R, G, B)
。 如下图所示,是一个图形软件中典型的 RGB 颜色选择器。 可以想象,每个滑块的范围从0
到255
:
您还可以看到,将纯红色添加到纯蓝色会产生完美的洋红色。 您可以在这个页面上使用 RGB 颜色图表。
分辨率为800×1200
的图像是具有 800 列和 1200 行的网格,包含800×1200 = 960,000
像素。 应该注意的是,知道图像中有多少像素并不表示其物理尺寸(一个像素不等于一毫米)。 取而代之的是,一个像素的大小(因此图像的大小)将取决于已设置的每英寸像素(PPI) 该图像。 一般的经验法则是 PPI 在[200 - 400]
范围内。
计算 PPI 的基本公式如下:
PPI = 图像的宽度(像素)/ 宽度(英寸)
PPI = 图像的高度(像素)/ 高度(英寸)
因此,例如,如果要打印4×6
英寸图像,并且图像为800×1200
,则 PPI 为 200。
现在,我们将研究文件扩展名。
文件扩展名
尽管我们将在 OpenCV 中处理的图像可以看作是 RGB 三元组的矩形数组(在 RGB 图像的情况下),但不一定必须以该格式创建,存储或传输它们。 从这个意义上讲,某些文件格式(例如 GIF,PNG,位图或 JPEG)使用不同形式的压缩(无损或有损)来更有效地表示图像。
这样,出于完整性考虑,此处简要介绍了这些图像文件,特别着重于 OpenCV 支持的文件格式。 OpenCV 支持以下文件格式(带有关联的文件扩展名):
- Windows 位图:
*.bmp
和*.dib
- JPEG 文件:
*.jpeg
,*.jpg
和*.jpe
- JPEG 2000 文件:
*.jp2
- 便携式网络图形:
*.png
- 便携式图像格式:
*.pbm
,*.pgm
和*.ppm
- TIFF 文件:
*.tiff
和*.tif
位图图像文件(BMP)或设备独立位图(DIB)文件格式是用于存储的光栅图像文件格式。 位图数字图像。 BMP 文件格式可以处理各种颜色深度的 2D 数字图像,还可以处理数据压缩,alpha 通道或颜色配置文件。
联合图像专家组(JPEG)是一种光栅图像文件格式,用于存储已压缩以在小文件中存储大量信息的图像。
JPEG 2000 是图像压缩标准和编码系统,它使用基于小波的压缩技术来提供高级别的可伸缩性和可访问性。 以此方式,JPEG 2000 压缩的图像比常规 JPEG 少。
便携式网络图形(PNG)是一种压缩的光栅图形文件格式,于 1994 年引入,作为图形交换格式(GIF)的改进替代。
便携式像素图格式(PPM),便携式位图格式(PBM)和便携式灰度图格式(PGM)指定交换图形文件的规则。 几个应用将这些文件格式统称为可移植任意图格式(PNM)。 这些文件是保存图像数据的便捷方法。 此外,它们易于阅读。 从这个意义上讲,PPM,PBM 和 PGM 格式都设计得尽可能简单。
标记图像文件格式(TIFF)是一种可调整的文件格式,用于处理单个文件中的图像和数据。
将无损和有损类型的压缩算法应用于图像,从而导致图像小于未压缩的图像。 一方面,在无损压缩算法中,生成的图像与原始图像等效,这意味着在反转压缩过程之后,生成的图像与原始图像等效(等于)。 另一方面,在有损压缩算法中,生成的图像与原始图像不相等,这意味着图像中的某些细节会丢失。 从这个意义上讲,在许多有损压缩算法中,可以调整压缩级别。
OpenCV 中的坐标系
为了向您展示 OpenCV 中的坐标系以及如何访问单个像素,我们将向您展示 OpenCV 徽标的低分辨率图像:
该徽标的尺寸为20 x 18
像素,即,该图像具有 360 像素。 因此,我们可以在每个轴上添加像素数,如下图所示:
现在,我们来看一下(x, y)
形式的像素索引。 请注意,像素是零索引的,这意味着左上角位于(0, 0)
,而不是(1, 1)
。 看一下下图,该图索引了三个单独的像素。 如您所见,图像的左上角是原点的坐标。 此外,坐标随着其下降而变大:
可以使用与在 Python 中引用数组的单个元素相同的方式从图像中提取单个像素的信息。 在下一节中,我们将看到如何做到这一点。
在 OpenCV 中访问和操作像素
在本节中,您将学习如何使用 OpenCV 访问和读取像素值以及如何对其进行修改。 此外,您将学习如何访问图像属性。 如果要一次处理多个像素,则需要创建图像的兴趣区域(ROI)。 在本节中,您将学习如何执行此操作。 最后,您将学习如何拆分和合并图像。
请记住,在 Python 中,图像表示为 NumPy 数组。 因此,这些示例中包含的大多数操作都与 NumPy 有关,因此需要对 NumPy 包有很好的了解,才能理解这些示例中包含的代码并使用 OpenCV 编写优化的代码。
在 OpenCV 中访问和操纵 BGR 图像的像素
现在,我们将了解如何在 OpenCV 中处理 BGR 图像。 OpenCV 加载彩色图像,因此蓝色通道是第一个,绿色通道是第二个,红色通道是第三个。 请参阅“使用灰度图像访问和操作 OpenCV 中的像素”,以全面了解此概念。
首先,使用cv2.imread()
函数读取要使用的图像。 该图像应位于工作目录中,或者应提供该图像的完整路径。 在这种情况下,我们将读取logo.png
图像并将其存储在img
变量中:
# The function cv2.imread() is used to read an image from the the working directory
# Alternatively, you should provide a full path of the image:
# Load OpenCV logo image (in this case from the working directoy):
img = cv2.imread('logo.png')
将图像加载到img
后,我们将可以访问图像的某些属性。 我们将从加载的图像中提取的第一个属性是shape
,它将告诉我们行,列和通道的数量(如果图像是彩色的)。 我们会将这些信息存储在dimensions
变量中,以备将来使用:
# To get the dimensions of the image use img.shape
# img.shape returns a tuple of number of rows, columns and channels (if a colour image)
# If image is grayscale, img.shape returns a tuple of number of rows and columns.
# So,it can be used to check if loaded image is grayscale or color image.
# Get the shape of the image:
dimensions = img.shape
另一个属性是图像的大小(img.size
等于高度×宽度×通道
的乘积):
# Total number of elements is obtained by img.size:
total_number_of_elements= img.size
属性图像数据类型是通过img.dtype
获得的。 在这种情况下,图像数据类型为uint8
(无符号字符),因为值在[0 - 255]
范围内:
# Image datatype is obtained by img.dtype.
# img.dtype is very important because a large number of errors is caused by invalid datatype.
# Get the image datatype:
image_dtype = img.dtype
要显示图像,我们将使用cv2.imshow()
函数在窗口中显示图像。 窗口自动适合图像尺寸。 此函数的第一个参数是窗口名称,第二个参数是要显示的图像。 在这种情况下,由于加载的图像已存储在img
变量中,因此我们将使用此变量作为第二个参数:
# The function cv2.imshow() is used to display an image in a window
# The first argument of this function is the window name
# The second argument of this function is the image to be shown.
# Each created window should have different window names.
# Show original image:
cv2.imshow("original image", img)
显示图像后,cv2.waitKey()
函数(一种键盘绑定函数)将为任何键盘事件等待指定的毫秒数。 参数是时间(以毫秒为单位)。 如果此时按任何键,程序将继续。 如果毫秒数是0
(cv2.waitKey(0)
),它将无限期地等待击键。 因此,此函数将使我们能够看到显示的窗口中等待按键输入:
# The function cv2.waitKey(), which is a keyboard binding function, waits for any keyboard event.
# This function waits the value indicated by the argument (in milliseconds).
# If any keyboard event is produced in this period of time, the program continues its execution
# If the value of the argument is 0, the program waits indefinitely until a keyboard event is produced:
cv2.waitKey(0)
要访问(读取)像素值,我们需要将所需像素的行和列提供给img
变量,该变量包含加载的图像。 例如,要获取像素值[x=40
,y=6
),我们将使用以下代码:
# A pixel value can be accessed by row and column coordinates.
# In case of BGR image, it returns an array of (Blue, Green, Red) values.
# Get the value of the pixel (x=40, y=6):
(b, g, r) = img[6, 40]
我们已将三个像素值加载到三个变量(b,g,r)
中。 您可以在此处看到 OpenCV 对彩色图像使用 BGR 格式。 此外,我们一次只能访问一个通道。 在这种情况下,我们将使用行,列和所需通道的索引进行索引。 例如,要仅获取像素的蓝色值(x=40
和y=6
),我们将使用以下代码:
# We can only access one channel at a time.
# In this case, we will use row, column and the index of the desired channel for indexing.
# Get only blue value of the pixel (x=40, y=6):
b = img[6, 40, 0]
像素值也可以以相同的方式修改。 请记住,它是(b, g, r)
格式。 例如,要将像素(x=40
,y=6
)设置为红色,请执行以下操作:
# The pixel values can be also modified in the same way - (b, g, r) format:
img[6, 40] = (0, 0, 255)
有时,您将不得不处理某个区域而不是一个像素。 在这种情况下,应提供值的范围而不是各个值。 例如,要到达图像的左上角,请输入以下内容:
# In this case, we get the top left corner of the image:
top_left_corner = img[0:50, 0:50]
top_left_corner
变量是另一张图像(小于img
),但是我们可以用相同的方式来播放它。
在 OpenCV 中访问和操作灰度图像的像素
灰度图像只有一个通道。 因此,在处理这些图像时会引入一些差异。 我们将在这里重点介绍这些差异。
同样,我们将使用cv2.imread()
函数读取图像。 在这种情况下,需要第二个参数,因为我们要以灰度加载图像。 第二个参数是一个标志,指定应读取图像的方式。 加载灰度图像所需的值为cv2.IMREAD_GRAYSCALE
:
# The function cv2.imshow() is used to display an image in a window
# The first argument of this function is the window name
# The second argument of this function is the image to be shown.
# In this case, the second argument is needed because we want to load the image in grayscale.
# Second argument is a flag specifying the way the image should be read.
# Value needed for loading an image in grayscale: 'cv2.IMREAD_GRAYSCALE'.
# load OpenCV logo image:
gray_img = cv2.imread('logo.png', cv2.IMREAD_GRAYSCALE)
在这种情况下,我们将图像存储在gray_img
变量中。 如果获得图像的尺寸(使用gray_img.shape
),则将仅获得两个值,即行和列。 在灰度图像中,不提供通道信息:
# To get the dimensions of the image use img.shape
# If color image, img.shape returns returns a tuple of number of rows, columns and channels
# If grayscale, returns a tuple of number of rows and columns.
# So, it can be used to check if the loaded image is grayscale or color image.
# Get the shape of the image (in this case only two components!):
dimensions = gray_img.shape
img.shape
将以元组的形式返回图像的尺寸,例如(99, 82)
。
像素值可以通过行和列坐标进行访问。 在灰度图像中,仅获得一个值(通常称为像素的强度)。 例如,如果要获取像素强度[x=40
,y=6
),则可以使用以下代码:
# You can access a pixel value by row and column coordinates.
# For BGR image, it returns an array of (Blue, Green, Red) values.
# Get the value of the pixel (x=40, y=6):
i = gray_img[6, 40]
图像的像素值也可以用相同的方式修改。 例如,如果我们要将像素(x=40
,y=6
)的值更改为黑色(强度等于0
),则可以使用以下代码:
# You can modify the pixel values of the image in the same way.
# Set the pixel to black:
gray_img[6, 40] = 0
OpenCV 中的 BGR 顺序
我们已经提到过 OpenCV 使用 BGR 颜色格式而不是 RGB 颜色格式。 可以在下图中看到,您可以在其中看到三个通道的顺序:
下图可以看到 BGR 图像的像素结构。 特别是,为了说明目的,我们详细介绍了如何访问像素(y = n, x = 1
):
OpenCV 的最初开发人员选择了 BGR 色彩格式(而不是 RGB 格式),因为当时 BGR 色彩格式在软件提供商和相机制造商中非常受欢迎。 例如,在 Windows 中,使用 COLORREF 指定颜色值时,他们使用 BGR 格式0x00bbggrr
。 总而言之,选择 BGR 是出于历史原因。
此外,其他 Python 包使用 RGB 颜色格式。 因此,我们需要知道如何将图像从一种格式转换为另一种格式。 例如,Matplotlib 使用 RGB 颜色格式。 Matplotlib 是最受欢迎的 2D Python 绘图库,可为您提供多种绘图方法。 您可以与绘制的图像进行交互(例如,放大并保存图像)。 Matplotlib 可以在 Python 脚本或 Jupyter 笔记本中使用。 您可以查看 Matplotlib 文档以获取更多详细信息。
因此,对于您的项目而言,一个不错的选择是使用 Matplotlib 包而不是 OpenCV 提供的函数来显示图像。 现在,我们将看到如何处理两个库中的不同颜色格式。
首先,我们使用cv2.imread()
函数加载图像:
# Load image using cv2.imread:
img_OpenCV = cv2.imread('logo.png')
图像存储在img_OpenCV
变量中,因为cv2.imread()
函数以 BGR 顺序加载图像。 然后,我们使用cv2.split()
函数将加载的图像分为三个通道(b, g, r)
。 该函数的参数是我们要分割的图像:
# Split the loaded image into its three channels (b, g, r):
b, g, r = cv2.split(img_OpenCV)
下一步是再次合并通道(以基于通道提供的信息构建新图像),但顺序不同。 我们更改b
和r
通道的顺序以遵循 RGB 格式,即 Matplotlib 需要的格式:
# Merge again the three channels but in the RGB format:
img_matplotlib = cv2.merge([r, g, b])
此时,我们有两个图像(img_OpenCV
和img_matplotlib
),将使用 OpenCV 和 Matplotlib 对其进行绘制,以便可以看到结果。 首先,我们将使用 Matplotlib 显示这两个图像。
为了在同一窗口中显示带有 Matplotlib 的两个图像,我们将使用subplot
,它将在同一窗口中放置多个图像。 您可以在subplot
中使用三个参数,例如subplot(m,n,p)
。 在这种情况下,subplot
处理xn
网格中的图,其中m
建立行数,n
建立列数,p
确定要在网格中放置绘图的位置。 为了显示 Matplotlib 的图像,我们将使用imshow
。
在这种情况下,由于我们水平显示两个图像m = 1
和n = 2
。 我们将在第一个子图img_OpenCV
中使用p = 1
,在第二个子图img_matplotlib
中使用p = 2
:
# Show both images (img_OpenCV and img_matplotlib) using matplotlib
# This will show the image in wrong color:
plt.subplot(121)
plt.imshow(img_OpenCV)
# This will show the image in true color:
plt.subplot(122)
plt.imshow(img_matplotlib)
plt.show()
因此,您将获得的输出应与下图所示的输出非常相似:
如您所见,第一个子图以错误的颜色(BGR 顺序)显示图像,而第二个子图以真实的颜色(RGB 顺序)显示图像。 以相同的方式,我们将使用cv2.imshow()
显示两个图像:
# Show both images (img_OpenCV and img_matplotlib) using cv2.imshow()
# This will show the image in true color:
cv2.imshow('bgr image', img_OpenCV)
# This will show the image in wrong color:
cv2.imshow('rgb image', img_matplotlib)
cv2.waitKey(0)
cv2.destroyAllWindows()
以下屏幕截图显示了执行前面的代码将获得的结果:
正如预期的那样,屏幕截图以真实的颜色显示图像,而第二个图以错误的颜色显示图像。
此外,如果要在同一窗口中显示两个图像,则可以构建一个完整的图像,其中包含两个图像,并将它们水平连接。 为此,我们将使用 NumPy 的concatenate()
方法。 此方法的参数是要连接的两个图像和轴。 在这种情况下,axis = 1
(水平堆叠):
# To stack horizontally (img_OpenCV to the left of img_matplotlib):
img_concats = np.concatenate((img_OpenCV, img_matplotlib), axis=1)
# Now, we show the concatenated image:
cv2.imshow('bgr image and rgb image', img_concats)
cv2.waitKey(0)
cv2.destroyAllWindows()
查看以下屏幕截图以查看连接的图像:
要考虑的一个因素是cv2.split()
是一项耗时的操作。 根据您的需求,考虑使用 NumPy 索引。 例如,如果要获取图像的一个通道,而不是使用cv2.split()
来获取所需的通道,则可以使用 NumPy 索引。 请参阅以下示例,以使用 NumPy 索引获取通道:
# Using numpy capabilities to get the channels and to build the RGB image
# Get the three channels (instead of using cv2.split):
B = img_OpenCV[:, :, 0]
G = img_OpenCV[:, :, 1]
R = img_OpenCV[:, :, 2]
另一个考虑因素是,您可以使用 NumPy 在单个指令中将图像从 BGR 转换为 RGB:
# Transform the image BGR to RGB using Numpy capabilities:
img_matplotlib = img_OpenCV[:, :, ::-1]
为了总结本章的所有内容,我们创建了两个 Jupyter 笔记本。 在这些笔记本中,您可以使用到目前为止介绍的所有概念:
Getting-And-Setting-BGR.ipynb
Getting-And-Setting-GrayScale.ipynb
利用福利笔记本(以及本章中包括的所有信息)的优势,无需其他信息即可使用它们。 因此,继续尝试一下。 请记住(请参阅第 1 章,“设置 OpenCV”),要运行笔记本,您需要在终端机(Mac/Linux)或命令提示符(Windows)上运行以下命令:
$ jupyter notebook
此命令将打印与笔记本服务器有关的信息,包括 Web 应用的 URL(默认情况下,此 URL 为http://localhost:8888
)。 此外,此命令还将打开指向该 URL 的 Web 浏览器:
此时,您可以通过单击“上载”按钮来上载Getting-And-Setting-BGR.ipynb
和Getting-And-Setting-GrayScale.ipynb
文件(请参见上一个屏幕截图)。 这些文件使用logo.png
图像。 因此,您应该以相同的方式上传此图像。 加载这三个文件之后,您应该看到已加载以下文件:
此时,您可以通过单击打开这些笔记本。 您应该看到笔记本的内容,如以下屏幕快照所示:
最后,您可以开始执行加载的笔记本文档。 您可以通过按Shift + Enter
来逐步执行笔记本(一次一个单元)。 另外,您可以通过单击“单元格 | 步骤”一步来执行整个笔记本。 运行全部菜单。 此外,您还可以通过单击“内核 | 重新启动内核”菜单(计算引擎)。
有关编辑笔记本的更多信息,请查看这里,它也是笔记本!
总结
在本章中,我们研究了与图像有关的关键概念。 图像构成了构建计算机视觉项目所必需的丰富信息。 OpenCV 使用 BGR 颜色格式而不是 RGB,但是某些 Python 包(例如 Matplotlib)使用后者。 因此,我们介绍了如何将图像从一种颜色格式转换为另一种颜色格式。
此外,我们总结了使用图像的主要函数和选项:
- 访问图像属性
- 一些 OpenCV 函数,例如
cv2.imread()
,cv2.split()
,cv2.merge()
,cv2.imshow()
,cv2.waitKey()
和cv2.destroyAllWindows()
- 如何在 BGR 和灰度图像中获取和设置图像像素
最后,我们包括了两个笔记本,可让您使用所有这些概念。 请记住,一旦加载了笔记本,就可以通过按Shift + Enter
来逐步运行它,或者单击“单元格” |“一步”来运行笔记本。 运行全部菜单。
在下一章中,您将学习如何处理文件和图像,这是构建计算机视觉应用所必需的。
问题
- 主要的图像处理步骤是什么?
- 三种处理级别是什么?
- 灰度图像和黑白图像有什么区别?
- 什么是像素?
- 什么是图像分辨率?
- 您使用哪些 OpenCV 函数执行以下操作?
- 加载(读取)图像
- 显示图像
- 等待按键
- 拆分通道
- 合并通道
- 您使用什么命令来运行 Jupyter 笔记本?
- 以下三元组会得到什么颜色?
B = 0
,G = 255
,R = 255
B = 255
,G = 255
,R = 0
B = 255
,G = 0
,R = 255
B = 255
,G = 255
,R = 255
- 假设您已在
img
中加载了图像。 如何检查img
是彩色还是灰度?
进一步阅读
以下参考文献将帮助您更深入地了解本章中介绍的概念:
- 有关 Git 的更多信息,请看这本书:
- 有关 Jupyter 笔记本的更多信息:
《Jupyter 笔记本:第一部分》,作者 Dan Toomey
三、处理文件和图像
在任何类型的项目中,处理文件和图像都是关键。 从这个意义上讲,许多项目都应将文件作为数据输入形式来使用。 此外,项目可以在完成任何类型的处理后生成一些数据,这些数据可以以文件或图像的形式输出。 在计算机视觉中,由于这些类型的项目的固有特征(例如,要处理的图像和由机器学习算法生成的模型),这种信息流(输入-处理-输出)具有特殊的意义。
在本章中,我们将看到如何处理文件和图像。 您将学习如何处理构建计算机视觉应用所必需的文件和图像。
更具体地说,我们将涵盖以下主题:
- 有关处理文件和图像的理论介绍
- 读/写图像
- 读取相机帧和视频文件
- 写入视频文件
- 玩转视频捕获属性
技术要求
本章的技术要求如下:
- Python 和 OpenCV
- 特定于 Python 的 IDE
- NumPy 和 Matplotlib Python 包
- Git 客户端
可以在这个页面上访问使用 Python 精通 OpenCV 的 GitHub 存储库。
处理文件和图像的简介
在深入处理文件和图像之前,我们将为您提供本章将要介绍的内容的概述。 下图概述了此概述:
在上图中,您可以看到计算机视觉项目(例如 OpenCV 和 Python 项目)应处理一些输入文件(例如文件和图片)。 另外,经过一些处理后,项目可以输出一些文件(例如图像和文件)。 因此,在本章中,我们将了解如何满足这些要求以及如何正确实现此流程(输入-处理-输出)。
执行程序的首要步骤是正确处理命令行参数,命令行参数是提供给包含某种参数化信息的程序或脚本的参数。 例如,如果编写脚本以将两个数字相加,则通常的方法是具有两个参数,这是执行加法所必需的两个数字。 在计算机视觉项目中,图像和不同类型的文件通常作为命令行参数传递给脚本。
命令行参数是参数化程序执行的常用且简单的方法。
sys.argv
为了处理命令行参数,Python 使用sys.argv
。 从这种意义上讲,执行程序时,Python 从命令行获取所有值并将其设置在sys.argv
列表中。 列表的第一个元素是脚本的完整路径(或脚本名称-取决于操作系统),该路径始终为sys.argv[0]
。 列表的第二个元素是脚本的第一个参数,即sys.argv[1]
,依此类推。 可以在下图中看到,其中sysargv_python.py
脚本使用两个参数执行:
要查看sys.argv
的工作方式,我们将使用sysargv_python.py
脚本:
# Import the required packages
import sys
# We will print some information in connection with sys.argv to see how it works:
print("The name of the script being processed is: '{}'".format(sys.argv[0]))
print("The number of arguments of the script is: '{}'".format(len(sys.argv)))
print("The arguments of the script are: '{}'".format(str(sys.argv)))
如果执行此脚本时不带任何参数,则将看到以下信息:
The name of the script being processed is: 'sysargv_python.py'
The number of arguments of the script is: '1'
The arguments of the script are: '['sysargv_python.py']'
另外,如果我们使用一个参数(例如sysargv_python.py OpenCV
)执行此脚本,我们将获得以下信息:
The name of the script being processed is: 'sysargv_python.py'
The number of arguments of the script is: '2'
The arguments of the script are: '['sysargv_python.py', 'OpenCV']'
如您所见,列表的第一个元素sysargv_python.py
(sys.argv[0]
)是脚本名称。 列表(sys.argv[1]
)中的第二个元素OpenCV
是脚本的第一个参数。
argv[0]
是脚本名,如果它不是完整路径名,则取决于操作系统。 请参阅这里了解更多信息。
argparse
– 命令行选项和参数解析
应该考虑到,我们不应该直接处理sys.argv
,主要是当我们的程序采用复杂的参数或多个文件名时。 另外,我们应该使用 Python 的argparse
库,该库以系统的方式处理命令行参数,从而使其可以编写用户友好的命令行程序。 换句话说,Python 在标准库中有一个名为argparse
的模块,用于解析命令行参数。 首先,程序确定所需的参数。 然后,argparse
将研究如何将这些参数解析为sys.argv
。 同样,argparse
会生成帮助和使用消息,并在提供无效参数时发出错误。
此处介绍此模块的最小示例为argparse_minimal.py
,如下所示:
# Import the required packages
import argparse
# We first create the ArgumentParser object
# The created object 'parser' will have the necessary information
# to parse the command-line arguments into data types.
parser = argparse.ArgumentParser()
# The information about program arguments is stored in 'parser' and used when parse_args() is called.
# ArgumentParser parses arguments through the parse_args() method:
parser.parse_args()
不带参数运行此脚本将不会显示任何内容给stdout
。 但是,如果包含--help
(或-h
)选项,我们将获得脚本的用法消息:
usage: argparse_minimal.py [-h]
optional arguments:
-h, --help show this help message and exit
指定其他任何参数都会导致错误,例如:
argparse_minimal.py 6
usage: argparse_minimal.py [-h]
argparse_minimal.py: error: unrecognized arguments: 6
因此,我们必须使用-h
参数调用此脚本。 这样,将显示使用消息信息。 由于未定义任何参数,因此不允许其他可能性。 这样,引入argparse
的第二个示例是添加一个参数,可以在argparse_positional_arguments.py
的示例中看到:
# Import the required packages
import argparse
# We first create the ArgumentParser object
# The created object 'parser' will have the necessary information
# to parse the command-line arguments into data types.
parser = argparse.ArgumentParser()
# We add a positional argument using add_argument() including a help
parser.add_argument("first_argument", help="this is the string text in connection with first_argument")
# The information about program arguments is stored in 'parser'
# Then, it is used when the parser calls parse_args().
# ArgumentParser parses arguments through the parse_args() method:
args = parser.parse_args()
# We get and print the first argument of this script:
print(args.first_argument)
我们添加了add_argument()
方法。 此方法用于指定程序将接受的命令行选项。 在这种情况下,需要first_argument
参数。 另外,argparse
模块存储所有参数,使其名称与每个添加的参数的名称(在本例中为first_argument
)匹配。 因此,为了获得我们的参数,我们执行args.first_argument
。
如果此脚本以argparse_positional_arguments.py 5
的身份执行,则输出将为5
。 但是,如果脚本不带参数argparse_positional_arguments.py
来执行,则输出如下:
usage: argparse_positional_arguments.py [-h] first_argument
argparse_positional_arguments.py: error: the following arguments are required: first_argument
最后,如果我们使用-h
选项执行脚本,输出将如下所示:
usage: argparse_positional_arguments.py [-h] first_argument
positional arguments:
first_argument this is the string text in connection with first_argument
optional arguments:
-h, --help show this help message and exit
默认情况下,argparse
将我们提供的选项视为字符串。 因此,如果参数不是字符串,则应建立type
选项。 我们将看到argparse_sum_two_numbers.py
脚本添加了两个参数,因此,这两个参数属于int
类型:
# Import the required packages
import argparse
# We first create the ArgumentParser object
# The created object 'parser' will have the necessary information
# to parse the command-line arguments into data types.
parser = argparse.ArgumentParser()
# We add 'first_number' argument using add_argument() including a help. The type of this argument is int
parser.add_argument("first_number", help="first number to be added", type=int)
# We add 'second_number' argument using add_argument() including a help The type of this argument is int
parser.add_argument("second_number", help="second number to be added", type=int)
# The information about program arguments is stored in 'parser'
# Then, it is used when the parser calls parse_args().
# ArgumentParser parses arguments through the parse_args() method:
args = parser.parse_args()
print("args: '{}'".format(args))
print("the sum is: '{}'".format(args.first_number + args.second_number))
# Additionally, the arguments can be stored in a dictionary calling vars() function:
args_dict = vars(parser.parse_args())
# We print this dictionary:
print("args_dict dictionary: '{}'".format(args_dict))
# For example, to get the first argument using this dictionary:
print("first argument from the dictionary: '{}'".format(args_dict["first_number"]))
如果在不带参数的情况下执行脚本,则输出将如下所示:
argparse_sum_two_numbers.py
usage: argparse_sum_two_numbers.py [-h] first_number second_number
argparse_sum_two_numbers.py: error: the following arguments are required: first_number, second_number
另外,如果我们使用-h
选项执行脚本,则输出将如下所示:
argparse_sum_two_numbers.py --help
usage: argparse_sum_two_numbers.py [-h] first_number second_number
positional arguments:
first_number first number to be added
second_number second number to be added
optional arguments:
-h, --help show this help message and exit
应该考虑到,在前面的示例中,我们通过调用vars()
函数引入了将参数存储在字典中的可能性:
# Additionally, the arguments can be stored in a dictionary calling vars() function:
args_dict = vars(parser.parse_args())
# We print this dictionary:
print("args_dict dictionary: '{}'".format(args_dict))
# For example, to get the first argument using this dictionary:
print("first argument from the dictionary: '{}'".format(args_dict["first_number"]))
例如,如果此脚本以argparse_sum_two_numbers.py 5 10
执行,则输出将如下所示:
args: 'Namespace(first_number=5, second_number=10)'
the sum is: '15'
args_dict dictionary: '{'first_number': 5, 'second_number': 10}'
first argument from the dictionary: '5'
这是对sys.argv
和argparse
的快速介绍。 可以在这个页面上看到argparse
的高级介绍。 此外,其文档非常详细,细致,并涵盖了许多示例。 此时,您现在可以在 OpenCV 和 Python 程序中学习如何使用argparse
读取和写入图像,这将在“读取和写入图像”部分中显示。
读写图像
在计算机视觉项目中,图像通常在脚本中用作命令行参数。 在以下各节中,我们将看到如何读取和写入图像。
在 OpenCV 中读取图像
以下示例argparse_load_image.py
展示了如何加载图像:
# Import the required packages
import argparse
import cv2
# We first create the ArgumentParser object
# The created object 'parser' will have the necessary information
# to parse the command-line arguments into data types.
parser = argparse.ArgumentParser()
# We add 'path_image' argument using add_argument() including a help. The type of this argument is string (by default)
parser.add_argument("path_image", help="path to input image to be displayed")
# The information about program arguments is stored in 'parser'
# Then, it is used when the parser calls parse_args().
# ArgumentParser parses arguments through the parse_args() method:
args = parser.parse_args()
# We can now load the input image from disk:
image = cv2.imread(args.path_image)
# Parse the argument and store it in a dictionary:
args = vars(parser.parse_args())
# Now, we can also load the input image from disk using args:
image2 = cv2.imread(args["path_image"])
# Show the loaded image:
cv2.imshow("loaded image", image)
cv2.imshow("loaded image2", image2)
# Wait until a key is pressed:
cv2.waitKey(0)
# Destroy all windows:
cv2.destroyAllWindows()
在此示例中,必需的参数为path_image
,其中包含我们要加载的图像的路径。 图像的路径是一个字符串。 因此,位置参数中不应包含任何类型,因为默认情况下它是字符串。 args.path_image
和args["path_image"]
都将包含图像的路径(从参数获取值的两种不同方式),因此我们将它们用作cv2.imread()
函数的参数。
在 OpenCV 中读取和写入图像
一种常见的方法是加载图像,执行某种处理,然后最终输出处理后的图像(请参阅第 2 章, OpenCV 中的图像基础知识,这三部分的详细说明) 脚步)。 从这个意义上讲,可以将处理后的图像保存到磁盘。 在以下示例中,介绍了这三个步骤(加载,处理和保存)。 在这种情况下,处理步骤非常简单(将图像转换为灰度)。 在以下示例argparse_load_processing_save_image.py
中可以看到:
# Import the required packages
import argparse
import cv2
# We first create the ArgumentParser object
# The created object 'parser' will have the necessary information
# to parse the command-line arguments into data types.
parser = argparse.ArgumentParser()
# Add 'path_image_input' argument using add_argument() including a help. The type is string (by default):
parser.add_argument("path_image_input", help="path to input image to be displayed")
# Add 'path_image_output' argument using add_argument() including a help. The type is string (by default):
parser.add_argument("path_image_output", help="path of the processed image to be saved")
# Parse the argument and store it in a dictionary:
args = vars(parser.parse_args())
# We can load the input image from disk:
image_input = cv2.imread(args["path_image_input"])
# Show the loaded image:
cv2.imshow("loaded image", image_input)
# Process the input image (convert it to grayscale):
gray_image = cv2.cvtColor(image_input, cv2.COLOR_BGR2GRAY)
# Show the processed image:
cv2.imshow("gray image", gray_image)
# Save the processed image to disk:
cv2.imwrite(args["path_image_output"], gray_image)
# Wait until a key is pressed:
cv2.waitKey(0)
# Destroy all windows:
cv2.destroyAllWindows()
在前面的示例中,有两个必需的参数。 第一个是path_image_input
,它包含我们要加载的图像的路径。 图像的路径是一个字符串。 因此,位置参数中不应包含任何类型,因为默认情况下它是字符串。 第二个是path_image_output
,它包含我们要保存的结果图像的路径。 在此示例中,处理步骤包括将加载的图像转换为灰度:
# Process the input image (convert it to grayscale)
gray_image = cv2.cvtColor(image_input, cv2.COLOR_BGR2GRAY)
应当注意,第二个参数cv2.COLOR_BGR2GRAY
假定加载的图像是 BGR 彩色图像。 如果您已加载 RGB 彩色图像,并且想要将其转换为灰度,则应使用cv2.COLOR_RGB2GRAY
。
这是一个非常简单的处理步骤,但为简单起见将其包括在内。 在以后的章节中,将显示更详细的处理算法。
读取相机帧和视频文件
在某些项目中,您必须捕获相机帧(例如,使用笔记本电脑的网络摄像头捕获的帧)。 在 OpenCV 中,我们具有cv2.VideoCapture
,该类用于从不同来源(例如图像序列,视频文件和相机)捕获视频。 在本节中,我们将看到一些示例,向我们介绍此类用于捕获相机帧的类。
读取相机帧
第一个示例read_camera.py
向您展示如何从连接到计算机的相机读取帧。 必需的参数为index_camera
,它指示要读取的摄像机的索引。 如果已将网络摄像头连接到计算机,则其索引为0
。 另外,如果您有第二台摄像机,则可以通过1
进行选择。 如您所见,此参数的类型为int
。
使用cv2.VideoCapture
的第一步是创建一个要使用的对象。 在这种情况下,对象是capture
,我们这样调用构造器:
# We create a VideoCapture object to read from the camera (pass 0):
capture = cv2.VideoCapture(args.index_camera)
如果index_camera
是0
(您的第一个连接的摄像机),则它等效于cv2.VideoCapture(0)
。 为了检查连接是否正确建立,我们使用capture.isOpened()
方法,如果无法建立连接,则返回False
。 同样,如果捕获已正确初始化,则此方法返回True
。
要从摄像机逐帧捕获素材,我们调用capture.read()
方法,该方法从摄像机返回帧。 该框架与 OpenCV 中的图像具有相同的结构,因此我们可以以相同的方式使用它。 例如,要将帧转换为灰度,请执行以下操作:
gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
此外,capture.read()
返回布尔值。 该布尔值指示是否已从捕获对象正确读取帧。
访问捕获对象的某些属性
最后,您可以使用capture.get(property_identifier)
访问捕获对象的某些属性。 在这种情况下,我们获得一些属性,例如帧宽度,帧高度和每秒帧(fps)。 如果我们调用不支持的属性,则返回值为0
:
# Import the required packages
import cv2
import argparse
# We first create the ArgumentParser object
# The created object 'parser' will have the necessary information
# to parse the command-line arguments into data types.
parser = argparse.ArgumentParser()
# We add 'index_camera' argument using add_argument() including a help.
parser.add_argument("index_camera", help="index of the camera to read from", type=int)
args = parser.parse_args()
# We create a VideoCapture object to read from the camera (pass 0):
capture = cv2.VideoCapture(args.index_camera)
# Get some properties of VideoCapture (frame width, frame height and frames per second (fps)):
frame_width = capture.get(cv2.CAP_PROP_FRAME_WIDTH)
frame_height = capture.get(cv2.CAP_PROP_FRAME_HEIGHT)
fps = capture.get(cv2.CAP_PROP_FPS)
# Print these values:
print("CV_CAP_PROP_FRAME_WIDTH: '{}'".format(frame_width))
print("CV_CAP_PROP_FRAME_HEIGHT : '{}'".format(frame_height))
print("CAP_PROP_FPS : '{}'".format(fps))
# Check if camera opened successfully
if capture.isOpened()is False:
print("Error opening the camera")
# Read until video is completed
while capture.isOpened():
# Capture frame-by-frame from the camera
ret, frame = capture.read()
if ret is True:
# Display the captured frame:
cv2.imshow('Input frame from the camera', frame)
# Convert the frame captured from the camera to grayscale:
gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# Display the grayscale frame:
cv2.imshow('Grayscale input camera', gray_frame)
# Press q on keyboard to exit the program
if cv2.waitKey(20) & 0xFF == ord('q'):
break
# Break the loop
else:
break
# Release everything:
capture.release()
cv2.destroyAllWindows()
保存相机帧
可以很容易地修改前面的示例,以添加有用的功能。 假设您想在发生一些有趣的事情时将一些帧保存到磁盘。 在下面的示例read_camera_capture.py
中,我们将添加此功能。 当按下键盘上的C
键时,我们将当前帧保存到磁盘。 我们同时保存了 BGR 和灰度帧。 执行此功能的代码如下所示:
# Press c on keyboard to save current frame
if cv2.waitKey(20) & 0xFF == ord('c'):
frame_name = "camera_frame_{}.png".format(frame_index)
gray_frame_name = "grayscale_camera_frame_{}.png".format(frame_index)
cv2.imwrite(frame_name, frame)
cv2.imwrite(gray_frame_name, gray_frame)
frame_index += 1
ord('c')
使用八位返回代表c
字符的值。 此外,cv2.waitKey()
值是按位的,并且将&
运算符与0xFF
一起使用只能得到其最后八位。 因此,我们可以在这两个 8 位值之间进行比较。 当按下C
键时,我们为两个帧建立名称。 然后,我们将两个图像保存到磁盘。 最后,增加frame_index
,以便为保存下一帧做好准备。 请查看read_camera_capture.py
以查看此脚本的完整代码。
读取视频文件
cv2.VideoCapture
也允许我们阅读视频文件。 因此,要读取视频文件,在创建cv2.VideoCapture
对象时应提供视频文件的路径:
# We first create the ArgumentParser object
# The created object 'parser' will have the necessary information
# to parse the command-line arguments into data types.
parser = argparse.ArgumentParser()
# We add 'video_path' argument using add_argument() including a help.
parser.add_argument("video_path", help="path to the video file")
args = parser.parse_args()
# Create a VideoCapture object. In this case, the argument is the video file name:
capture = cv2.VideoCapture(args.video_path)
请查看read_video_file.py
,以查看如何使用cv2.VideoCapture
读取和显示视频文件的完整示例。
从 IP 摄像机读取
为了完成cv2.VideoCapture
,我们将看看如何从 IP 摄像机读取数据。 从 OpenCV 中的 IP 摄像机读取与从文件读取非常相似。 从这个意义上讲,仅应更改cv2.VideoCapture
构造器的参数。 这样做的好处是,您无需在本地网络中使用 IP 摄像机即可尝试此功能。 您可以尝试连接许多公共 IP 摄像机。 例如,我们要连接到公共 IP 摄像机,该摄像机位于俱乐部 Näutic 德拉塞尔瓦港–布拉瓦海岸–克雷乌斯角(西班牙赫罗纳)。 该端口的网页位于这个页面上。 您可以导航到网络摄像头部分以找到一些要连接的网络摄像头。
因此,您唯一需要修改的就是cv2.VideCapture
的参数。 在这种情况下,它是http://217.126.89.102:8010/axis-cgi/mjpg/video.cgi
。 如果执行此示例(read_ip_camera.py
),应该会看到类似于以下屏幕截图的内容,其中显示了从 IP 摄像机获得的 BGR 和灰度图像:
写入视频文件
在本节中,我们将看到如何使用cv2.VideoWriter
写入视频文件。 但是,应首先介绍一些概念(例如 fps,编解码器和视频文件格式)。
计算每秒帧
在“读取摄像机帧和视频文件”部分中,我们看到了如何从cv2.VideoCapture
对象获得一些属性。 fps 是计算机视觉项目中的重要指标。 该度量指示每秒处理多少帧。 可以肯定地说,较高的 fps 更好。 但是,算法每秒应处理的帧数取决于您要解决的特定问题。 例如,如果您的算法应跟踪并检测沿着街道行走的人,则 15 fps 可能就足够了。 但是,如果您的目标是检测和跟踪高速公路上快速行驶的汽车,则可能需要 20-25 fps。
因此,了解如何在计算机视觉项目中计算 fps 指标很重要。 在下面的示例read_camera_fps.py
中,我们将修改read_camera.py
以输出 fps 数。 关键点显示在以下代码中:
# Read until the video is completed, or 'q' is pressed
while capture.isOpened():
# Capture frame-by-frame from the camera
ret, frame = capture.read()
if ret is True:
# Calculate time before processing the frame:
processing_start = time.time()
# All the processing should be included here
# ...
# ...
# End of processing
# Calculate time after processing the frame
processing_end = time.time()
# Calculate the difference
processing_time_frame = processing_end - processing_start
# FPS = 1 / time_per_frame
# Show the number of frames per second
print("fps: {}".format(1.0 / processing_time_frame))
# Break the loop
else:
break
首先,我们花点时间进行处理:
processing_start = time.time()
然后,我们在所有处理完成之后花时间:
processing_end = time.time()
然后,我们计算出差异:
processing_time_frame = processing_end - processing_start
最后,我们计算并打印 fps 数:
print("fps: {}".format(1.0 / processing_time_frame))
写入视频文件的注意事项
视频代码是一种用于压缩和解压缩数字视频的软件。 因此,编解码器可以用于将未压缩的视频转换为压缩的视频,也可以用于将压缩的视频转换为未压缩的视频。 压缩视频格式通常遵循称为视频压缩规范或视频编码格式的标准规范。 从这个意义上讲,OpenCV 提供了 FOURCC,这是一个 4 字节的代码,用于指定视频编解码器。 FOURCC 代表四个字符的代码。 可以在这个页面上找到所有可用代码的列表。 应该考虑到受支持的编解码器是平台相关的。 这意味着,如果要使用特定的编解码器,则该编解码器应该已经安装在系统上。 典型的编解码器是 DIVX,XVID,X264 和 MJPG。
此外,视频文件格式是一种用于存储数字视频数据的文件格式。 典型的视频文件格式为 AVI(*.avi
),MP4(*.mp4
),QuickTime(*.mov
)和 Windows Media Video(*.wmv
)。
最后,应考虑到视频文件格式(例如*.avi
)和 FOURCC(例如 DIVX)之间的正确组合并不是简单明了的。 您可能需要尝试并使用这些值。 因此,在 OpenCV 中创建视频文件时,您必须考虑所有这些因素。
下图试图对其进行总结:
此图总结了在 OpenCV 中使用cv2.VideoWriter()
创建视频文件时应考虑的主要注意事项。 在此图中,已创建video_demo.avi
视频。 在这种情况下,FOURCC 值为XVID
,视频文件格式为AVI
(*.avi
)。 最后,应确定 fps 和视频每一帧的尺寸。
此外,下面的示例write_video_file.py
编写了一个视频文件,使用这些概念也可能会有所帮助。 此示例的一些关键点在此处进行了注解。 在此示例中,必需的参数是视频文件名(例如video_demo.avi
):
# We first create the ArgumentParser object
# The created object 'parser' will have the necessary information
# to parse the command-line arguments into data types.
parser = argparse.ArgumentParser()
# We add 'output_video_path' argument using add_argument() including a help.
parser.add_argument("output_video_path", help="path to the video file to write")
args = parser.parse_args()
我们将从连接到计算机的第一台相机拍摄帧。 因此,我们相应地创建对象:
# Create a VideoCapture object and pass 0 as argument to read from the camera
capture = cv2.VideoCapture(0)
接下来,我们将从捕获对象中获取一些属性(帧宽度,帧高度和 fps)。 我们将使用它们来创建我们的视频文件:
# Get some properties of VideoCapture (frame width, frame height and frames per second (fps)):
frame_width = capture.get(cv2.CAP_PROP_FRAME_WIDTH)
frame_height = capture.get(cv2.CAP_PROP_FRAME_HEIGHT)
fps = capture.get(cv2.CAP_PROP_FPS)
现在,我们使用 FOURCC(四字节代码)指定视频编解码器。 请记住,它是依赖于平台的。 在这种情况下,我们将编解码器定义为XVID
:
# FourCC is a 4-byte code used to specify the video codec and it is platform dependent!
# In this case, define the codec XVID
fourcc = cv2.VideoWriter_fourcc('X', 'V', 'I', 'D')
以下行也适用:
# FourCC is a 4-byte code used to specify the video codec and it is platform dependent!
# In this case, define the codec XVID
fourcc = cv2.VideoWriter_fourcc(*'XVID')
然后,我们创建cv2.VideoWriter
对象out_gray
。 我们使用与输入相机相同的属性。 最后一个参数是False
,以便我们可以以灰度级编写视频。 如果我们要创建彩色视频,则最后一个参数应为True
:
# Create VideoWriter object. We use the same properties as the input camera.
# Last argument is False to write the video in grayscale. True otherwise (write the video in color)
out_gray = cv2.VideoWriter(args.output_video_path, fourcc, int(fps), (int(frame_width), int(frame_height)), False)
我们使用capture.read()
从捕获对象逐帧输出。 每帧都将转换为灰度并写入视频文件。 我们可以显示框架,但这不是编写视频所必需的。 如果按q
,程序结束:
# Read until video is completed or 'q' is pressed
while capture.isOpened():
# Read the frame from the camera
ret, frame = capture.read()
if ret is True:
# Convert the frame to grayscale
gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# Write the grayscale frame to the video
out_gray.write(gray_frame)
# We show the frame (this is not necessary to write the video)
# But we show it until 'q' is pressed
cv2.imshow('gray', gray_frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
else:
break
最后,我们释放所有内容(cv2.VideoCapture
和cv2.VideWriter
对象,并销毁创建的窗口):
# Release everything:
capture.release()
out_gray.release()
cv2.destroyAllWindows()
该示例的完整代码可以在write_video_file.py
文件中看到。
玩转视频捕获属性
在前面的一些示例中,我们看到了如何从cv2.VideoCapture
对象获取某些属性。 在本节中,我们将看到如何获取所有属性并了解它们如何工作。 最后,我们将使用这些属性来加载视频文件并向后输出(首先显示视频的最后一帧,依此类推)。
从视频捕获对象获取所有属性
首先,我们创建read_video_file_all_properties.py
脚本以显示所有属性。 其中一些属性仅在我们使用相机(不适用于视频文件)时才起作用。 在这些情况下,将返回0
值。 此外,我们创建了decode_fourcc()
函数,该函数将capture.get(cv2.CAP_PROP_FOURCC)
返回的值转换为包含编解码器的int
表示形式的字符串值。 从这个意义上讲,此值应转换为四字节的char
表示形式,以正确输出编解码器。 因此,decode_fourcc()
函数可以解决此问题。
该函数的代码如下:
def decode_fourcc(fourcc):
"""Decodes the fourcc value to get the four chars identifying it
"""
fourcc_int = int(fourcc)
# We print the int value of fourcc
print("int value of fourcc: '{}'".format(fourcc_int))
# We can also perform this in one line:
# return "".join([chr((fourcc_int >> 8 * i) & 0xFF) for i in range(4)])
fourcc_decode = ""
for i in range(4):
int_value = fourcc_int >> 8 * i & 0xFF
print("int_value: '{}'".format(int_value))
fourcc_decode += chr(int_value)
return fourcc_decode
为了说明其工作原理,下图总结了主要步骤:
如您所见,第一步是获取capture.get(cv2.CAP_PROP_FOURCC)
返回的值的int
表示形式,该值是一个字符串。 然后,我们迭代四次以获取每八位,并将这八位转换为int
。 最后,使用chr()
函数将这些int
值转换为char
。 应该注意的是,我们只能在一行代码中执行此功能,如下所示:
return "".join([chr((fourcc_int >> 8 * i) & 0xFF) for i in range(4)])
CAP_PROP_POS_FRAMES
属性为您提供视频文件的当前帧,CAP_PROP_POS_MSEC
属性为您提供当前帧的时间戳。 我们还可以使用CAP_PROP_FPS
属性获取 fps 的数量。 CAP_PROP_FRAME_COUNT
属性为您提供视频文件的总帧数。
要获取并打印所有属性,请使用以下代码:
# Get and print these values:
print("CV_CAP_PROP_FRAME_WIDTH: '{}'".format(capture.get(cv2.CAP_PROP_FRAME_WIDTH)))
print("CV_CAP_PROP_FRAME_HEIGHT : '{}'".format(capture.get(cv2.CAP_PROP_FRAME_HEIGHT)))
print("CAP_PROP_FPS : '{}'".format(capture.get(cv2.CAP_PROP_FPS)))
print("CAP_PROP_POS_MSEC : '{}'".format(capture.get(cv2.CAP_PROP_POS_MSEC)))
print("CAP_PROP_POS_FRAMES : '{}'".format(capture.get(cv2.CAP_PROP_POS_FRAMES)))
print("CAP_PROP_FOURCC : '{}'".format(decode_fourcc(capture.get(cv2.CAP_PROP_FOURCC))))
print("CAP_PROP_FRAME_COUNT : '{}'".format(capture.get(cv2.CAP_PROP_FRAME_COUNT)))
print("CAP_PROP_MODE : '{}'".format(capture.get(cv2.CAP_PROP_MODE)))
print("CAP_PROP_BRIGHTNESS : '{}'".format(capture.get(cv2.CAP_PROP_BRIGHTNESS)))
print("CAP_PROP_CONTRAST : '{}'".format(capture.get(cv2.CAP_PROP_CONTRAST)))
print("CAP_PROP_SATURATION : '{}'".format(capture.get(cv2.CAP_PROP_SATURATION)))
print("CAP_PROP_HUE : '{}'".format(capture.get(cv2.CAP_PROP_HUE)))
print("CAP_PROP_GAIN : '{}'".format(capture.get(cv2.CAP_PROP_GAIN)))
print("CAP_PROP_EXPOSURE : '{}'".format(capture.get(cv2.CAP_PROP_EXPOSURE)))
print("CAP_PROP_CONVERT_RGB : '{}'".format(capture.get(cv2.CAP_PROP_CONVERT_RGB)))
print("CAP_PROP_RECTIFICATION : '{}'".format(capture.get(cv2.CAP_PROP_RECTIFICATION)))
print("CAP_PROP_ISO_SPEED : '{}'".format(capture.get(cv2.CAP_PROP_ISO_SPEED)))
print("CAP_PROP_BUFFERSIZE : '{}'".format(capture.get(cv2.CAP_PROP_BUFFERSIZE)))
您可以在read_video_file_all_properties.py
文件中查看此脚本的完整代码。
使用属性来反向播放视频
为了了解如何使用上述属性,我们将了解read_video_file_backwards.py
脚本,该脚本使用其中的某些属性加载视频并向后输出,首先显示视频的最后一帧,依此类推。 我们将使用以下属性:
cv2.CAP_PROP_FRAME_COUNT
:此属性提供帧的总数cv2.CAP_PROP_POS_FRAMES
:此属性提供当前帧
第一步是获取最后一帧的索引:
# We get the index of the last frame of the video file
frame_index = capture.get(cv2.CAP_PROP_FRAME_COUNT) - 1
因此,我们将当前帧设置为读取到以下位置:
# We set the current frame position
capture.set(cv2.CAP_PROP_POS_FRAMES, frame_index)
这样,我们可以照常阅读此框架:
# Capture frame-by-frame from the video file
ret, frame = capture.read()
最后,我们递减索引以从视频文件中读取下一帧:
# Decrement the index to read next frame
frame_index = frame_index - 1
完整代码在read_video_file_backwards.py
脚本中提供。 可以轻松修改此脚本,以保存生成的视频向后播放(不仅显示该视频)。 在“问题”部分中提出了此脚本。
总结
在本章中,我们看到使用图像和文件是计算机视觉项目的关键元素。 这种项目中的常见方法是先加载一些图像,执行一些处理,然后输出处理后的图像。 在本章中,我们回顾了该流程。 另外,关于视频流,cv2.VideoCapture
和cv2.VideoWriter
均被覆盖。 我们还研究了用于视频写作的cv2.VideoWriter
类。 编写视频文件时,审查了两个关键方面-视频编解码器(例如 DIVX)和视频文件格式(例如 AVI)。 要使用视频编解码器,OpenCV 提供了四字节代码 FOURCC。 典型的编解码器是 DIVX,XVID,X264 和 MJPG,而典型的视频文件格式是 AVI(*.avi
),MP4(*.mp4
),QuickTime(*.mov
)和 Windows Media Video(*.wmv
)。
我们还回顾了 fps 的概念以及如何在程序中进行计算。 此外,我们研究了如何获取cv2.VideoCapture
对象的所有属性,以及如何使用它们加载视频并向后输出,从而首先显示了视频的最后一帧。 最后,我们看到了如何应对命令行参数。 Python 使用sys.argv
处理命令行参数。 当我们的程序采用复杂的参数或多个文件名时,我们应该使用 Python 的argparse
库。
在下一章中,我们将学习如何使用 OpenCV 库绘制基本的和更高级的形状。 OpenCV 提供绘制线,圆,矩形,椭圆,文本和折线的函数。 与计算机视觉项目有关,这是在图像中绘制基本形状以执行以下操作的常用方法:
- 显示算法的一些中间结果(例如,检测到的对象的边界框)
- 显示算法的最终结果(例如,检测到的对象的类别,例如汽车,猫或狗)
- 显示一些调试信息(例如,执行时间)
因此,下一章将对您的计算机视觉算法有很大帮助。
问题
- 什么是
sys.argv[1]
? - 编写一段代码以添加
int
类型的first_number
参数,并包括要使用parser.add_argument()
添加的帮助优先编号。 - 编写一段代码,将想象中的
img
保存到名称为image.png
的磁盘上。 - 使用
cv2.VideoCapture()
创建capture
对象,以从连接到计算机的第一台摄像机读取。 - 使用
cv2.VideoCapture()
创建对象捕获,以从连接到计算机的第一台摄像机读取并打印CAP_PROP_FRAME_WIDTH
属性。 - 读取图像并将其保存到名称相同但以
_copy.png
结尾的磁盘(例如logo_copy.png
)。 - 创建一个脚本(
read_video_file_backwards_save_video.py
),以加载视频文件并创建另一个向后播放的脚本(首先包含视频的最后一帧,依此类推)。
进一步阅读
以下参考资料将帮助您深入研究argparse
,这是您的计算机视觉项目的关键点:
四、在 OpenCV 中构造基本形状
OpenCV 提供的一种基本功能是绘制基本形状。 OpenCV 提供绘制线,圆,矩形,椭圆等的函数。 在构建计算机视觉项目时,通常需要通过绘制一些形状来修改图像。 例如,如果开发人脸检测算法,则应绘制一个矩形以突出显示在计算图像中检测到的人脸。 此外,如果您开发了面部识别算法,则应绘制一个矩形突出显示检测到的面部,并编写一些文本来显示检测到的面部的身份。 最后,写一些调试信息是一种常见的方法。 例如,您可以显示检测到的脸部数量(以便查看脸部检测算法的表现)或处理时间。 在本章中,您将了解如何使用 OpenCV 库绘制基本的和更高级的形状。
将涵盖以下主题:
- OpenCV 中绘图的理论介绍
- 基本形状 - 直线,矩形和圆形
- 基本形状(2)- 直线和箭头线,椭圆和折线
- 绘制文字
- 带有鼠标事件的动态绘图
- 高级绘图
技术要求
技术要求如下:
- Python 和 OpenCV
- 特定于 Python 的 IDE
- NumPy 和 Matplotlib 包
- Git 客户端
有关如何安装它们的更多详细信息,请参见第 1 章,“设置 OpenCV”。 可以在此处访问 GitHub 存储库,该存储库包含从第一章到最后一章都需要完成本书的所有支持项目文件。
OpenCV 中绘图的理论介绍
OpenCV 提供了许多绘制基本形状的函数。 常见的基本形状包括直线,矩形和圆形。 但是,使用 OpenCV,我们可以绘制更多基本形状。 如引言中简要提到的,这是在图像上绘制基本形状以执行以下操作的常用方法:
- 显示算法的一些中间结果
- 显示算法的最终结果
- 显示一些调试信息
在下一个屏幕截图中,您可以看到一张经过修改的图像,其中包含与导言中提到的两种算法(面部检测和面部识别)有关的一些有用信息。 这样,您可以处理目录中的所有图像,然后,您可以查看算法检测到错误人脸(假正例)甚至丢失人脸(假负例)的位置:
假正例是一个错误,其中结果指示存在条件,而实际上不满足条件(例如,椅子被分类为人脸)。 假负例是一个错误,其中结果指示不存在条件,而实际上应满足条件(例如,未检测到脸部)。
在本章中,我们将看到如何用不同的颜色绘制一些基本形状和文本。 为了对此进行介绍并回顾先前各章中的一些概念,我们将向您展示我们将在本章的大多数示例中使用的两个基本功能。 第一个功能是构建colors
字典,该字典定义了要使用的主要颜色。 在下一个屏幕截图中,您可以看到它是如何工作的:
应该指出的是,这本词典仅用于训练和实践目的。 为了其他目的,您可以使用其他选项。 一种常见的方法是创建一个constant.py
文件来定义颜色。 每种颜色由一个常量定义:
"""
Common colors triplets (BGR space) to use in OpenCV
"""
BLUE = (255, 0, 0)
GREEN = (0, 255, 0)
RED = (0, 0, 255)
YELLOW = (0, 255, 255)
MAGENTA = (255, 0, 255)
CYAN = (255, 255, 0)
DARK_GRAY = (50, 50, 50)
...
以下代码将使您能够使用这些常量:
import constant
# Getting red color:
print("red: '{}'".format(constant.RED))
常量通常以大写字母(例如BLUE
)指定,单词之间带有下划线(例如DARK_GRAY
)。
另外,当我们要使用 Matplotlib 绘制图形时,我们创建了带有两个参数的show_with_matplotlib()
函数。 第一个是我们要显示的图像,第二个是要绘制的图形的标题。 因此,此函数的第一步是将BGR
图像转换为RGB
,因为您必须使用 Matplotlib 显示彩色图像。 此函数的第二个也是最后一个步骤是使用 Matplotlib 功能显示图像。 为了将这些片段放在一起,已对testing_colors.py
脚本进行了编码。 在此脚本中,我们绘制了几行,每行以字典的颜色显示。
创建字典的代码如下所示:
# Dictionary containing some colors
colors = {'blue': (255, 0, 0), 'green': (0, 255, 0), 'red': (0, 0, 255), 'yellow': (0, 255, 255), 'magenta': (255, 0, 255), 'cyan': (255, 255, 0), 'white': (255, 255, 255), 'black': (0, 0, 0), 'gray': (125, 125, 125), 'rand': np.random.randint(0, high=256, size=(3,)).tolist(), 'dark_gray': (50, 50, 50), 'light_gray': (220, 220, 220)}
您会看到此词典中包含一些预定义的颜色-blue
,green
,red
,yellow
,magenta
,cyan
,white
,black
,gray
, 随机数gray
,dark_gray
和light_gray
。 如果要使用特定的颜色(例如magenta
),则应执行以下操作:
colors['magenta']
或者,您可以使用(255, 0, 255)
获得magenta
颜色。 但是,使用此字典比写数字三元组更容易,因为您不需要记住 RGB 颜色空间的相加属性(将blue
-(255,0,0)
和red
相加-(0,0,255)
会得出magenta
— (255, 0, 255)
)。 请记住,您可以使用constant.py
执行此功能。
如果您不知道这些数字是什么或代表什么,则应阅读第 2 章,“OpenCV 中的图像基础知识”,在此介绍了这些概念。
为了了解如何使用这两个函数,我们在本章的大多数示例中都使用了这些函数(colors
函数和show_with_matplotlib()
函数),我们创建了testing_colors.py
脚本。 如果执行它,您将看到下一个屏幕截图:
在此示例中,我们创建了大小为500x500
的图像,具有3
通道(我们需要彩色图像)和uint8
类型(8 位无符号整数)。 我们用黑色背景创建了它:
# We create the canvas to draw: 400 x 400 pixels, 3 channels, uint8 (8-bit unsigned integers)
# We set background to black using np.zeros()
image = np.zeros((500, 500, 3), dtype="uint8")
在这种情况下,我们想将背景设置为浅灰色,而不是黑色。 如果要更改背景,可以执行以下操作:
# If you want another background color, you can do the following:
image[:] = colors['light_gray']
接下来,我们添加了绘制一些线的功能,每条线都使用字典的颜色。 应当注意,在下一节中,我们将看到如何创建一些基本形状,因此,如果您不了解创建线条的代码,请不要担心:
# We draw all the colors to test the dictionary
# We draw some lines, each one in a color. To get the color, use 'colors[key]'
separation = 40
for key in colors:
cv2.line(image, (0, separation), (500, separation), colors[key], 10)
separation += 40
最后,我们使用创建的show_with_matplotlib()
函数绘制图像:
# Show image:
show_with_matplotlib(image, 'Dictionary with some predefined colors')
show_with_matplotlib()
的两个参数是要绘制的图像和要显示的标题。 因此,现在我们准备开始使用 OpenCV 和 Python 创建一些基本形状。
绘制形状
在本节中,我们将看到如何使用 OpenCV 功能绘制形状。 首先,我们将研究如何绘制基本形状,然后将重点放在更高级的形状上。
基本形状 – 直线,矩形和圆形
在下一个示例中,我们将看到如何在 OpenCV 中绘制基本形状。 这些基本形状包括线条,矩形和圆形,它们是最常见且最简单的绘制形状。 第一步是创建将在其中绘制形状的图像。 为此,将创建带有3
通道的400x400
图像(以正确显示 BGR 图像)和uint8
类型(8 位无符号整数):
# We create the canvas to draw: 400 x 400 pixels, 3 channels, uint8 (8-bit unsigned integers)
# We set the background to black using np.zeros()
image = np.zeros((400, 400, 3), dtype="uint8")
我们使用colors
字典将背景设置为浅灰色:
# If you want another background color, you can do the following:
image[:] = colors['light_gray']
下一个屏幕截图显示了此画布(或图像):
现在,我们准备绘制基本形状。 应该注意的是,OpenCV 提供的大多数绘图函数都有共同的参数。 为了简单起见,在此简要介绍这些参数:
img
:是要绘制形状的图像。color
:它是用来绘制形状的颜色(BGR 三元组)。thickness
:如果该值为正,则是形状轮廓的厚度。 否则,将绘制填充形状。lineType
:这是形状边界的类型。 OpenCV 提供三种类型的线:cv2.LINE_4
:这表示四线连接cv2.LINE_8
:这表示八连接线cv2.LINE_AA
:这表示抗锯齿线
shift
:这表示与定义形状的某些点的坐标有关的小数位数。
结合上述参数,lineType
的cv2.LINE_AA
选项可产生质量更好的绘图(例如,在绘制文本时),但绘制速度较慢。 因此,应考虑这一因素。 八连接线和四连接线(均为非抗锯齿线)均使用 Bresenham 算法绘制。 对于抗锯齿线类型,使用高斯滤波算法。 另外,shift
参数是必需的,因为许多绘图函数无法处理亚像素精度。 为了简单起见,我们将使用整数坐标。 因此,该值将设置为0
(shift = 0
)。 但是,为了让您有一个完整的了解,还将提供一个有关如何使用shift
参数的示例。
请记住,对于本节中包含的所有示例,已经创建了一个画布来绘制所有形状。 此画布是400 x 400
像素图像,背景为浅灰色。 请参阅前面的屏幕快照,其中显示了此画布。
绘制直线
我们将要看到的第一个函数是cv2.line()
。 签名如下:
img = line(img, pt1, pt2, color, thickness=1, lineType=8, shift=0)
此函数在img
图像上画一条连接pt1
和pt2
的线:
cv2.line(image, (0, 0), (400, 400), colors['green'], 3)
cv2.line(image, (0, 400), (400, 0), colors['blue'], 10)
cv2.line(image, (200, 0), (200, 400), colors['red'], 3)
cv2.line(image, (0, 200), (400, 200), colors['yellow'], 10)
对这些行进行编码后,我们调用show_with_matplotlib(image, 'cv2.line()')
函数。 结果显示在下一个屏幕截图中:
绘制矩形
cv2.rectangle()
函数的签名如下:
img = rectangle(img, pt1, pt2, color, thickness=1, lineType=8, shift=0)
给定两个相对的角pt1
和pt2
,此函数绘制一个矩形:
cv2.rectangle(image, (10, 50), (60, 300), colors['green'], 3)
cv2.rectangle(image, (80, 50), (130, 300), colors['blue'], -1)
cv2.rectangle(image, (150, 50), (350, 100), colors['red'], -1)
cv2.rectangle(image, (150, 150), (350, 300), colors['cyan'], 10)
绘制这些矩形后,我们调用show_with_matplotlib(image, 'cv2.rectangle()')
函数。 结果显示在下一个屏幕截图中:
请记住,thickness
参数的负值(例如-1
)表示将绘制填充的形状。
绘制圆形
cv2.circle()
函数的签名如下:
img = circle(img, center, radius, color, thickness=1, lineType=8, shift=0)
此函数绘制一个以center
位置为中心的radius
半径的圆。 以下代码定义了一些圈子:
cv2.circle(image, (50, 50), 20, colors['green'], 3)
cv2.circle(image, (100, 100), 30, colors['blue'], -1)
cv2.circle(image, (200, 200), 40, colors['magenta'], 10)
cv2.circle(image, (300, 300), 40, colors['cyan'], -1)
绘制这些圆之后,我们调用show_with_matplotlib(image, 'cv2.circle()')
函数。 结果显示在下一个屏幕截图中:
本节示例的完整代码可以在basic_drawing.py
中看到。
了解高级形状
在本节中,我们将看到如何绘制剪切线,箭头线,椭圆和折线。 这些形状的绘制不像我们在上一节中看到的那样简单,但是它们很容易理解。 第一步是创建将在其中绘制形状的图像。 为此,使用3
通道(以正确显示 BGR 图像)和uint8
类型的300 x 300
图像(8 位无符号整数)将被创建:
# We create the canvas to draw: 300 x 300 pixels, 3 channels, uint8 (8-bit unsigned integers)
# We set the background to black using np.zeros()
image = np.zeros((300, 300, 3), dtype="uint8")
我们使用colors
字典将背景设置为浅灰色:
# If you want another background color, you can do the following:
image[:] = colors['light_gray']
此时,我们可以开始绘制新形状了。
绘制剪切线
cv2.clipLine()
函数的签名如下:
retval, pt1, pt2 = clipLine(imgRect, pt1, pt2)
cv2.clipLine()
函数返回矩形内的段(由pt1
和pt2
输出点定义)(函数根据定义的矩形剪切片段)。 从这个意义上讲,如果两个原始pt1
和pt2
点都在矩形外部,则retval
为False
。 否则(两个pt1
或pt2
点中的一些在矩形内)此函数返回True
。 在下一段代码中可以更清楚地看到这一点:
cv2.line(image, (0, 0), (300, 300), colors['green'], 3)
cv2.rectangle(image, (0, 0), (100, 100), colors['blue'], 3)
ret, p1, p2 = cv2.clipLine((0, 0, 100, 100), (0, 0), (300, 300))
if ret:
cv2.line(image, p1, p2, colors['yellow'], 3)
在下一个屏幕截图中,执行以下代码后,您可以看到结果图:
如您所见,由p1
和p2
点定义的线段显示为黄色,根据矩形剪切原始线段。 在这种情况下,ret
为True
,因为至少有一个点在矩形内,这就是绘制由pt1
和pt2
定义的黄色部分的原因。
绘制箭头
该函数的签名如下:
cv.arrowedLine(img, pt1, pt2, color, thickness=1, lineType=8, shift=0, tipLength=0.1)
此函数允许您创建一个箭头,该箭头从pt1
定义的第一个点指向pt2
定义的第二个点。 箭头尖端的长度可以由tipLength
参数控制,该参数相对于段长度(pt1
和pt2
之间的距离)定义:
cv2.arrowedLine(image, (50, 50), (200, 50), colors['red'], 3, 8, 0, 0.1)
cv2.arrowedLine(image, (50, 120), (200, 120), colors['green'], 3, cv2.LINE_AA, 0, 0.3)
cv2.arrowedLine(image, (50, 200), (200, 200), colors['blue'], 3, 8, 0, 0.3)
如您所见,定义了三个箭头。 请参见下一个屏幕截图,其中绘制了这些箭头。 另外,请查看cv2.LINE_AA
(您也可以写16
)和8
(您也可以写cv2.LINE_8
)之间的区别:
在此示例中,我们结合了两个枚举(例如,cv2.LINE_AA
)(或引起您的注意),或将lineType
参数直接写入值(例如,8
)。 这绝对不是一个好主意,因为它可能会使您感到困惑。 应该在您的所有代码中建立并维护一个标准。
绘制椭圆
该函数的签名如下:
cv2.ellipse(img, center, axes, angle, startAngle, endAngle, color, thickness=1, lineType=8, shift=0)
此函数使您可以创建不同类型的椭圆。 angle
参数(以度为单位)允许您旋转椭圆。 axes
参数控制对应于轴尺寸一半的椭圆尺寸。 如果需要完整的椭圆,请startAngle = 0
和endAngle = 360
。 否则,应将这些参数调整为所需的椭圆弧(以度为单位)。 您还可以看到,通过为轴传递相同的值,您可以绘制一个圆:
cv2.ellipse(image, (80, 80), (60, 40), 0, 0, 360, colors['red'], -1)
cv2.ellipse(image, (80, 200), (80, 40), 0, 0, 360, colors['green'], 3)
cv2.ellipse(image, (80, 200), (10, 40), 0, 0, 360, colors['blue'], 3)
cv2.ellipse(image, (200, 200), (10, 40), 0, 0, 180, colors['yellow'], 3)
cv2.ellipse(image, (200, 100), (10, 40), 0, 0, 270, colors['cyan'], 3)
cv2.ellipse(image, (250, 250), (30, 30), 0, 0, 360, colors['magenta'], 3)
cv2.ellipse(image, (250, 100), (20, 40), 45, 0, 360, colors['gray'], 3)
这些省略号可以在下一个屏幕截图中看到:
绘制多边形
该函数的签名如下:
cv2.polylines(img, pts, isClosed, color, thickness=1, lineType=8, shift=0)
使用此函数可以创建多边形曲线。 此处的关键参数是pts
,应在其中提供定义多边形曲线的数组。 该参数的形状应为(number_vertex, 1, 2)
。 因此,一种常见的方法是通过使用np.array
创建(np.int32
类型的坐标)来定义它,然后对其进行重塑以匹配上述形状。 例如,要创建一个三角形,代码将如下所示:
# These points define a triangle
pts = np.array([[250, 5], [220, 80], [280, 80]], np.int32)
# Reshape to shape (number_vertex, 1, 2)
pts = pts.reshape((-1, 1, 2))
# Print the shapes: this line is not necessary, only for visualization
print("shape of pts '{}'".format(pts.shape))
# this gives: shape of pts '(3, 1, 2)'
另一个重要参数是isClosed
。 如果此参数为True
,则多边形将被绘制为封闭状态。 否则,将不会绘制第一个顶点与最后一个顶点之间的线段,从而形成一个开放的多边形。 为了完整说明,为了绘制一个闭合的三角形,下面给出代码:
# These points define a triangle
pts = np.array([[250, 5], [220, 80], [280, 80]], np.int32)
# Reshape to shape (number_vertex, 1, 2)
pts = pts.reshape((-1, 1, 2))
# Print the shapes: this line is not necessary, only for visualization
print("shape of pts '{}'".format(pts.shape))
# Draw this poligon with True option
cv2.polylines(image, [pts], True, colors['green'], 3)
同样,我们对五边形和矩形进行了编码,可以在下一个屏幕截图中看到它们:
要查看本节的完整代码,您可以查看basic_drawing_2.py
脚本。
绘图函数中的shift
参数
某些以前的函数(带有shift
参数的函数)可以与像素坐标结合使用,达到亚像素精度。 为了解决这个问题,您应该将坐标作为定点数字传递,并以整数编码。
定点数表示为整数(在小数点左边)和小数部分(在小数点右边)都保留了特定(固定)位数(位)。
因此,shift
参数允许您指定小数位数(在小数点右边)。 最后,实点坐标计算如下:
例如,这段代码绘制了两个半径为300
的圆。 其中之一使用shift = 2
值提供子像素精度。 在这种情况下,应将原点和半径乘以4
(2^(shift = 2)
)的倍数:
shift = 2
factor = 2 ** shift
print("factor: '{}'".format(factor))
cv2.circle(image, (int(round(299.99 * factor)), int(round(299.99 * factor))), 300 * factor, colors['red'], 1, shift=shift)
cv2.circle(image, (299, 299), 300, colors['green'], 1)
如果shift = 3
,则因子的值为8
(2^(shift = 3)
),依此类推。 乘以2
的幂与将对应于整数二进制表示的位左移一相同。 这样您可以绘制浮动坐标。 总结一下,我们还可以为cv2.circle()
创建一个包装函数,该函数可以使用shift
参数属性来处理浮点坐标draw_float_circle()
。 接下来显示此示例的关键代码。 完整代码在shift_parameter.py
脚本中定义:
def draw_float_circle(img, center, radius, color, thickness=1, lineType=8, shift=4):
"""Wrapper function to draw float-coordinate circles
"""
factor = 2 ** shift
center = (int(round(center[0] * factor)), int(round(center[1] * factor)))
radius = int(round(radius * factor))
cv2.circle(img, center, radius, color, thickness, lineType, shift)
draw_float_circle(image, (299, 299), 300, colors['red'], 1, 8, 0)
draw_float_circle(image, (299.9, 299.9), 300, colors['green'], 1, 8, 1)
draw_float_circle(image, (299.99, 299.99), 300, colors['blue'], 1, 8, 2)
draw_float_circle(image, (299.999, 299.999), 300, colors['yellow'], 1, 8, 3)
绘图函数中的lineType
参数
另一个常用参数是lineType
,它可以采用三个不同的值。 我们之前曾评论过这三种类型之间的区别。 为了更清楚地看到它,请看下一个屏幕截图,其中我们绘制了三条具有相同粗细和倾斜度的线:yellow = cv2.LINE_4
,red = cv2.LINE_AA
和green = cv2.LINE_8
。 要查看此示例的完整代码,可以检查basic_line_types.py
脚本:
在上一个屏幕截图中,您可以清楚地看到使用三种不同的线型绘制一条线的区别。
绘制文字
OpenCV 还可以用于在图像中呈现文本。 在本节中,我们将看到如何使用cv2.putText()
函数绘制文本。 此外,我们将看到您可以使用的所有可用字体。 最后,我们将看到一些与文本绘制有关的 OpenCV 函数。
绘制文字
cv2.putText()
函数具有以下签名:
img = cv.putText( img, text, org, fontFace, fontScale, color, thickness=1, lineType= 8, bottomLeftOrigin=False)
此函数使用由fontFace
和fontScale
因素提供的字体类型,从org
坐标(如果是bottomLeftOrigin = False
则为左上角,否则为左下角)开始绘制所提供的文本字符串。 结合此示例,您可以看到最后提供的参数lineType
带有 OpenCV 中可用的三个不同值(cv2.LINE_4
,cv2.LINE_8
和cv2.LINE_AA
)。 这样,在绘制这些类型时,您可以更好地看到差异。 请记住,cv2.LINE_AA
的质量更好(抗锯齿线型),但是绘制速度比其他两种类型慢。 接下来给出绘制一些文本的关键代码。 该示例的完整代码可以在text_drawing.py
脚本中看到:
# We draw some text on the image:
cv2.putText(image, 'Mastering OpenCV4 with Python', (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.9, colors['red'], 2, cv2.LINE_4)
cv2.putText(image, 'Mastering OpenCV4 with Python', (10, 70), cv2.FONT_HERSHEY_SIMPLEX, 0.9, colors['red'], 2, cv2.LINE_8)
cv2.putText(image, 'Mastering OpenCV4 with Python', (10, 110), cv2.FONT_HERSHEY_SIMPLEX, 0.9, colors['red'], 2, cv2.LINE_AA)
# Show image:
show_with_matplotlib(image, 'cv2.putText()')
在下一个屏幕截图中,您可以看到结果:
在此示例中,背景色设置为白色。 要执行此函数,您可以执行以下操作:
image.fill(255)
使用所有 OpenCV 文本字体
OpenCV 中所有可用的字体如下:
FONT_HERSHEY_SIMPLEX = 0
FONT_HERSHEY_PLAIN = 1
FONT_HERSHEY_DUPLEX = 2
FONT_HERSHEY_COMPLEX = 3
FONT_HERSHEY_TRIPLEX = 4
FONT_HERSHEY_COMPLEX_SMALL = 5
FONT_HERSHEY_SCRIPT_SIMPLEX = 6
FONT_HERSHEY_SCRIPT_COMPLEX = 7
与此相关,我们对text_drawing_fonts.py
脚本进行了编码,该脚本绘制了所有可用字体。 由于所有这些字体都在(0-7)
范围内,因此我们可以迭代并调用cv2.putText()
函数,从而更改color
,fontFace
和org
参数。 我们还绘制了这些字体的小写和大写版本。 执行此函数的关键代码如下:
position = (10, 30)
for i in range(0, 8):
cv2.putText(image, fonts[i], position, i, 1.1, colors[index_colors[i]], 2, cv2.LINE_4)
position = (position[0], position[1] + 40)
cv2.putText(image, fonts[i].lower(), position, i, 1.1, colors[index_colors[i]], 2, cv2.LINE_4)
position = (position[0], position[1] + 40)
生成的屏幕截图如下所示:
在上一个屏幕截图中,您可以看到 OpenCV 中所有可用的字体(小写和大写版本)。 因此,您可以将此屏幕截图用作参考,以轻松检查要在项目中使用的字体。
与文字相关的更多函数
OpenCV 提供了更多与文本绘制有关的函数。 应当注意,这些函数不是用于绘制文本,而是可以用于对上述cv2.putText()
函数进行补充,它们的评论如下。 我们要看到的第一个函数是cv2.getFontScaleFromHeight()
。 该函数的签名如下:
retval = cv2.getFontScaleFromHeight(fontFace, pixelHeight, thickness=1)
此函数返回字体比例(fontScale
),这是在cv2.putText()
函数中使用的参数,以达到所提供的高度(以像素为单位),并且同时考虑了字体类型(fontFace
)和thickness
。
第二个函数是cv2.getTextSize()
:
retval, baseLine = cv2.getTextSize(text, fontFace, fontScale, thickness)
此函数可用于基于以下参数获取文字大小(宽度和高度):text
,字体类型fontFace
,scale
和thickness
。 此函数返回size
和baseLine
,它们对应于基线相对于文本底部的y
坐标。 下一段代码向您展示了查看此函数的关键方面。 完整代码可在text_drawing_bounding_box.py
脚本中找到:
# assign parameters to use in the drawing functions
font = cv2.FONT_HERSHEY_SIMPLEX
font_scale = 2.5
thickness = 5
text = 'abcdefghijklmnopqrstuvwxyz'
circle_radius = 10
# We get the size of the text
ret, baseline = cv2.getTextSize(text, font, font_scale, thickness)
# We get the text width and text height from ret
text_width, text_height = ret
# We center the text in the image
text_x = int(round((image.shape[1] - text_width) / 2))
text_y = int(round((image.shape[0] + text_height) / 2))
# Draw this point for reference:
cv2.circle(image, (text_x, text_y), circle_radius, colors['green'], -1)
# Draw the rectangle (bounding box of the text)
cv2.rectangle(image, (text_x, text_y + baseline), (text_x + text_width - thickness, text_y - text_height),
colors['blue'], thickness)
# Draw the circles defining the rectangle
cv2.circle(image, (text_x, text_y + baseline), circle_radius, colors['red'], -1)
cv2.circle(image, (text_x + text_width - thickness, text_y - text_height), circle_radius, colors['cyan'], -1)
# Draw the baseline line
cv2.line(image, (text_x, text_y + int(round(thickness/2))), (text_x + text_width - thickness, text_y +
int(round(thickness/2))), colors['yellow'], thickness)
# Write the text centered in the image
cv2.putText(image, text, (text_x, text_y), font, font_scale, colors['magenta'], thickness)
下一个屏幕截图给出了此示例的输出:
注意如何绘制三个小点(red
,cyan
和green
),以及如何显示黄色基线。
使用鼠标事件的动态绘图
在本节中,您将学习如何使用鼠标事件执行动态绘图。 我们将以复杂度递增的顺序来查看一些示例。
绘制动态形状
下一个示例向您介绍如何使用 OpenCV 处理鼠标事件。 cv2.setMouseCallback()
函数执行此功能。 此方法的签名如下:
cv2.setMouseCallback(windowName, onMouse, param=None)
此函数为名为windowName
的窗口建立鼠标处理器。 onMouse
函数是回调函数,当执行鼠标事件时(例如,双击,按下鼠标左键,按下鼠标左键等),将调用该函数。 可选的param
参数用于将其他信息传递给callback
函数。
因此,第一步是创建callback
函数:
# This is the mouse callback function:
def draw_circle(event, x, y, flags, param):
if event == cv2.EVENT_LBUTTONDBLCLK:
print("event: EVENT_LBUTTONDBLCLK")
cv2.circle(image, (x, y), 10, colors['magenta'], -1)
if event == cv2.EVENT_MOUSEMOVE:
print("event: EVENT_MOUSEMOVE")
if event == cv2.EVENT_LBUTTONUP:
print("event: EVENT_LBUTTONUP")
if event == cv2.EVENT_LBUTTONDOWN:
print("event: EVENT_LBUTTONDOWN")
draw_circle()
函数接收特定事件以及每个鼠标事件的坐标(x, y)
。 在这种情况下,当执行左键双击(cv2.EVENT_LBUTTONDBLCLK
)时,我们在事件的相应(x, y)
坐标中绘制一个圆。
此外,我们还打印了一些消息以查看其他产生的事件,但是我们不使用它们执行任何其他操作。
下一步是创建一个命名窗口。 在这种情况下,我们将其命名为Image mouse
。 此命名窗口是鼠标回调函数将与之关联的位置:
# We create a named window where the mouse callback will be established
cv2.namedWindow('Image mouse')
最后,我们将鼠标回调函数设置(或激活)为之前创建的函数:
# We set the mouse callback function to 'draw_circle'
cv2.setMouseCallback('Image mouse', draw_circle)
总之,当执行左双击时,将以执行的双击的(x, y)
位置为中心绘制一个填充的洋红色圆圈。 该示例的完整代码可以在mouse_drawing.py
脚本中看到。
绘制文字和形状
在此示例中,我们将鼠标事件和图形文本结合在一起。 从这种意义上讲,将渲染一些文本以显示如何使用鼠标事件来执行特定操作。 为了更好地理解此示例,在下一个屏幕截图中,您可以看到渲染的文本:
您可以执行以下操作:
- 使用鼠标左键双击添加一个圆
- 只需单击鼠标左键即可删除最后添加的圆圈
- 双击右键删除所有圆圈
为了执行此功能,我们创建了一个名为circles
的列表,其中维护着用户选择的当前圈子。 此外,我们还使用渲染的文本创建了一个备份图像。 产生鼠标事件时,我们从circles
列表中添加或删除圆圈。 然后,在绘制时,我们仅绘制列表中的当前圆。 因此,例如,当用户执行简单的右键单击时,最后一个添加的圆圈将从列表中删除。 完整代码在mouse_drawing_circles_and_text.py
脚本中提供。
Matplotlib 事件处理
您可以在前面的示例中看到我们没有使用 Matplotlib 来显示图像。 这是因为 Matplotlib 也可以处理事件处理和选择。 因此,您可以使用 Matplotlib 功能捕获鼠标事件。 我们可以使用 Matplotlib 连接更多事件。 例如,关于鼠标,我们可以连接以下事件-button_press_event
,button_release_event
,motion_notify_event
和scroll_event
。
我们将展示一个简单的示例,以便在与button_press_event
事件相关的单击鼠标时渲染一个圆形:
# 'button_press_event' is a MouseEvent where a mouse botton is click (pressed)
# When this event happens the function 'click_mouse_event' is called:
figure.canvas.mpl_connect('button_press_event', click_mouse_event)
我们还必须为button_press_event
事件定义事件监听器:
# We define the event listener for the 'button_press_event':
def click_mouse_event(event):
# (event.xdata, event.ydata) contains the float coordinates of the mouse click event:
cv2.circle(image, (int(round(event.xdata)), int(round(event.ydata))), 30, colors['blue'], cv2.FILLED)
# Call 'update_image()' method to update the Figure:
update_img_with_matplotlib()
因此,执行鼠标单击时,将显示blue
圆圈。 应当指出,我们已经对update_img_with_matplotlib()
函数进行了编码。 在前面的示例中,我们使用了show_with_matplotlib()
。 show_with_matplotlib()
函数用于使用 Matplotlib 显示图像,而update_img_with_matplotlib()
用于更新现有图形。 该示例的完整代码可以在matplotlib_mouse_events.py
脚本中看到。
高级绘图
在本节中,我们将看到如何结合上述某些函数以在 OpenCV 中绘制基本形状(例如,线,圆,矩形和文本等)以呈现更高级的绘图。 为了将所有这些部分放在一起,我们构建了一个模拟时钟来向您显示当前时间(小时,分钟和秒)。 为此,编写了两个脚本:
analog_clock_values.py
analog_clock_opencv.py
analog_clock_opencv.py
脚本使用cv.line()
,cv.circle()
,cv.rectangle()
和cv2.putText()
绘制模拟时钟。 在此脚本中,我们首先绘制静态图形。 从这个意义上讲,您可以看到有两个数组包含固定的坐标:
hours_orig = np.array(
[(620, 320), (580, 470), (470, 580), (320, 620), (170, 580), (60, 470), (20, 320), (60, 170), (169, 61), (319, 20),
(469, 60), (579, 169)])
hours_dest = np.array(
[(600, 320), (563, 460), (460, 562), (320, 600), (180, 563), (78, 460), (40, 320), (77, 180), (179, 78), (319, 40),
(459, 77), (562, 179)])
这些数组是绘制小时标记所必需的,因为它们定义了时钟每一小时的线条的起点和终点。 因此,这些标记绘制如下:
for i in range(0, 12):
cv2.line(image, array_to_tuple(hours_orig[i]), array_to_tuple(hours_dest[i]), colors['black'], 3)
此外,绘制了一个大圆圈,对应于模拟时钟的形状:
cv2.circle(image, (320, 320), 310, colors['dark_gray'], 8)
最后,我们绘制包含Mastering OpenCV 4 with Python
文本的矩形,该文本将在时钟内渲染:
cv2.rectangle(image, (150, 175), (490, 270), colors['dark_gray'], -1)
cv2.putText(image, "Mastering OpenCV 4", (150, 200), 1, 2, colors['light_gray'], 1, cv2.LINE_AA)
cv2.putText(image, "with Python", (210, 250), 1, 2, colors['light_gray'], 1, cv2.LINE_AA)
在图像中绘制出此静态信息后,我们将其复制到image_original
图像中:
image_original = image.copy()
要绘制动态信息,执行几个步骤:
- 获取当前时间的小时,分钟和秒:
# Get current date:
date_time_now = datetime.datetime.now()
# Get current time from the date:
time_now = date_time_now.time()
# Get current hour-minute-second from the time:
hour = math.fmod(time_now.hour, 12)
minute = time_now.minute
second = time_now.second
- 将这些值(小时,分钟和秒)转换为角度:
# Get the hour, minute and second angles:
second_angle = math.fmod(second * 6 + 270, 360)
minute_angle = math.fmod(minute * 6 + 270, 360)
hour_angle = math.fmod((hour*30) + (minute/2) + 270, 360)
- 绘制与时针,分针和秒针相对应的线:
# Draw the lines corresponding to the hour, minute and second needles:
second_x = round(320 + 310 * math.cos(second_angle * 3.14 / 180))
second_y = round(320 + 310 * math.sin(second_angle * 3.14 / 180))
cv2.line(image, (320, 320), (second_x, second_y), colors['blue'], 2)
minute_x = round(320 + 260 * math.cos(minute_angle * 3.14 / 180))
minute_y = round(320 + 260 * math.sin(minute_angle * 3.14 / 180))
cv2.line(image, (320, 320), (minute_x, minute_y), colors['blue'], 8)
hour_x = round(320 + 220 * math.cos(hour_angle * 3.14 / 180))
hour_y = round(320 + 220 * math.sin(hour_angle * 3.14 / 180))
cv2.line(image, (320, 320), (hour_x, hour_y), colors['blue'], 10)
- 最后,绘制一个小圆圈,对应于三个针的连接点:
cv2.circle(image, (320, 320), 10, colors['dark_gray'], -1)
在下一个屏幕截图中,您可以看到模拟时钟的外观:
script analog_clock_values.py
脚本计算hours_orig
和hours_dest
数组的固定坐标。 要计算小时标记的(x, y)
坐标,我们使用圆的参数方程式,如下面的屏幕截图所示:
我们已按照上一个屏幕快照中的公式计算了每30
度并从0
度开始的 12 个点P(x, y)
的坐标(0
,30
,60
,90
,120
,150
,180
,210
,240
,270
,300
和330
)具有两个不同的半径。 这样,我们可以为定义小时标记的线定义坐标。 计算这些坐标的代码如下:
radius = 300
center = (320, 320)
for x in (0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330):
x_coordinate = center[0] + radius * math.cos(x * 3.14/180)
y_coordinate = center[1] + radius * math.sin(x * 3.14/180)
print("x: {} y: {}".format(round(x_coordinate), round(y_coordinate)))
for x in (0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330):
x_coordinate = center[0] + (radius - 20) * math.cos(x * 3.14/180)
y_coordinate = center[1] + (radius - 20) * math.sin(x * 3.14/180)
print("x: {} y: {}".format(round(x_coordinate), round(y_coordinate)))
该脚本的完整代码可以在analog_clock_values.py
中看到。 应该注意的是,我们可能已经在其他脚本中包含了用于计算这些坐标的代码,但这对您来说是一个很好的练习。
总结
在本章中,我们回顾了 OpenCV 提供的与图形形状和文本有关的功能。 关于形状,我们已经看到了如何绘制非常基本的形状(直线,矩形和圆形),以及更高级的形状(直线,箭头,椭圆和多边形)。 在文本方面,我们已经看到了如何绘制文本以及如何在 OpenCV 库中呈现所有可用字体。 此外,我们还介绍了如何捕获鼠标事件并使用它们执行特定的操作(例如,绘制与执行的鼠标事件的(x, y)
坐标关联的点)。 最后,我们绘制了一个模拟时钟,试图总结本章的所有先前概念。
在下一章中,我们将看到有关图像处理技术的主要概念。 我们还将解决如何执行基本的图像转换(例如,平移,旋转,调整大小,翻转和裁剪)。 另一个关键方面是如何对图像执行基本算术,例如按位运算(AND,OR,XOR 和 NOT)。 最后,我们将介绍主要的颜色空间和颜色图。
问题
- 您应该正确配置哪个参数以绘制填充形状(例如,圆形或矩形)?
- 您应该正确配置哪个参数以绘制抗锯齿线型?
- 创建一条从
(0, 0)
开始到(512, 512)
结束的对角线。 - 使用所需的参数呈现文本
Hello OpenCV
。 - 使用 12 个点绘制一个多边形(圆形)。
- 双击时使用鼠标事件和 Matplotlib 事件绘制一个矩形。
- 尝试使用
lenna.png
图像作为背景绘制这个非常简单的模因生成器:
进一步阅读
以下参考资料将帮助您更深入地了解 Matplotlib:
五、图像处理技术
图像处理技术是计算机视觉项目的核心。 它们可以看作是有用的关键工具,可用于完成各种任务。 换句话说,图像处理技术就像构造块,在处理图像时应牢记。 因此,如果要使用计算机视觉项目,则需要对图像处理有基本的了解。
在本章中,您将学习所需的大多数常见图像处理技术。 这些将由本书的后三章介绍的其他图像处理技术(直方图,阈值技术,轮廓检测和滤波)进行补充。
本章将讨论以下主题:
- 拆分和合并通道
- 图像的几何变换-平移,旋转,缩放,仿射变换,透视变换和裁剪
- 使用图像进行算术运算—按位运算(AND,OR,XOR 和 NOT)和掩码
- 平滑和锐化技术
- 形态操作
- 色彩空间
- 颜色图
技术要求
本章的技术要求如下:
- Python 和 OpenCV
- 特定于 Python 的 IDE
- NumPy 和 Matplotlib 包
- 一个 Git 客户端
有关如何安装这些要求的更多详细信息,请参见第 1 章,“设置 OpenCV”。 《精通 Python OpenCV 4》的 GitHub 存储库,其中包含从第一章到最后一章学习本书所需的所有支持项目文件,可以在以下网址访问。
在 OpenCV 中拆分和合并通道
有时,您必须使用多通道图像上的特定通道。 为此,您必须将多通道图像拆分为几个单通道图像。 此外,一旦处理完成,您可能想从不同的单通道图像创建一个多通道图像。 为了同时分割和合并通道,可以分别使用cv2.split()
和cv2.merge()
函数。 cv2.split()
函数将源多通道图像分割为几个单通道图像。 cv2.merge()
函数将几个单通道图像合并为一个多通道图像。
在下一个示例splitting_and_merging.py
中,您将学习如何使用上述两个函数。 使用cv2.split()
函数,如果要从已加载的 BGR 图像中获取三个通道,则应使用以下代码:
(b, g, r) = cv2.split(image)
使用cv2.merge()
函数,如果要从其三个通道再次构建 BGR 图像,则应使用以下代码:
image_copy = cv2.merge((b, g, r))
您应该记住cv2.split()
是一项耗时的操作,因此仅在绝对必要时才应使用它。 否则,您可以使用 NumPy 函数处理特定的通道。 例如,如果要获取图像的蓝色通道,可以执行以下操作:
b = image[:, :, 0]
另外,您可以消除(设置为0
)多通道图像的某些通道。 生成的图像将具有相同数量的通道,但在相应通道中具有0
值; 例如,如果要消除 BGR 图像的蓝色通道,可以使用以下代码:
image_without_blue = image.copy()
image_without_blue[:, :, 0] = 0
如果执行splitting_and_merging.py
脚本,则会看到以下屏幕截图:
为了理解此屏幕截图,您应该记住 RGB 颜色空间的累加属性。 例如,使用不带 B 的 BGR 子图,您可以看到它大部分是黄色的。 这是因为绿色和红色值产生黄色。 您可以看到的另一个关键特征是与我们设置为0
的特定通道相对应的黑色子图。
图像的几何变换
在本节的第一部分中,将介绍图像的主要几何变换。 我们将看一些缩放,平移,旋转,仿射变换,透视变换和图像裁剪的示例。 执行这些几何变换的两个关键函数是cv2.warpAffine()
和cv2.warpPerspective()
。 cv2.warpAffine()
函数通过使用以下2 x 3
的M
变换矩阵来变换源图像:
cv2.warpPerspective()
函数使用以下3 x 3
转换矩阵对源图像进行转换:
在接下来的小节中,我们将学习最常见的几何变换技术,当我们查看geometric_image_transformations.py
脚本时,将学习更多有关几何变换的技术。
缩放图像
缩放图像时,可以调用特定大小的cv2.resize()
,并且缩放因子(fx
和fy
)将根据提供的大小进行计算,如以下代码所示:
resized_image = cv2.resize(image, (width * 2, height * 2), interpolation=cv2.INTER_LINEAR)
另一方面,您可以同时提供fx
和fy
值。 例如,如果要将图像缩小2
倍,则可以使用以下代码:
dst_image = cv2.resize(image, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_AREA)
如果要放大图像,最好的方法是使用cv2.INTER_CUBIC
插值方法(一种费时的插值方法)或cv2.INTER_LINEAR
。 如果要缩小图像,通常的方法是使用cv2.INTER_LINEAR
。
OpenCV 提供的五种插值方法是cv2.INTER_NEAREST
(最近邻插值),cv2.INTER_LINEAR
(双线性插值),cv2.INTER_AREA
(使用像素面积关系重采样),cv2.INTER_CUBIC
(双曲线插值)和cv2.INTER_LANCZOS4
(正弦形) 插值)。
平移图像
为了转换对象,您需要使用带有浮点值的 NumPy 数组来创建2 x 3
转换矩阵,以提供x
和y
方向(以像素为单位),如以下代码所示:
M = np.float32([[1, 0, x], [0, 1, y]])
这给出了以下M
转换矩阵:
创建此矩阵后,将调用cv2.warpAffine()
函数,如以下代码所示:
dst_image = cv2.warpAffine(image, M, (width, height))
cv2.warpAffine()
函数使用提供的M
矩阵变换源图像。 第三个(width, height)
参数确定输出图像的大小。
请记住,image.shape
返回(width, height)
。
例如,如果要转换x
方向为200
像素,y
方向为30
像素的图像,请使用以下内容:
height, width = image.shape[:2]
M = np.float32([[1, 0, 200], [0, 1, 30]])
dst_image = cv2.warpAffine(image, M, (width, height))
请注意,转换也可以是负数,如以下代码所示:
M = np.float32([[1, 0, -200], [0, 1, -30]])
dst_image = cv2.warpAffine(image, M, (width, height))
旋转图像
为了旋转图像,我们利用cv.getRotationMatrix2D()
函数来构建2 x 3
变换矩阵。 该矩阵以所需角度(以度为单位)旋转图像,其中正值表示逆时针旋转。 旋转center
和scale
因子也都可以调整。 在我们的示例中使用这些元素,将计算以下转换矩阵:
该表达式具有以下值:
下面的示例构建M
变换矩阵,以1
的比例因子(不缩放)相对于图像中心旋转180
度。 然后,将此M
矩阵应用于图像,如下所示:
height, width = image.shape[:2]
M = cv2.getRotationMatrix2D((width / 2.0, height / 2.0), 180, 1)
dst_image = cv2.warpAffine(image, M, (width, height))
图像的仿射变换
在仿射变换中,我们首先使用cv2.getAffineTransform()
函数来构建2 x 3
变换矩阵,该矩阵将从输入图像和变换图像中的相应坐标获得。 最后,将该M
矩阵传递给cv2.warpAffine()
,如下所示:
pts_1 = np.float32([[135, 45], [385, 45], [135, 230]])
pts_2 = np.float32([[135, 45], [385, 45], [150, 230]])
M = cv2.getAffineTransform(pts_1, pts_2)
dst_image = cv2.warpAffine(image_points, M, (width, height))
仿射变换是保留点,直线和平面的变换。 另外,在此变换之后,平行线将保持平行。 但是,仿射变换不能同时保留点之间的距离和角度。
图像的透视变换
为了校正透视图(也称为透视变换),您将需要使用cv2.getPerspectiveTransform()
函数创建变换矩阵,其中构造了3 x 3
矩阵。 此函数需要四对点(源图像和输出图像中的四边形的坐标),并从这些点计算透视变换矩阵。 然后,将M
矩阵传递到cv2.warpPerspective()
,在其中通过应用具有指定大小的指定矩阵来变换源图像,如以下代码所示:
pts_1 = np.float32([[450, 65], [517, 65], [431, 164], [552, 164]])
pts_2 = np.float32([[0, 0], [300, 0], [0, 300], [300, 300]])
M = cv2.getPerspectiveTransform(pts_1, pts_2)
dst_image = cv2.warpPerspective(image, M, (300, 300))
裁剪图像
要裁剪图像,我们将使用 NumPy 切片,如以下代码所示:
dst_image = image[80:200, 230:330]
如前所述,这些几何变换的代码对应于geometric_image_transformations.py
脚本。
过滤图像
在本节中,我们将解决如何通过应用几个过滤器和定制核来模糊和锐化图像。 此外,我们将研究一些常见的核,这些核可用于执行其他图像处理功能。
应用任意核
OpenCV 提供cv2.filter2D()
函数,以便将任意核应用于图像,从而将图像与提供的核进行卷积。 为了了解此函数的工作原理,我们应该先构建核,稍后再使用。 在这种情况下,将使用5 x 5
核,如以下代码所示:
kernel_averaging_5_5 = np.array([[0.04, 0.04, 0.04, 0.04, 0.04], [0.04, 0.04, 0.04, 0.04, 0.04], [0.04, 0.04, 0.04, 0.04, 0.04],[0.04, 0.04, 0.04, 0.04, 0.04], [0.04, 0.04, 0.04, 0.04, 0.04]])
这对应于5 x 5
平均核。 另外,您还可以像这样创建核:
kernel_averaging_5_5 = np.ones((5, 5), np.float32) / 25
然后,通过应用上述函数,将核应用于源图像,如以下代码所示:
smooth_image_f2D = cv2.filter2D(image, -1, kernel_averaging_5_5)
现在我们已经看到了将任意核应用于图像的方法。 在前面的示例中,创建了一个平均核来平滑图像。 还有其他无需创建核即可执行图像平滑的方法(也称为图像模糊)。 相反,可以将其他一些参数提供给相应的 OpenCV 函数。 在smoothing_techniques.py
脚本中,您可以看到此先前示例和下一小节的完整代码。
平滑图像
如前所述,在smoothing_techniques.py
脚本中,您将看到其他常用的滤波技术来执行平滑操作。 平滑技术通常用于减少噪声,此外,这些技术还可应用于减少低分辨率图像中的像素化效果。 这些技术评论如下。
您可以在以下屏幕截图中看到此脚本的输出:
在前面的屏幕截图中,您可以看到在图像处理中应用通用核的效果。
均值过滤
您可以同时使用cv2.blur()
和cv2.boxFilter()
通过将图像与核进行卷积来执行平均,在cv2.boxFilter()
情况下,该核可以不规范化。 它们仅获取核区域下所有像素的平均值,然后用该平均值替换中心元素。 您可以控制核大小和锚定核(默认为(-1,-1)
,这意味着锚定位于核中心)。 当cv2.boxFilter()
的normalize
参数(默认为True
)等于True
时,两个函数执行相同的操作。 这样,两个函数都使用核对图像进行平滑处理,如以下表达式所示:
对于cv2.boxFilter()
函数:
对于cv2.blur()
函数:
换句话说,cv2.blur()
始终使用标准化的框式过滤器,如以下代码所示:
smooth_image_b = cv2.blur(image, (10, 10))
smooth_image_bfi = cv2.boxFilter(image, -1, (10, 10), normalize=True)
在前面的代码中,两行代码是等效的。
高斯过滤
OpenCV 提供cv2.GaussianBlur()
函数,该函数使用高斯核模糊图像。 可以使用以下参数控制该核:ksize
(核大小),sigmaX
(x
-高斯核方向的标准差)和sigmaY
(y
-高斯核的方向)。 为了知道已经应用了哪个核,可以使用cv2.getGaussianKernel()
函数。
例如,在下面的代码行中,cv2.GaussianBlur()
使用大小为(9, 9)
的高斯核模糊图像:
smooth_image_gb = cv2.GaussianBlur(image, (9, 9), 0)
中值过滤
OpenCV 提供cv2.medianBlur()
函数,该函数使用中值核模糊图像,如以下代码所示:
smooth_image_mb = cv2.medianBlur(image, 9)
可以应用此过滤器来减少图像的椒盐噪声。
双边过滤
可以将cv2.bilateralFilter()
函数应用于输入图像,以便应用双边过滤器。 此函数可用于减少噪声,同时保持边缘清晰,如以下代码所示:
smooth_image_bf = cv2.bilateralFilter(image, 5, 10, 10)
应当注意,所有先前的过滤器都倾向于使包括边缘在内的所有图像平滑。
锐化图像
与最后一个函数结合使用,您可以尝试一些选项以锐化图像的边缘。 一种简单的方法是执行所谓的锐化遮罩,其中从原始图像中减去图像的锐化或平滑版本。 在以下示例中,首先应用了高斯平滑过滤器,然后从原始图像中减去了所得图像:
smoothed = cv2.GaussianBlur(img, (9, 9), 10)
unsharped = cv2.addWeighted(img, 1.5, smoothed, -0.5, 0)
另一种选择是使用特定的核锐化边缘,然后应用cv2.filter2D()
函数。 在sharpening_techniques.py
脚本中,有一些定义的核可用于此目的。 以下屏幕快照显示了此脚本的输出:
在前面的屏幕截图中,您可以看到应用不同的锐化核的效果,可以在sharpening_techniques.py
脚本中看到。
图像处理中的常见核
我们已经看到核对生成的图像有很大的影响。 在filter_2D_kernels.py
脚本中,有一些定义的通用核可用于不同目的-边缘检测,平滑,锐化或压花等。 提醒一下,为了应用特定的核,应使用cv2.filter2D()
函数。 以下屏幕快照显示了此脚本的输出:
您可以使用cv2.filter2D()
函数查看应用不同核的效果,该函数可用于应用特定核。
创建卡通化图像
如前所述,可以使用cv2.bilateralFilter()
来减少噪声,同时保留尖锐的边缘。 但是,此过滤器可以在滤波后的图像中同时产生强度平稳(阶梯效应)和假边缘(梯度反转)。 尽管可以在过滤后的图像中考虑到这一点(对处理这些伪像的双边过滤器进行了一些改进),但是创建卡通化图像可能非常酷。 完整的代码可以在cartoonizing.py
中看到,但是在本节中,我们将进行简要描述。
将图像卡通化的过程非常简单,可以在cartonize_image()
函数中执行。 首先,基于图像的边缘构造图像的草图(请参见sketch_image()
函数)。 还有其他边缘检测器可以使用,但是在这种情况下,使用拉普拉斯算子。 在调用cv2.Laplacian()
函数之前,我们通过cv2.medianBlur()
中值过滤器对图像进行平滑处理来降低噪声。 一旦获得边缘,则通过应用cv2.threshold()
对所得图像进行阈值处理。 我们将在下一章中介绍阈值技术,但是在本示例中,此函数从给定的灰度图像中为我们提供了一个与sketch_image()
函数的输出相对应的二进制图像。 您可以使用阈值(在这种情况下固定为70
)进行操作,以查看该值如何控制结果图像中出现的黑色像素(对应于检测到的边缘)数量。 如果此值较小(例如10
),则会出现许多黑色边框像素。 如果该值较大(例如200
),则几乎不会输出黑色边框像素。 为了获得卡通效果,我们将cv2.bilateralFilter()
函数称为具有较大值的函数(例如cv2.bilateralFilter(img, 10, 250, 250)
)。 最后一步是使用草图图像作为遮罩,使用cv2.bitwise_and()
将草图图像和双边过滤器的输出放在一起,以便将这些值设置为输出。 如果需要,输出也可以转换为灰度。 注意cv2.bitwise_and()
函数是按位操作,我们将在下一部分中看到。
为了完整起见,OpenCV 提供了类似的功能,并且也在此脚本中进行了测试。 它可以使用以下过滤器工作:
cv2.pencilSketch()
:此过滤器生成铅笔素描线图(类似于我们的sketch_image()
函数)。cv2.stylization()
:此过滤器可用于产生各种非真实感效果。 在这种情况下,我们应用cv2.stylization()
以获得卡通效果(类似于我们的cartonize_image()
函数)。
以下屏幕快照显示了与cartoonizing.py
脚本对应的输出:
如您所见,cartonize_image()
函数还可以输出调用cv2.cvtColor()
的灰度图像,以将图像从 BGR 转换为灰度。
图像算术
在本节中,我们将学习一些可以在图像上执行的常见算术运算,例如按位运算,加法和减法。 与这些操作有关,要考虑的一个关键点是饱和算法的概念,以下小节对此进行了说明。
饱和算法
饱和算术是一种算术运算,其中通过限制运算可以采用的最大值和最小值将运算限制在固定范围内。 例如,对图像的某些操作(例如,色彩空间转换,插值技术等)可能会产生超出可用范围的值。 为了解决这个问题,使用了饱和算法。
例如,要存储r
(这可能是对 8 位图像(值范围从0
到255
的值)执行特定操作的结果),请应用以下公式:
可以在以下saturation_arithmetic.py
脚本中看到此概念:
x = np.uint8([250])
y = np.uint8([50])
# 250+50 = 300 => 255:
result_opencv = cv2.add(x, y)
print("cv2.add(x:'{}' , y:'{}') = '{}'".format(x, y, result_opencv))
# 250+50 = 300 % 256 = 44
result_numpy = x + y
print("x:'{}' + y:'{}' = '{}'".format(x, y, result_numpy))
在 OpenCV 中,对值进行裁剪以确保它们永远不会超出[0, 255]
范围。 这称为饱和操作。 在 NumPy 中,值被包裹起来。 这也称为取模操作。
图像加减
可以分别通过cv2.add()
和cv2.subtract()
函数执行图像加和减。 这些函数对两个数组的每个元素进行求和/减法。 这些函数还可以用于对数组和标量求和/相减。 例如,如果要向图像的所有像素添加60
,我们首先必须使用以下代码构建要添加到原始图像的图像:
M = np.ones(image.shape, dtype="uint8") * 60
然后,我们使用以下代码执行添加:
added_image = cv2.add(image, M)
另一种可能性是创建标量并将其添加到原始图像。 例如,如果要将110
添加到图像的所有像素,则首先必须使用以下代码构建标量:
scalar = np.ones((1, 3), dtype="float") * 110
然后,我们使用以下代码执行加法:
added_image_2 = cv2.add(image, scalar)
在减法的情况下,过程相同,但是我们调用了cv2.subtract()
函数。 可以在arithmetic.py
中看到此脚本的完整代码。 在以下屏幕截图中可以看到此脚本的输出:
在前面的屏幕截图中,您可以清楚地看到添加和减去预定义值的效果(以两种不同的方式计算,但显示的结果相同)。 当我们添加一个值时,图像将更亮,而当我们减去一个值时,图像将更暗。
图像融合
图像融合也是图像添加,但是对图像赋予不同的权重,给人以透明感。 为此,将使用cv2.addWeighted()
函数。 此函数通常用于从Sobel
运算符获取输出。
Sobel
运算符用于边缘检测,在其中创建强调边缘的图像。 Sobel
运算符使用两个3×3
核,它们与原始图像卷积在一起,以便计算导数的近似值,同时捕获水平和垂直变化,如以下代码所示:
# Gradient x is calculated:
# the depth of the output is set to CV_16S to avoid overflow
# CV_16S = one channel of 2-byte signed integers (16-bit signed integers)
gradient_x = cv2.Sobel(gray_image, cv2.CV_16S, 1, 0, 3)
gradient_y = cv2.Sobel(gray_image, cv2.CV_16S, 0, 1, 3)
因此,在计算出水平和垂直变化之后,可以使用上述函数将它们混合成图像,如下所示:
# Conversion to an unsigned 8-bit type:
abs_gradient_x = cv2.convertScaleAbs(gradient_x)
abs_gradient_y = cv2.convertScaleAbs(gradient_y)
# Combine the two images using the same weight:
sobel_image = cv2.addWeighted(abs_gradient_x, 0.5, abs_gradient_y, 0.5, 0)
可以在arithmetic_sobel.py
脚本中看到。 在以下屏幕截图中可以看到此脚本的输出:
在前面的屏幕截图中,显示了Sobel
运算符的输出,包括水平和垂直更改。
按位运算
可以使用按位运算符在位级别执行某些操作,这些操作可用于操纵值以进行比较和计算。 这些按位运算很简单,并且计算很快。 这意味着它们是处理图像时的有用工具。
按位运算包括AND
,OR
,NOT
和XOR
。
- 按位与:
bitwise_and = cv2.bitwise_and(img_1, img_2)
- 按位或:
bitwise_xor = cv2.bitwise_xor(img_1, img_2)
- 按位异或:
bitwise_xor = cv2.bitwise_xor(img_1, img_2)
- 按位非:
bitwise_not_1 = cv2.bitwise_not(img_1)
为了解释这些操作的工作方式,请在以下屏幕截图中查看bitwise_operations.py
脚本的输出:
为了进一步处理按位运算,您可以查看下面的bitwise_operations_images.py
脚本,其中加载了两个图像并执行了一些按位运算(AND 和 OR)。 应当注意,图像应具有相同的形状:
# Load the original image (250x250):
image = cv2.imread('lenna_250.png')
# Load the binary image (but as a GBR color image - with 3 channels) (250x250):
binary_image = cv2.imread('opencv_binary_logo_250.png')
# Bitwise AND
bitwise_and = cv2.bitwise_and(image, binary_image)
# Bitwise OR
bitwise_or = cv2.bitwise_or(image, binary_image)
在以下屏幕截图中可以看到输出:
在上一个屏幕截图中,您可以在执行按位运算(AND,OR)时看到生成的图像。
形态变换
形态变换是通常在二进制图像上并且基于图像形状执行的操作。 确切的操作由核结构元素确定,该元素决定了操作的性质。 膨胀和侵蚀是形态转换领域中的两个基本运算符。 另外,打开和关闭是两个重要的操作,它们是从上述两个操作(膨胀和腐蚀)派生而来的。 最后,还有其他三个基于这些先前操作之间的差异的操作。
所有这些形态转换将在以下小节中介绍,morphological_operations.py
脚本显示了将这些转换应用于某些测试图像时的输出。 关键点也将被注解。
膨胀操作
对二值图像进行膨胀操作的主要效果是逐渐扩展前景对象的边界区域。 这意味着在这些区域内的孔缩小时,前景对象的区域将变大。 以下代码显示了操作的详细信息:
dilation = cv2.dilate(image, kernel, iterations=1)
侵蚀操作
对二进制图像进行腐蚀操作的主要效果是逐渐侵蚀掉前景对象的边界区域。 这意味着前景对象的区域将变小,并且这些区域内的孔洞将变大。 您可以在以下代码中查看此操作的详细信息:
erosion = cv2.erode(image, kernel, iterations=1)
打开操作
对于两个操作,打开操作都执行侵蚀,然后使用相同的结构元素(或核)进行膨胀。 这样,可以施加腐蚀来消除一小组不希望的像素(例如,盐和胡椒噪声)。
侵蚀将不加选择地影响图像的所有区域。 通过在腐蚀之后执行膨胀操作,我们将减少其中一些影响。 您可以在以下代码中查看此操作的详细信息:
opening = cv2.morphologyEx(image, cv2.MORPH_OPEN, kernel)
关闭操作
与其相反,关闭操作可以从腐蚀和膨胀运算中得出。 在这种情况下,操作会先进行扩张,然后进行腐蚀。 膨胀操作通常用于填充图像中的小孔。 但是,膨胀操作也会使不希望出现的像素的较小组变大。 通过在膨胀后对图像进行腐蚀操作,可以减少这种影响。 您可以在以下代码中查看此操作的详细信息:
closing = cv2.morphologyEx(image, cv2.MORPH_CLOSE, kernel)
形态梯度操作
形态学梯度运算定义为输入图像的膨胀和腐蚀之间的差:
morph_gradient = cv2.morphologyEx(image, cv2.MORPH_GRADIENT, kernel)
高帽操作
高帽操作定义为输入图像和图像打开之间的差异。 您可以在以下代码中查看此操作的详细信息:
top_hat = cv2.morphologyEx(image, cv2.MORPH_TOPHAT, kernel)
黑帽操作
黑帽操作定义为输入图像和输入图像关闭之间的差异。 您可以在以下代码中查看此操作的详细信息:
black_hat = cv2.morphologyEx(image, cv2.MORPH_BLACKHAT, kernel)
结构元素
与结构元素一起,OpenCV 提供了cv2.getStructuringElement()
函数。
此函数输出所需的核(类型为uint8
的 NumPy 数组)。 应该向该函数传递两个参数-核的形状和大小。 OpenCV 提供以下三种形状:
- 矩形核:
cv2.MORPH_RECT
- 椭圆核:
cv2.MORPH_ELLIPSE
- 十字形核:
cv2.MORPH_CROSS
将形态学变换应用于图像
在morphological_operations.py
脚本中,我们使用不同的核大小和形状,形态转换和图像。 我们将在本节中描述此脚本的一些关键点。
首先,build_kernel()
函数根据核类型和大小返回用于形态转换的特定核。 其次,morphological_operations
词典包含所有已实现的形态学运算。 如果我们打印字典,输出将如下所示:
index: '0', key: 'erode', value: '<function erode at 0x0C1F8228>'
index: '1', key: 'dilate', value: '<function dilate at 0x0C1F8390>'
index: '2', key: 'closing', value: '<function closing at 0x0C1F83D8>'
index: '3', key: 'opening', value: '<function opening at 0x0C1F8420>'
index: '4', key: 'gradient', value: '<function morphological_gradient at 0x0C1F8468>'
index: '5', key: 'closing|opening', value: '<function closing_and_opening at 0x0C1F8348>'
index: '6', key: 'opening|closing', value: '<function opening_and_closing at 0x0C1F84B0>'
换句话说,字典的键标识要使用的形态操作,并且值是使用相应键时要调用的函数。 例如,如果要调用erode
操作,则必须执行以下操作:
result = morphological_operations['erode'](image, kernel_type, kernel_size)
在前面的代码image
,kernel_type
和kernel_size
是erode
函数的参数(实际上,它们是字典中所有函数的参数)。
apply_morphological_operation()
函数将字典中定义的所有形态运算应用于图像数组。 最后,调用show_images()
函数,在其中绘制数组中包含的所有图像。 具体的实现细节可以在morphological_operations.py
脚本的源代码中找到,该脚本包含大量注释。
该脚本绘制了四个图形,其中测试了不同的核类型和大小。 例如,在以下屏幕截图中,当使用(3, 3)
的核大小和矩形核(cv2.MORPH_RECT
)时,您可以看到输出:
如您在前面的屏幕快照中所见,在对图像进行预处理时,形态学运算是一种有用的技术,因为您可以消除一些噪声,这些噪声会干扰图像的正确处理。 另外,形态学操作也可以用于处理图像结构中的缺陷。
色彩空间
在本节中,将介绍流行的色彩空间的基础知识。 这些颜色空间是-RGB,CIELab,HSL 和 HSV 以及 YCbCr。
OpenCV 提供了 150 多种颜色空间转换方法来执行用户所需的转换。 在以下示例中,从加载到 RGB(OpenCV 中的 BGR)的图像到其他颜色空间(例如,HSV,HLS 或 YCbCr)执行转换。
显示色彩空间
RGB 颜色空间是加法颜色空间,其中特定颜色由红色,绿色和蓝色值表示。 人类视觉的工作方式相似,因此此色彩空间是显示计算机图形的合适方式。
CIELAB 颜色空间(也称为 CIELab 或简称为 LAB)代表一种特定的颜色,作为三个数值,其中L
表示亮度,a
代表绿色-红色分量,b
代表蓝黄色分量。 在某些图像处理算法中也使用此色彩空间。
色相,饱和度,亮度(HSL)和色相,饱和度,值(HSV)是两个色彩空间,其中只有一个通道 (H)用于描述颜色,使其非常直观地指定颜色。 在这些颜色模型中,当应用图像处理技术时,亮度分量的分离具有一些优势。
YCbCr 是在视频和数字摄影系统中使用的一系列色彩空间,以色度分量(Y)和两个色度分量/色度(Cb 和 Cr)表示颜色。 基于从 YCbCr 图像派生的颜色模型,此颜色空间在图像分割中非常受欢迎。
在color_spaces.py
脚本中,图像被加载到 BGR 颜色空间中并转换为上述颜色空间。 在此脚本中,关键函数是cv2.cvtColor()
,它可以将一种颜色空间的输入图像转换为另一种颜色空间。
在与 RGB 颜色空间之间进行转换的情况下,应明确指定通道的顺序(BGR 或 RGB)。 例如:
image = cv2.imread('color_spaces.png')
该图像被加载到 BGR 颜色空间中。 因此,如果我们要将其转换为 HSV 颜色空间,则必须执行以下操作:
hsv_image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
注意,我们使用了cv2.COLOR_BGR2HSV
而不是cv2.COLOR_RGB2HSV
。
该脚本的完整代码可以在color_space.py
中看到。 在以下屏幕截图中可以看到输出:
如前面的屏幕快照所示,BGR 图像被转换为HSV,HLS,YCrCb 和 Lab 颜色空间。 还显示了每个颜色空间的所有组件(通道)。
不同颜色空间中的皮肤分割
前述色彩空间可用于不同的图像处理任务和技术。 例如,skin_segmentation.py
脚本实现了不同的算法,以在不同的颜色空间(YCrCb,HSV 和 RGB)中执行皮肤分割工作。 该脚本还加载了一些测试图像,以查看这些算法如何工作。
该脚本中的关键函数是cv2.cvtColor()
(我们已经提到过)和cv2.inRange()
,它检查一个数组中包含的元素是否位于其他两个数组的元素之间(下边界数组和上边界数组) )。
因此,我们使用cv2.inRange()
函数分割与皮肤对应的颜色。 如您所见,为这两个数组(上下边界)定义的值在分割算法的表现中起着至关重要的作用。 以此方式,已经进行了广泛的调查以正确设置它们。 在此示例中,这些值是从以下研究论文中获得的:
RGB-H-CbCr Skin Color Model for Human Face Detection by Nusirwan Anwar, Abdul Rahman, K. C. Wei, and John See
Skin segmentation algorithm based on the YCrCb color space by Shruti D Patravali, Jyoti Waykule, and Apurva Katre
Face Segmentation Using Skin-Color Map in Videophone Applications by D. Chai and K.N. Ngan
skin_detectors
词典已构建为将所有皮肤分割算法应用于测试图像。 如果我们打印它,输出将如下所示:
index: '0', key: 'ycrcb', value: '<function skin_detector_ycrcb at 0x07B8C030>'
index: '1', key: 'hsv', value: '<function skin_detector_hsv at 0x07B8C0C0>'
index: '2', key: 'hsv_2', value: '<function skin_detector_hsv_2 at 0x07B8C108>'
index: '3', key: 'bgr', value: '<function skin_detector_bgr at 0x07B8C1E0>'
您可以看到定义了四个皮肤检测器。 为了调用皮肤分割检测器(例如skin_detector_ycrcb
),必须执行以下操作:
detected_skin = skin_detectors['ycrcb'](image)
脚本的输出可以在以下屏幕截图中看到:
您可以通过使用多个测试图像来查看应用不同的皮肤分割算法的效果,以了解这些算法在不同条件下的工作方式。
颜色图
在许多计算机视觉应用中,算法的输出是灰度图像。 但是,人眼不善于观察灰度图像的变化。 当意识到彩色图像的变化时,它们更加敏感,因此一种常见的方法是将灰度图像转换(重新着色)为伪彩色等效图像。
OpenCV 中的颜色图
为了执行此转换,OpenCV 具有多个颜色图以增强可视化。 cv2.applyColorMap()
函数在给定图像上应用颜色图。 color_map_example.py
脚本加载灰度图像并应用cv2.COLORMAP_HSV
色彩映射,如以下代码所示:
img_COLORMAP_HSV = cv2.applyColorMap(gray_img, cv2.COLORMAP_HSV)
最后,我们将所有颜色图应用于同一灰度图像,并在同一图中绘制它们。 可以在color_map_all.py
脚本中看到。 OpenCV 定义的颜色图如下所示:
COLORMAP_AUTUMN = 0
COLORMAP_BONE = 1
COLORMAP_JET = 2
COLORMAP_WINTER = 3
COLORMAP_RAINBOW = 4
COLORMAP_OCEAN = 5
COLORMAP_SUMMER = 6
COLORMAP_SPRING = 7
COLORMAP_COOL = 8
COLORMAP_HSV = 9
COLORMAP_HOT = 11
COLORMAP_PINK = 10
COLORMAP_PARULA = 12
color_map_all.py
脚本将所有这些颜色映射应用于灰度图像。 在以下屏幕截图中可以看到此脚本的输出:
在上一个屏幕截图中,您可以看到将所有预定义的颜色图应用于灰度图像以增强可视化效果的效果。
自定义颜色图
您还可以将自定义颜色映射应用于图像。 此功能可以通过几种方式实现。
第一种方法是定义一个将0
到255
灰度值映射到 256 种颜色的颜色图。 这可以通过创建大小为256 x 1
的 8 位彩色图像来完成,以存储所有创建的颜色。 之后,您可以通过查找表将图像的灰度强度映射为定义的颜色。 为此,您可以执行以下操作之一:
- 利用
cv2.LUT()
函数 - 将图像的灰度强度映射为定义的颜色,以便可以使用
cv2.applyColorMap()
一个关键点是在创建大小为256 x 1
的 8 位彩色图像时存储所创建的颜色。 如果要使用cv2.LUT()
,则应按以下方式创建图像:
lut = np.zeros((256, 3), dtype=np.uint8)
如果要使用cv2.cv2.applyColorMap()
,则应如下所示:
lut = np.zeros((256, 1, 3), dtype=np.uint8)
完整的代码可以在color_map_custom_values.py
中看到。 在以下屏幕截图中可以看到此脚本的输出:
定义颜色图的第二种方法是只提供一些关键色,然后内插这些值,以便获得所有必要的颜色来建立查找表。 color_map_custom_key_colors.py
脚本显示了如何实现此目的。
build_lut()
函数基于这些关键色构建查找表。 基于五个色点,此函数调用np.linespace()
以获取在该间隔内计算出的所有 64 种均匀分布的颜色,每种颜色由两个色点定义。 为了更好地理解这一点,请看以下屏幕截图:
在此屏幕截图中,您可以查看例如如何计算两个线段的所有64
等距颜色(请参见绿色和蓝色突出显示的线段)。
最后,为了构建以下五个关键色点((0, (0, 255, 128))
,(0.25, (128, 184, 64))
,(0.5, (255, 128, 0))
,(0.75, (64, 128, 224))
和(1.0, (0, 128, 255))
)的查找表,对np.linespace()
进行了以下调用:
blue : np.linspace('0', '128', '64' - '0' = '64')
green : np.linspace('255', '184', '64' - '0' = '64')
red : np.linspace('128', '64', '64' - '0' = '64')
blue : np.linspace('128', '255', '128' - '64' = '64')
green : np.linspace('184', '128', '128' - '64' = '64')
red : np.linspace('64', '0', '128' - '64' = '64')
blue : np.linspace('255', '64', '192' - '128' = '64')
green : np.linspace('128', '128', '192' - '128' = '64')
red : np.linspace('0', '224', '192' - '128' = '64')
blue : np.linspace('64', '0', '256' - '192' = '64')
green : np.linspace('128', '128', '256' - '192' = '64')
red : np.linspace('224', '255', '256' - '192' = '64')
下一个屏幕截图中可以看到color_map_custom_key_colors.py
脚本的输出:
在上一个屏幕截图中,您可以看到将两个自定义颜色图应用于灰度图像的效果。
显示自定义颜色图的图例
最后,一种有趣的功能是在显示自定义颜色图时提供图例。 这可以通过color_map_custom_legend.py
脚本来实现。
为了构建图例图像,build_lut_image()
函数执行此功能。 我们首先调用build_lut()
函数以获得查询表。 然后,我们调用np.repeat()
,以便多次复制此查找表(此操作重复height
次)。 请注意,查询表的形状为(256, 3)
。 我们希望输出图像的形状为height
,256 和 3,因此可以将np.repeat()
与np.newaxis()
结合使用,如下所示:
image = np.repeat(lut[np.newaxis, ...], height, axis=0)
在以下屏幕截图中可以看到此脚本的输出:
在前面的屏幕截图中,您可以看到将两个自定义颜色图应用于灰度图像并显示每个颜色图的图例的效果。
总结
在本章中,我们回顾了计算机视觉项目中所需的大多数常见图像处理技术。 在接下来的三章中(第 6 章,“构造和构建直方图”,第 7 章,“阈值处理技术”,和第 8 章,“轮廓检测,滤波和绘图”),将对最常见的图像处理技术进行回顾。
在第 6 章,“构造和构建直方图”中,您将学习如何创建和理解直方图,这是一种强大的技术,可用于更好地理解图像内容。
问题
- 哪个函数可将多通道分割成几个单通道图像?
- 哪个函数可以将几个单通道图像合并为一个多通道图像?
- 在
x
方向上平移图像 150 像素,在y
方向上平移图像 300。 - 以
1
的比例因子将名为img
的图像相对于图像中心旋转30
度。 - 构建
5 x 5
平均核,然后使用cv2.filter2D()
将其应用于图像。 - 将
40
添加到灰度图像中的所有像素。 - 将
COLORMAP_JET
颜色图应用于灰度图像。
进一步阅读
以下参考将帮助您更深入地了解 OpenCV 中的图像处理技术:
六、构造和建立直方图
直方图是一种强大的技术,可用于更好地理解图像内容。 例如,许多摄像机实时显示正在捕获的场景的直方图,以便调整摄像机采集的某些参数(例如,曝光时间,亮度或对比度),目的是捕获合适的图像并帮助检测图像获取问题。
在本章中,您将看到如何创建和理解直方图。
本章将讨论有关直方图的主要概念,并将涵盖以下主题:
- 直方图的理论介绍
- 灰度直方图
- 颜色直方图
- 直方图的自定义可视化
- 比较 OpenCV,NumPy 和 Matplotlib 直方图
- 直方图均衡
- 自适应直方图均衡
- 比较 CLAHE 和直方图均衡
- 直方图比较
技术要求
技术要求如下:
- Python 和 OpenCV
- 特定于 Python 的 IDE
- NumPy 和 Matplotlib 包
- Git 客户端
有关如何安装这些要求的更多详细信息,请参见第 1 章,“设置 OpenCV”。 可以通过以下 URL 访问《精通 Python OpenCV 4》的 GitHub 存储库,其中包含从本书第一章到最后一章的所有必要的支持项目文件。
直方图的理论介绍
图像直方图是一种直方图,可反映图像的色调分布,并绘制每个色调值的像素数。 每个色调值的像素数也称为频率。 因此,强度值在[0, K-1]
范围内的灰度图像的直方图将准确包含K
条目。 例如,在 8 位灰度图像的情况下,K = 256
(2^8 = 256
),因此,强度值在[0, 255]
的范围内。 直方图的每个条目定义如下:
例如, h(80)
为强度为 80 的像素数。
在下一个屏幕截图中,您可以看到图像(左)具有7
不同的灰度级。 灰度等级为:30
,60
,90
,120
,150
,180
和210
。 直方图(右)显示每个色调值出现在图像中的次数(频率)。 在这种情况下,由于每个区域的大小为50 x 50
像素(2,500 像素),因此上述灰度值的频率将为 2,500,否则为0
:
请注意,直方图仅显示统计信息,而不显示像素的位置。 这就是两个图像的直方图完全相同的原因。
histogram_introduction.py
脚本如前所示绘制图形。 在此脚本中,build_sample_image()
函数使用 NumPy 操作构建第一张图像(上),build_sample_image_2()
函数构建第二张图像(下)。 接下来提供build_sample_image()
的代码:
def build_sample_image():
"""Builds a sample image with 50x50 regions of different tones of gray"""
# Define the different tones. In this case: 60, 90, 120, ..., 210
# The end of interval (240) is not included
tones = np.arange(start=60, stop=240, step=30)
# Initialize result withe the first 50x50 region with 30-intensity level
result = np.ones((50, 50, 3), dtype="uint8") * 30
# Build the image concatenating horizontally the regions:
for tone in tones:
img = np.ones((50, 50, 3), dtype="uint8") * tone
result = np.concatenate((result, img), axis=1)
return result
在这里,请注意build_sample_image2()
的代码:
def build_sample_image_2():
"""Builds a sample image with 50x50 regions of different tones of gray
flipping the output of build_sample_image()
"""
# Flip the image in the left/right direction:
img = np.fliplr(build_sample_image())
return img
下面简要描述了用于构建这些图像(np.ones()
,np.arange()
,np.concatenate()
和np.fliplr()
)的 NumPy 操作:
np.ones()
:返回给定形状和类型的数组,并填充1
的值。 在这种情况下,形状为(50, 50, 3)
和dtype="uint8"
。np.arange()
:考虑到提供的步骤,返回给定间隔内的均匀间隔的值。 不包括间隔的结尾(在这种情况下为240
)。np.concatenate()
:沿着现有轴连接一系列数组; 在这种情况下,axis=1
可以水平连接图像。np.fliplr()
:沿左右方向翻转数组。
下一节将介绍计算和显示直方图的功能。
直方图术语
在深入了解直方图以及如何通过使用与直方图相关的 OpenCV(以及 NumPy 和 Matplotlib)功能构建和可视化直方图之前,我们需要了解一些与直方图有关的术语:
bins
:上一个屏幕截图中的直方图显示了每个色调值的像素数(频率),范围从0
到255
。 这些256
值的每个在直方图术语中称为箱子。 可以根据需要选择bins
的数量。 常用值为8
,16
,32
,64
,128
和256
。 OpenCV 使用histSize
来引用bins
。range
:这是我们要测量的强度值的范围。 通常,它是[0,255]
,对应于所有色调值(0
对应于黑色,255
对应于白色)。
灰度直方图
OpenCV 提供cv2.calcHist()
函数以便计算一个或多个数组的直方图。 因此,该函数可以应用于单通道图像(例如灰度图像)和多通道图像(例如 BGR 图像)。
在本节中,我们将看到如何计算灰度图像的直方图。 该函数的签名如下:
cv2.calcHist(images, channels, mask, histSize, ranges[, hist[, accumulate]])
为此,适用以下条件:
images
:它表示作为列表提供的uint8
或float32
类型的源图像(例如[gray_img]
)。channels
:它代表我们计算其列表的直方图的通道索引(例如,对于灰度图像,[0]
;对于多通道图像,[0]
,[1]
,[2]
分别计算第一,第二或第三通道的直方图)。mask
:它代表一个遮罩图像,用于计算由遮罩定义的图像特定区域的直方图。 如果此参数等于None
,则将在没有遮罩的情况下计算直方图,并且将使用完整图像。histSize
:表示作为列表提供的bins
的数量(例如[256]
)。ranges
:它表示我们要测量的强度值的范围(例如[0,256]
)。
没有遮罩的灰度直方图
因此,用于计算完整灰度图像(不带遮罩)的直方图的代码如下:
image = cv2.imread('lenna.png')
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
hist = cv2.calcHist([gray_image], [0], None, [256], [0, 256])
在这种情况下,hist
是一个(256, 1)
数组。 数组的每个值(仓)对应于具有相应色调值的像素数(频率)。
要使用 Matplotlib 绘制直方图,可以使用plt.plot()
,提供直方图和颜色以显示直方图(例如color='m'
)。 支持以下颜色缩写-'b'
-蓝色,'g'
-绿色,‘r’
-红色,'c'
-青色,'m'
-品红色,'y'
-黄色,'k'
-黑色和 'w'
-白色。 该示例的完整代码可以在grayscale_histogram.py
脚本中看到。
我们在引言中评论了直方图可用于揭示或检测图像采集问题。 以下示例将向您展示如何检测图像亮度问题。 灰度图像的亮度可以定义为以下公式给出的图像所有像素的平均强度:
在此,I(x, y)
是图像特定像素的色调值。
因此,如果图像的平均色调较高(例如220
),则意味着图像的大多数像素将非常接近白色。 相反,如果图像上的平均色调较低(例如30
),则意味着图像的大多数像素将非常接近黑色。
在上述脚本grayscale_histogram.py
中,我们将看到如何更改图像的亮度以及直方图如何更改。
在此脚本中(为了介绍如何计算灰度图像的直方图并进行显示)已经引入了该脚本,我们还对加载灰度图像进行了一些基本数学运算。 具体来说,我们已经执行了图像加法和减法运算,以便向图像的每个像素的灰度级强度中添加特定的量或从中减去特定的量。 这可以通过cv2.add()
和cv2.subtract()
函数执行。
在第 5 章,“图像处理技术”中,我们介绍了如何对图像执行算术运算。 因此,如果您对此有任何疑问,可以阅读上一章。
这样,可以改变图像的平均亮度水平。 可以在下一个屏幕截图中看到,与脚本的输出相对应:
在此特定情况下,我们在原始图像的每个像素中添加/减去了35
,然后计算了所得图像的直方图:
# Add 35 to every pixel on the grayscale image (the result will look lighter) and calculate histogram
M = np.ones(gray_image.shape, dtype="uint8") * 35
added_image = cv2.add(gray_image, M)
hist_added_image = cv2.calcHist([added_image], [0], None, [256], [0, 256])
# Subtract 35 from every pixel (the result will look darker) and calculate histogram
subtracted_image = cv2.subtract(gray_image, M)
hist_subtracted_image = cv2.calcHist([subtracted_image], [0], None, [256], [0, 256])
如您所见,中央灰度图像对应于将35
添加到原始图像的每个像素的图像,从而使图像更亮。 在此图像中,在没有强度在[0-35]
范围内的像素的意义上,直方图似乎向右移动。 相反,右侧的灰度图像与从原始图像的每个像素中减去35
的图像相对应,从而产生较暗的图像。 在没有强度在[220-255]
范围内的像素的意义上,直方图似乎向左移动。
带遮罩的灰度直方图
要了解如何应用遮罩,请参见grayscale_histogram_mask.py
脚本,其中会创建一个遮罩,并使用先前创建的遮罩来计算直方图。 为了创建遮罩,以下行是必需的:
mask = np.zeros(gray_image.shape[:2], np.uint8)
mask[30:190, 30:190] = 255
因此,遮罩由尺寸与加载的图像相同的黑色图像组成,而白色的图像对应于我们要计算直方图的区域。
然后,通过创建的掩码调用cv2.calcHist()
函数:
hist_mask = cv2.calcHist([gray_image], [0], mask, [256], [0, 256])
在以下屏幕截图中可以看到此脚本的输出:
如您所见,我们已经修改了图像,以分别添加一些具有0
和255
灰度强度的黑色和白色小圆圈(换句话说,黑色和白色圆圈)。 这可以在第一个直方图中看到,它在bins
= 0
和255
中有两个选择。 但是,这些选择不会出现在遮罩图像的最终直方图中,因为在计算直方图时并未考虑它们,因为已应用了遮罩。
颜色直方图
在本节中,我们将看到如何计算颜色直方图。 执行此功能的脚本为color_histogram.py
。 在多通道图像(例如,BGR 图像)的情况下,计算颜色直方图的过程包括计算每个通道中的直方图。 在这种情况下,我们创建了一个从三通道图像计算直方图的函数:
def hist_color_img(img):
"""Calculates the histogram from a three-channel image"""
histr = []
histr.append(cv2.calcHist([img], [0], None, [256], [0, 256]))
histr.append(cv2.calcHist([img], [1], None, [256], [0, 256]))
histr.append(cv2.calcHist([img], [2], None, [256], [0, 256]))
return histr
应当注意,我们可能已经创建了for
循环或类似的方法,以便三次调用cv2.calcHist()
函数。 但是,为简单起见,我们执行了三个调用,分别明确指示了不同的通道。 在这种情况下,当我们加载 BGR 图片时,调用如下:
- 计算蓝色通道的直方图:
cv2.calcHist([img], [0], None, [256], [0, 256])
- 计算绿色通道的直方图:
cv2.calcHist([img], [1], None, [256], [0, 256])
- 计算红色通道的直方图:
cv2.calcHist([img], [1], None, [256], [0, 256])
因此,为了计算图像的颜色直方图,请注意以下几点:
image = cv2.imread('lenna.png')
hist_color = hist_color_img(image)
在此脚本中,我们还使用了cv2.add()
和cv2.subtract()
来修改加载的 BGR 图像的亮度,并观察直方图的变化。 在这种情况下,15
已添加/减去到原始 BGR 图像的每个像素。 在与color_histogram.py
脚本输出相对应的下一个屏幕截图中可以看到:
直方图的自定义可视化
为了可视化直方图,我们使用了plt.plot()
函数。 如果我们只想使用 OpenCV 功能来可视化直方图,则没有 OpenCV 函数可以绘制直方图。 在这种情况下,我们必须利用 OpenCV 原语(例如cv2.polylines()
和cv2.rectangle()
等)来创建一些(基本)绘制直方图的函数。 在histogram_custom_visualization.py
脚本中,我们创建了plot_hist()
函数,该函数执行此功能。 此函数创建 BGR 彩色图像,并在其中绘制直方图。 该函数的代码如下:
def plot_hist(hist_items, color):
"""Plots the histogram of a image"""
# For visualization purposes we add some offset:
offset_down = 10
offset_up = 10
# This will be used for creating the points to visualize (x-coordinates):
x_values = np.arange(256).reshape(256, 1)
canvas = np.ones((300, 256, 3), dtype="uint8") * 255
for hist_item, col in zip(hist_items, color):
# Normalize in the range for proper visualization:
cv2.normalize(hist_item, hist_item, 0 + offset_down, 300 - offset_up, cv2.NORM_MINMAX)
# Round the normalized values of the histogram:
around = np.around(hist_item)
# Cast the values to int:
hist = np.int32(around)
# Create the points using the histogram and the x-coordinates:
pts = np.column_stack((x_values, hist))
# Draw the points:
cv2.polylines(canvas, [pts], False, col, 2)
# Draw a rectangle:
cv2.rectangle(canvas, (0, 0), (255, 298), (0, 0, 0), 1)
# Flip the image in the up/down direction:
res = np.flipud(canvas)
return res
此函数接收直方图,并为直方图的每个元素建立(x, y)
点,pts
点,其中y
值表示x
元素的频率。 直方图的这些点pts
是使用cv2.polylines()
函数绘制的,我们已经在第 4 章,“在 OpenCV 中构造基本形状”。 此函数基于pts
数组绘制多边形曲线。 最后,由于y
值上下颠倒,因此图像垂直翻转。 在下一个屏幕截图中,我们可以使用plt.plot()
和我们的自定义函数比较可视化效果:
比较 OpenCV,NumPy 和 Matplotlib 直方图
我们已经看到 OpenCV 提供了cv2.calcHist()
函数来计算直方图。 此外,NumPy 和 Matplotlib 为创建直方图提供了类似的函数。 在comparing_opencv_numpy_mpl_hist.py
脚本中,我们出于性能目的比较这些函数。 从这个意义上讲,我们将看到如何使用 OpenCV,NumPy 和 Matplotlib 创建直方图,然后测量每个图形的执行时间并将结果绘制在图中。
为了测量执行时间,我们使用timeit.default_timer
,因为它会自动在您的平台和 Python 版本上提供最佳时钟。 这样,我们将其导入脚本的开头:
from timeit import default_timer as timer
这里总结了我们使用计时器的方式:
start = timer()
# ...
end = timer()
execution_time = start - end
应当考虑到default_timer()
测量可能会受到同一台计算机上同时运行的其他程序的影响。 因此,执行准确计时的最佳方法是重复几次并花费最佳时间。
为了计算直方图,我们将使用以下函数:
cv2.calcHist()
由 OpenCV 提供np.histogram()
由 NumPy 提供- Matplotlib 提供的
plt.hist()
因此,用于计算上述每个函数执行的代码如下:
start = timer()
# Calculate the histogram calling cv2.calcHist()
hist = cv2.calcHist([gray_image], [0], None, [256], [0, 256])
end = timer()
exec_time_calc_hist = (end - start) * 1000
start = timer()
# Calculate the histogram calling np.histogram():
hist_np, bins_np = np.histogram(gray_image.ravel(), 256, [0, 256])
end = timer()
exec_time_np_hist = (end - start) * 1000
start = timer()
# Calculate the histogram calling plt.hist():
(n, bins, patches) = plt.hist(gray_image.ravel(), 256, [0, 256])
end = timer()
exec_time_plt_hist = (end - start) * 1000
我们将值乘以得到毫秒(而不是秒)。 在以下屏幕截图中可以看到comparing_opencv_numpy_mpl_hist.py
脚本的输出:
可以看出,cv2.calcHist()
比np.histogram()
和plt.hist()
都快。 因此,出于性能目的,您可以使用 OpenCV 函数。
直方图均衡
在本节中,我们将看到如何使用 OpenCV 函数cv2.equalizeHist()
执行直方图均衡,以及如何将其应用于灰度图像和彩色图像。 cv2.equalizeHist()
函数可标准化亮度,并增加图像的对比度。 因此,在应用此函数后会修改图像的直方图。 在接下来的小节中,我们将探索原始直方图和修改后的直方图,以查看其变化方式。
灰度直方图均衡
使用cv2.equalizeHist()
函数以均衡给定灰度图像的对比度非常容易:
image = cv2.imread('lenna.png')
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gray_image_eq = cv2.equalizeHist(gray_image)
在grayscale_histogram_equalization.py
脚本中,我们将直方图均衡化应用于三个图像。 第一个是原始灰度图像。 第二个是原始图像,但是在我们已经向图像的每个像素添加35
的意义上进行了修改。 第三个是原始图像,但是在我们已经从图像的每个像素中减去35
的意义上进行了修改。 我们还计算了直方图均衡之前和之后的直方图。 最后,绘制所有这些图像。 在以下屏幕截图中可以看到此脚本的输出:
在上一个屏幕截图中,我们可以看到三个均衡图像确实非常相似,并且这一事实也可以反映在均衡直方图中,其中所有三个图像也非常相似。 这是因为直方图均衡化趋向于标准化图像的亮度(并增加对比度)。
颜色直方图均衡
按照相同的方法,我们可以在彩色图像中执行直方图均衡化。 我们必须说这不是彩色图像中直方图均衡的最佳方法,我们将看到如何正确执行它。 因此,此第一个(以及不正确的)版本将直方图均衡化应用于 BGR 图像的每个通道。 在以下代码中可以看到这种方法:
def equalize_hist_color(img):
"""Equalize the image splitting the image applying cv2.equalizeHist() to each channel and merging the results"""
channels = cv2.split(img)
eq_channels = []
for ch in channels:
eq_channels.append(cv2.equalizeHist(ch))
eq_image = cv2.merge(eq_channels)
return eq_image
我们创建了equalize_hist_color()
函数,该函数通过使用cv2.split()
分割 BGR 图像并将cv2.equalizeHist()
函数应用于每个通道。 最后,我们使用cv2.merge()
合并所有结果通道。 我们已将此函数应用于三个不同的图像。 第一个是原始的 BGR 图片。 第二个是原始图像,但在某种意义上进行了修改,即我们已经向图像的每个像素添加了15
。 第三个是原始图像,但在某种意义上进行了修改,即我们已经从图像的每个像素中减去了15
。 我们还计算了直方图均衡之前和之后的直方图。
最后,绘制所有这些图像。 在以下屏幕截图中可以看到color_histogram_equalization.py
脚本的输出:
我们评论说,均衡三个通道不是一个好方法,因为色调会发生巨大变化。 这是由于 BGR 颜色空间的累加特性。 由于我们分别更改三个通道的亮度和对比度,因此在合并均衡的通道时,这可能导致图像中出现新的阴影。 在上一个屏幕截图中可以看到此问题。
更好的方法是将 BGR 图像转换为包含亮度/强度通道(Yuv,Lab,HSV 和 HSL)的色彩空间。 然后,我们仅在亮度通道上应用直方图均衡化,最后执行逆变换,也就是说,我们合并通道并将它们转换回 BGR 颜色空间。
在color_histogram_equalization_hsv.py
脚本中可以看到这种方法,其中equalize_hist_color_hsv()
函数将执行以下功能:
def equalize_hist_color_hsv(img):
"""Equalize the image splitting the image after HSV conversion and applying cv2.equalizeHist()
to the V channel, merging the channels and convert back to the BGR color space
"""
H, S, V = cv2.split(cv2.cvtColor(img, cv2.COLOR_BGR2HSV))
eq_V = cv2.equalizeHist(V)
eq_image = cv2.cvtColor(cv2.merge([H, S, eq_V]), cv2.COLOR_HSV2BGR)
return eq_image
在以下屏幕截图中可以看到输出:
可以看出,仅对 HSV 图像的 V 通道进行均衡后获得的结果要比对 BGR 图像的所有通道进行均衡好得多。 正如我们所评论的,这种方法对于包含亮度/强度通道(Yuv,Lab,HSV 和 HSL)的色彩空间也是有效的。 这将在下一部分中看到。
对比度受限的自适应直方图均衡
在本节中,我们将了解如何应用对比度受限的自适应直方图均衡(CLAHE)来均衡图像,这是自适应直方图均衡的一种形式 (AHE),其中对比度放大受到限制。 图像的相对均匀区域中的噪声被 AHE 过度放大,而 CLAHE 通过限制对比度放大来解决此问题。 该算法可用于改善图像的对比度。 该算法通过创建原始图像的多个直方图来工作,并使用所有这些直方图重新分配图像的亮度。
在clahe_histogram_equalization.py
脚本中,我们将 CLAHE 应用于灰度和彩色图像。 应用 CLAHE 时,有两个参数需要调整。 第一个是clipLimit
,它设置对比度限制的阈值。 默认值为40
。 第二个是tileGridSize
,它设置行和列中瓦片的数量。 当应用 CLAHE 时,图像被分成称为瓦片的小块(默认为8 x 8
)以执行其计算。
要将 CLAHE 应用于灰度图像,我们必须执行以下操作:
clahe = cv2.createCLAHE(clipLimit=2.0)
gray_image_clahe = clahe.apply(gray_image)
此外,我们还可以将 CLAHE 应用于彩色图像,这与上一节中介绍的彩色图像对比度均衡方法非常相似,其中仅均衡 HSV 图像的亮度通道后的结果要比均衡所有通道的结果好得多。 BGR 图片。
在本节中,我们将创建四个函数,以通过仅在不同颜色空间的亮度通道上使用 CLAHE 来均衡彩色图像:
def equalize_clahe_color_hsv(img):
"""Equalize the image splitting after conversion to HSV and applying CLAHE
to the V channel and merging the channels and convert back to BGR
"""
cla = cv2.createCLAHE(clipLimit=4.0)
H, S, V = cv2.split(cv2.cvtColor(img, cv2.COLOR_BGR2HSV))
eq_V = cla.apply(V)
eq_image = cv2.cvtColor(cv2.merge([H, S, eq_V]), cv2.COLOR_HSV2BGR)
return eq_image
def equalize_clahe_color_lab(img):
"""Equalize the image splitting after conversion to LAB and applying CLAHE
to the L channel and merging the channels and convert back to BGR
"""
cla = cv2.createCLAHE(clipLimit=4.0)
L, a, b = cv2.split(cv2.cvtColor(img, cv2.COLOR_BGR2Lab))
eq_L = cla.apply(L)
eq_image = cv2.cvtColor(cv2.merge([eq_L, a, b]), cv2.COLOR_Lab2BGR)
return eq_image
def equalize_clahe_color_yuv(img):
"""Equalize the image splitting after conversion to YUV and applying CLAHE
to the Y channel and merging the channels and convert back to BGR
"""
cla = cv2.createCLAHE(clipLimit=4.0)
Y, U, V = cv2.split(cv2.cvtColor(img, cv2.COLOR_BGR2YUV))
eq_Y = cla.apply(Y)
eq_image = cv2.cvtColor(cv2.merge([eq_Y, U, V]), cv2.COLOR_YUV2BGR)
return eq_image
def equalize_clahe_color(img):
"""Equalize the image splitting the image applying CLAHE to each channel and merging the results"""
cla = cv2.createCLAHE(clipLimit=4.0)
channels = cv2.split(img)
eq_channels = []
for ch in channels:
eq_channels.append(cla.apply(ch))
eq_image = cv2.merge(eq_channels)
return eq_image
下一个屏幕截图中可以看到此脚本的输出,在将所有这些函数应用于测试图像后,我们在其中比较了结果:
在上一个屏幕截图中,我们可以通过更改clipLimit
参数在测试图像上应用 CLAHE 后看到结果。 此外,在不同颜色空间(LAB,HSV 和 YUV)的亮度通道上应用 CLAHE 之后,我们可以看到不同的结果。 最后,我们会看到在 BGR 图像的三个通道上应用 CLAHE 的错误方法。
比较 CLAHE 和直方图均衡
为了完整起见,在comparing_hist_equalization_clahe.py
脚本中,您可以看到 CLAHE 和直方图均衡化(cv2.equalizeHist()
)如何在同一图像上工作,同时可视化结果图像和结果直方图。
在以下屏幕截图中可以看到:
可以肯定地说,与在许多情况下应用直方图均衡化相比,CLAHE 提供了更好的结果和性能。 从这个意义上讲,CLAHE 通常在许多计算机视觉应用(例如,面部处理等)中用作第一步。
直方图比较
OpenCV 提供的与直方图相关的一种有趣函数是cv2.compareHist()
函数,该函数可用于获取一个数值参数,该数值参数表示两个直方图相互匹配的程度。 从这个意义上讲,由于直方图反映了图像中像素值的强度分布,因此该函数可用于比较图像。 如前所述,直方图仅显示统计信息,而不显示像素的位置。 因此,图像比较的常用方法是将图像划分为一定数量的区域(通常具有相同的大小),计算每个区域的直方图,最后将所有直方图连接起来以创建图像的特征表示 。 在此示例中,为简单起见,我们不会将图像划分为一定数量的区域,因此将仅使用一个区域(完整图像)。
cv2.compareHist()
函数的签名如下:
cv2.compareHist(H1, H2, method)
这里,H1
和H2
是要比较的直方图,method
建立了比较方法。
OpenCV 提供了四种不同的度量标准(方法)来计算匹配项:
cv2.HISTCMP_CORREL
:此度量标准计算两个直方图之间的相关性。 此指标返回[-1, 1]
范围内的值,其中1
表示完美匹配,而-1
完全不匹配。cv2.HISTCMP_CHISQR
:此度量标准计算两个直方图之间的卡方距离。 此指标返回[0, unbounded]
范围内的值,其中0
表示完美匹配,而不匹配则不受限制。cv2.HISTCMP_INTERSECT
:此度量标准计算两个直方图之间的交集。 如果直方图已归一化,则此度量标准将返回[0,1]
范围内的值,其中1
表示完美匹配,而0
则完全不匹配。cv2.HISTCMP_BHATTACHARYYA
:此度量标准计算两个直方图之间的 Bhattacharyya 距离。 此指标返回[0,1]
范围内的值,其中0
是完美匹配,而1
完全不匹配。
在compare_histograms.py
脚本中,我们首先加载四个图像,然后使用先前注释的所有度量标准来计算所有这些图像与测试图像之间的相似度。
我们使用的四个图像如下:
gray_image.png
:此图像对应于灰度图像。gray_added_image.png
:此图像对应于原始图像,但在某种意义上进行了修改,即我们已经向图像的每个像素添加了35
。gray_subtracted_image.png
:此图像对应于原始图像,但在某种意义上进行了修改,因为我们已经对图像的每个像素减去了35
。gray_blurred.png
:此图像对应于原始图像,但已使用模糊过滤器(cv2.blur(gray_image, (10, 10)
)进行了修改。
测试(或查询)图像也是gray_image.png
。 在下一个屏幕截图中可以看到该示例的输出:
如您所见,由于img 1
是同一张图片,因此可提供最佳结果(在所有度量标准中均完美匹配)。 此外,img 2
还提供了非常好的表现指标。 这是有道理的,因为img 2
是查询图像的平滑版本。 最后,img 3
和img 4
的表现指标不佳,因为直方图已移动。
总结
在本章中,已经回顾了与直方图有关的所有主要概念。 从这个意义上讲,我们已经了解了直方图的含义以及如何使用 OpenCV,NumPy 和 Matplotlib 函数计算直方图。 此外,我们已经看到了灰度直方图和颜色直方图之间的差异,展示了如何计算和显示这两种类型。 直方图均衡也是处理直方图时的重要因素,我们已经了解了如何对灰度图像和彩色图像执行直方图均衡。 最后,直方图比较对于执行图像比较也可能非常有帮助。 我们已经看到 OpenCV 提供的四个度量来度量两个直方图之间的相似性。
与下一章相关,将介绍与计算机视觉应用中作为图像分割的关键部分所需内容相关的主要阈值处理技术(简单阈值处理,自适应阈值处理和大津的阈值处理等)。
问题
- 什么是图像直方图?
- 使用
64
箱计算灰度图像的直方图。 - 将
50
添加到灰度图像上的每个像素(结果看起来更亮)并计算直方图。 - 计算没有遮罩的 BGR 图像的红色通道直方图。
- OpenCV,NumPy 和 Matplotlib 提供哪些函数来计算直方图?
- 修改
grayscale_histogram.py
脚本以计算这三个图像(gray_image
,added_image
和subtracted_image
)的亮度。 将脚本重命名为grayscale_histogram_brightness.py
。 - 修改
comparing_hist_equalization_clahe.py
脚本以显示cv2.equalizeHist()
和 CLAHE 的执行时间。 将其重命名为comparing_hist_equalization_clahe_time.py
。
进一步阅读
此处列出的参考将帮助您更深入地研究 OpenCV 中的图像处理技术:
七、分割技术
图像分割是许多计算机视觉应用中的关键过程。 它通常用于将图像划分为不同的区域,理想情况下,这些区域对应于从背景提取的现实世界对象。 因此,图像分割是图像识别和内容分析的重要步骤。 图像阈值化是一种简单但有效的图像分割方法,其中,根据像素的强度值对像素进行分割,因此,可以将其用于将图像分割为前景和背景。
在本章中,您将学习阈值技术在计算机视觉项目中的重要性。 我们将审查 OpenCV(以及 Scikit-image 图像处理库)提供的主要阈值技术,这些技术将在计算机视觉应用中用作图像分割的关键部分。
本章的主要部分如下:
- 阈值技术简介
- 简单阈值技术
- 自适应阈值技术
- 大津阈值算法
- 三角阈值算法
- 彩色图像的阈值
- 使用 scikit-image 的阈值算法
技术要求
技术要求如下:
- Python 和 OpenCV。
- 特定于 Python 的 IDE。
- NumPy 和 Matplotlib 包。
- scikit-image 图像处理库(对于本章的最后部分是可选的。请参阅 scikit-image 阈值算法以了解如何为基于 Conda 的发行版安装它)。 请参阅以下说明,以便使用
pip
进行安装。 - 还需要 SciPy 库(对于本章的最后部分是可选的)。 请参阅以下说明,以便使用
pip
进行安装。 - 一个 Git 客户端。
有关如何安装这些要求的更多详细信息,请参见第 1 章,“设置 OpenCV”。 可通过 github 访问《精通 Python OpenCV4》的 GitHub 存储库,其中包含从本书第一章到最后一章的所有必要的支持项目文件。。
安装 scikit-image
要安装 scikit-image,请使用以下命令:
$ pip install scikit-image
或者,您也可以为基于 Conda 的发行版安装 scikit-image,如使用该库的特定部分所述。
要检查安装是否正确执行,只需打开 Python shell 并尝试按以下方式导入scikit-image
库:
python
import skimage
安装 SciPy
要安装 SciPy,请使用以下命令:
$ pip install scipy
要检查安装是否正确执行,只需打开 Python shell 并尝试按以下方式导入scipy
库:
python
import scipy
请记住,推荐的方法是在虚拟环境中安装包。 请参阅第 1 章,“设置 OpenCV”,以了解如何创建和管理虚拟环境。
阈值技术简介
阈值是一种简单而有效的方法,可将图像划分为前景和背景。 图像分割的目的是将图像的表示形式修改为更易于处理的另一种表示形式。 例如,图像分割通常用于根据对象的某些属性(例如颜色,边缘或直方图)从背景中提取对象。 如果像素强度小于某个预定义常数(阈值),则最简单的阈值化方法将源图像中的每个像素替换为黑色像素;如果像素强度大于阈值,则将像素替换为白色像素。
OpenCV 提供cv2.threshold()
函数以对图像进行阈值处理。 我们将在本章接下来的小节中详细介绍该函数。
在thresholding_introduction.py
脚本中,我们将cv2.threshold()
函数与一些预定义的阈值一起应用-0
,50
,100
,150
,200
和250
,以便查看对于不同的阈值图像如何变化。
例如,要使用thresh = 50
的阈值对图像进行阈值处理,代码如下:
ret1, thresh1 = cv2.threshold(gray_image, 50, 255, cv2.THRESH_BINARY)
在此,thresh1
是阈值图像,是黑白图像。 强度小于50
的像素将为黑色,强度大于50
的像素将为白色。
在下面的代码中可以看到另一个示例,其中thresh5
对应于阈值图像:
ret5, thresh5 = cv2.threshold(gray_image, 200, 255, cv2.THRESH_BINARY)
在这种情况下,强度小于200
的像素将为黑色,强度大于200
的像素将为白色。
在以下屏幕截图中可以看到上述脚本的输出:
在此屏幕截图中,您可以看到源图像,它是一个示例图像,其中一些大小相同的区域填充了不同的灰色调。 更具体地,这些灰色色调是0
,50
,100
,150
,200
和250
。 build_sample_image()
函数按以下方式构建此样本图像:
def build_sample_image():
"""Builds a sample image with 50x50 regions of different tones of gray"""
# Define the different tones.
# The end of interval is not included
tones = np.arange(start=50, stop=300, step=50)
# print(tones)
# Initialize result with the first 50x50 region with 0-intensity level
result = np.zeros((50, 50, 3), dtype="uint8")
# Build the image concatenating horizontally the regions:
for tone in tones:
img = np.ones((50, 50, 3), dtype="uint8") * tone
result = np.concatenate((result, img), axis=1)
return result
简要描述了用于构建此示例图像的 NumPy 操作(np.ones()
,np.zeros()
,np.arange()
,np.concatenate()
和np.fliplr()
):
np.ones()
:这将返回给定形状和类型的数组,并填充为 1; 在这种情况下,形状为(50, 50, 3)
和dtype="uint8"
。np.zeros()
:这将返回给定形状和类型的数组,并用零填充; 在这种情况下,形状为(50, 50, 3)
和dtype="uint8"
。np.arange()
:考虑到提供的步骤,它会在给定的间隔内返回均匀间隔的值。 不包括间隔的末尾(在这种情况下为300
)。np.concatenate()
:这将沿着现有轴(在本例中为axis=1
)连接一系列数组,以水平连接图像。
构建样本图像后,下一步是使用不同的阈值对其进行阈值处理。 在这种情况下,阈值是0
,50
,100
,150
,200
和250
。
您将看到阈值与样本图像中不同的灰度色调相同。 用不同的阈值对样本图像进行阈值处理的代码如下:
ret1, thresh1 = cv2.threshold(gray_image, 0, 255, cv2.THRESH_BINARY)
ret2, thresh2 = cv2.threshold(gray_image, 50, 255, cv2.THRESH_BINARY)
ret3, thresh3 = cv2.threshold(gray_image, 100, 255, cv2.THRESH_BINARY)
ret4, thresh4 = cv2.threshold(gray_image, 150, 255, cv2.THRESH_BINARY)
ret5, thresh5 = cv2.threshold(gray_image, 200, 255, cv2.THRESH_BINARY)
ret6, thresh6 = cv2.threshold(gray_image, 250, 255, cv2.THRESH_BINARY)
您可以根据阈值和样本图像的不同灰度色调,看到阈值化后的黑白图像如何变化。
在对图像进行阈值处理后,共同的输出是黑白图像。 在前面的章节中,屏幕截图的背景也是白色的。 在本章中,为了进行适当的可视化,我们已使用fig.patch.set_facecolor('silver')
将屏幕快照的背景更改为silver
颜色。
简单阈值
为了执行简单的阈值处理,OpenCV 提供了cv2.threshold()
函数,该函数在上一节中进行了简要介绍。 此方法的签名如下:
cv2.threshold(src, thresh, maxval, type, dst=None) -> retval, dst
cv2.threshold()
函数将固定级别的阈值应用于src
输入数组(多通道,8 位或 32 位浮点)。 固定级别由thresh
参数调整,该参数设置阈值。 type
参数设置阈值类型,这将在下一个小节中进一步说明。
不同的类型如下:
cv2.THRESH_BINARY
cv2.THRESH_BINARY_INV
cv2.THRESH_TRUNC
cv2.THRESH_TOZERO
cv2.THRESH_TOZERO_INV
cv2.THRESH_OTSU
cv2.THRESH_TRIANGLE
此外,maxval
参数设置最大值,仅与cv2.THRESH_BINARY
和cv2.THRESH_BINARY_INV
阈值类型一起使用。 最后,仅在cv2.THRESH_OTSU
和cv2.THRESH_TRIANGLE
阈值类型中,输入图像应为单通道。
在本节中,我们将检查所有可能的配置以了解所有这些参数。
阈值类型
阈值操作的类型根据其公式描述。 考虑到src
是源(原始)图像,而dst
对应于阈值化后的目标(结果)图像。 从这个意义上讲,src(x, y)
对应于源图像像素(x, y)
的强度,而dst(x, y)
将对应于目标图像的像素(x, y)
的强度。
这是cv2.THRESH_BINARY
的公式:
因此,如果像素src(x, y)
的强度高于thresh
,则将新像素强度设置为maxval
参数。 否则,将像素设置为0
。
这是cv2.THRESH_BINARY_INV
的公式:
因此,如果像素src(x, y)
的强度高于thresh
,则新像素强度设置为0
。 否则,将其设置为maxval
。
这是cv2.THRESH_TRUNC
的公式:
因此,如果像素src(x, y)
的强度高于thresh
,则新像素强度设置为threshold
。 否则,将其设置为src(x, y)
。
这是cv2.THRESH_TOZERO
的公式:
因此,如果像素src(x, y)
的强度高于thresh
,则新像素值将设置为src(x, y)
。 否则,将其设置为0
。
这是cv2.THRESH_TOZERO_INV
的公式:
因此,如果像素src(x, y)
的强度大于thresh
,则新像素值将设置为0
。 否则,将其设置为src(x, y)
。
另外,可以将特殊的cv2.THRESH_OTSU
和cv2.THRESH_TRIANGLE
值与先前引入的值之一cv2.THRESH_BINARY
,cv2.THRESH_BINARY_INV
,cv2.THRESH_TRUNC
,cv2.THRESH_TOZERO
和cv2.THRESH_TOZERO_INV
组合在一起。 在这些情况下(cv2.THRESH_OTSU
和cv2.THRESH_TRIANGLE
),阈值运算(仅对 8 位图像实现)将计算最佳阈值,而不是指定的thresh
值。 应当注意,阈值操作返回计算出的最佳阈值。
thresholding_simple_types.py
脚本可帮助您了解上述类型。 我们使用上一节中介绍的相同样本图像,并对所有先前类型使用固定阈值(thresh = 100
)执行阈值操作。
执行此操作的关键代码如下:
ret1, thresh1 = cv2.threshold(gray_image, 100, 255, cv2.THRESH_BINARY)
ret2, thresh2 = cv2.threshold(gray_image, 100, 220, cv2.THRESH_BINARY)
ret3, thresh3 = cv2.threshold(gray_image, 100, 255, cv2.THRESH_BINARY_INV)
ret4, thresh4 = cv2.threshold(gray_image, 100, 220, cv2.THRESH_BINARY_INV)
ret5, thresh5 = cv2.threshold(gray_image, 100, 255, cv2.THRESH_TRUNC)
ret6, thresh6 = cv2.threshold(gray_image, 100, 255, cv2.THRESH_TOZERO)
ret7, thresh7 = cv2.threshold(gray_image, 100, 255, cv2.THRESH_TOZERO_INV)
如前所述,maxval
参数将最大值设置为仅与cv2.THRESH_BINARY
和cv2.THRESH_BINARY_INV
阈值类型一起使用。 在此示例中,我们为cv2.THRESH_BINARY
和cv2.THRESH_BINARY_INV
类型将maxval
的值设置为255
和220
,以查看阈值图像在两种情况下如何变化。 下一个屏幕截图中可以看到此脚本的输出:
在上一个屏幕截图中,您既可以看到原始灰度图像,也可以看到所执行的七个阈值操作的结果。 此外,您可以看到maxval
参数的效果,该参数仅与cv2.THRESH_BINARY
和cv2.THRESH_BINARY_INV
阈值类型一起使用。 更具体地说,例如,参见第一和第二阈值运算结果之间的差异(结果图像中的白色与灰色之间的差异),以及第三和第四阈值运算结果之间的差异(结果图像中的白色与灰色之间的差异)。
应用于真实图像的简单阈值
在前面的示例中,我们将简单的阈值操作应用于定制图像,以查看不同参数的工作方式。 在本节中,我们将cv2.threshold()
应用于实际图像。 thresholding_example.py
脚本执行此操作。 我们对cv2.threshold()
函数应用了不同的阈值,如下所示– 60,70,80,90,100,110,120,130
:
ret1, thresh1 = cv2.threshold(gray_image, 60, 255, cv2.THRESH_BINARY)
ret2, thresh2 = cv2.threshold(gray_image, 70, 255, cv2.THRESH_BINARY)
ret3, thresh3 = cv2.threshold(gray_image, 80, 255, cv2.THRESH_BINARY)
ret4, thresh4 = cv2.threshold(gray_image, 90, 255, cv2.THRESH_BINARY)
ret5, thresh5 = cv2.threshold(gray_image, 100, 255, cv2.THRESH_BINARY)
ret6, thresh6 = cv2.threshold(gray_image, 110, 255, cv2.THRESH_BINARY)
ret7, thresh7 = cv2.threshold(gray_image, 120, 255, cv2.THRESH_BINARY)
ret8, thresh8 = cv2.threshold(gray_image, 130, 255, cv2.THRESH_BINARY)
最后,我们显示阈值图像,如下所示:
show_img_with_matplotlib(cv2.cvtColor(thresh1, cv2.COLOR_GRAY2BGR), "threshold = 60", 2)
show_img_with_matplotlib(cv2.cvtColor(thresh2, cv2.COLOR_GRAY2BGR), "threshold = 70", 3)
show_img_with_matplotlib(cv2.cvtColor(thresh3, cv2.COLOR_GRAY2BGR), "threshold = 80", 4)
show_img_with_matplotlib(cv2.cvtColor(thresh4, cv2.COLOR_GRAY2BGR), "threshold = 90", 5)
show_img_with_matplotlib(cv2.cvtColor(thresh5, cv2.COLOR_GRAY2BGR), "threshold = 100", 6)
show_img_with_matplotlib(cv2.cvtColor(thresh6, cv2.COLOR_GRAY2BGR), "threshold = 110", 7)
show_img_with_matplotlib(cv2.cvtColor(thresh7, cv2.COLOR_GRAY2BGR), "threshold = 120", 8)
show_img_with_matplotlib(cv2.cvtColor(thresh8, cv2.COLOR_GRAY2BGR), "threshold = 130", 9)
在以下屏幕截图中可以看到此脚本的输出:
如您所见,在使用cv2.threshold()
阈值图像时,阈值起着至关重要的作用。 假设您的图像处理算法尝试识别网格内的数字。 如果阈值较低(例如threshold = 60
),则阈值图像中会缺少一些数字。 另一方面,如果阈值高(例如threshold = 120
),则黑色像素会遮挡一些数字。 因此,为整个图像建立全局阈值是非常困难的。 此外,如果图像受不同的照明条件影响,则几乎不可能完成此任务。 这就是为什么其他阈值算法可以应用于图像阈值的原因。 在下一节中,将介绍自适应阈值算法。
最后,您可以在代码段中看到我们已经创建了几个具有固定阈值的阈值图像(一张一张)。 可以通过创建一个包含阈值的数组(使用np.arange()
)并在创建的数组上进行迭代以针对该数组的每个值调用cv.threshold()
来进行优化。 请参阅“问题”部分,因为建议将此优化作为练习。
自适应阈值
在上一节中,我们已使用全局阈值应用了cv2.threshold()
。 如我们所见,由于图像不同区域的照明条件不同,因此获得的结果不是很好。 在这些情况下,您可以尝试自适应阈值化。 在 OpenCV 中,自适应阈值通过cv2.adapativeThreshold()
函数执行。 此方法的签名如下:
adaptiveThreshold(src, maxValue, adaptiveMethod, thresholdType, blockSize, C[, dst]) -> dst
此函数将自适应阈值应用于src
数组(8 位单通道图像)。 maxValue
参数设置dst
图像中满足条件的像素的值。 adaptiveMethod
参数设置自适应阈值算法以使用:
cv2.ADAPTIVE_THRESH_MEAN_C
:T(x, y)
阈值计算为(x, y)
的blockSize x blockSize
邻域平均值减去C
参数cv2.ADAPTIVE_THRESH_GAUSSIAN_C
:将T(x, y)
阈值计算为(x, y)
的blockSize x blockSize
邻域的加权总和减去C
参数
blockSize
参数设置用于计算像素阈值的邻域的大小,并且可以采用3, 5, 7,...
等值。
C
参数只是从均值或加权均值中减去的常数(取决于adaptiveMethod
参数设置的自适应方法)。 通常,此值为正,但可以为零或负。 最后,thresholdType
参数设置cv2.THRESH_BINARY
或cv2.THRESH_BINARY_INV
阈值类型。
根据以下公式,其中T(x, y)
是为每个像素计算的阈值,thresholding_adaptive.py
脚本使用cv2.ADAPTIVE_THRESH_MEAN_C
和cv2.ADAPTIVE_THRESH_GAUSSIAN_C
方法将自适应阈值应用于测试图像:
- 这是
cv2.THRESH_BINARY
的公式:
- 这是
cv2.THRESH_BINARY_INV
的公式:
在以下屏幕截图中可以看到此脚本的输出:
在上一个屏幕截图中,您可以在应用具有不同参数的cv2.adaptiveThreshold()
之后看到输出。 如前所述,如果您的任务是识别数字,则自适应阈值处理可以为您提供更好的阈值图像。 但是,您也可以看到,图像中出现了很多噪点。 为了对其进行处理,可以应用一些平滑操作(请参阅第 5 章,“图像处理技术”)。
在这种情况下,我们可以应用双边过滤器,因为它在去除噪声的同时保持尖锐边缘非常有用。 为了应用双边过滤器,OpenCV 提供了cv2.bilateralFilter()
函数。 因此,我们可以在对图像进行阈值处理之前应用该函数,如下所示:
gray_image = cv2.bilateralFilter(gray_image, 15, 25, 25)
此示例的代码可以在thresholding_adaptive_filter_noise.py
脚本中看到。 在以下屏幕截图中可以看到输出:
您会看到,应用平滑过滤器是处理噪声的好方法。 在这种情况下,应用双边过滤器是因为我们要保持边缘清晰。
大津阈值算法
正如我们在前面的部分中看到的那样,简单的阈值算法应用了任意全局阈值。 在这种情况下,我们需要做的是尝试使用不同的阈值,并查看阈值图像,以查看结果是否满足我们的需求。 但是,这种方法可能非常繁琐。
一种解决方案是使用 OpenCV 通过cv2.adapativeThreshold()
函数提供的自适应阈值。 在 OpenCV 中应用自适应阈值设置时,无需设置阈值,这是一件好事。
但是,应正确建立两个参数:blockSize
参数和C
参数。 另一种方法是使用大津的二值化算法,这在处理双峰图像时是一种很好的方法。 双峰图像可以通过其包含两个峰的直方图来表征。大津的算法通过最大化两类像素之间的方差来自动计算将两个峰分开的最佳阈值。 等效地,最佳阈值使组内差异最小化。大津的二值化算法是一种统计方法,因为它依赖于从直方图得出的统计信息(例如,均值,方差或熵)。 为了计算 OpenCV 中大津的二值化,我们使用cv2.threshold()
函数,如下所示:
ret, th = cv2.threshold(gray_image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
在这种情况下,由于大津的二值化算法会计算最佳阈值,因此无需设置阈值,这就是thresh = 0
的原因。 cv2.THRESH_OTSU
标志指示将应用大津算法。 另外,在这种情况下,此标志与cv2.THRESH_BINARY
组合。 实际上,它可以与cv2.THRESH_BINARY
,cv2.THRESH_BINARY_INV
,cv2.THRESH_TRUNC
,cv2.THRESH_TOZERO
和cv2.THRESH_TOZERO_INV
组合。 此函数返回阈值图像th
和阈值ret
。
在thresholding_otsu.py
脚本中,我们已将此算法应用于样本图像。 在以下屏幕截图中可以看到输出。 我们修改了show_hist_with_matplotlib_gray()
函数,添加了一个额外的参数,该参数对应于大津算法计算出的最佳阈值。 为了绘制此阈值,我们绘制一条线,以t
阈值建立x
坐标,如下所示:
plt.axvline(x=t, color='m', linestyle='--')
在以下屏幕截图中可以看到thresholding_otsu.py
脚本的输出:
在上一个屏幕截图中,我们可以看到图像没有噪点,带有白色背景和非常清晰的绿叶。 但是,噪声会影响阈值算法,因此我们应该对其进行适当处理。 例如,在上一节中,我们执行双边滤波,以滤除一些噪声并保留边缘。 在下一个示例中,我们将向叶图像添加一些噪声,以查看阈值算法如何受到影响。 可以在thresholding_otsu_filter_noise.py
脚本中看到。 在此脚本中,我们在应用高斯过滤器之前和之后应用大津的二值化算法,以查看阈值图像如何急剧变化。
在以下屏幕截图中可以看到:
如我们所见,如果不应用平滑过滤器(在这种情况下为高斯过滤器),则阈值图像也会充满噪声。 但是,应用高斯过滤器是正确过滤噪声的好方法。 此外,滤波后的图像是双峰的。 这个事实可以在对应于滤波图像的直方图中看到。 在这种情况下,大津的二值化算法可以正确分割叶子。
三角二值化算法
另一种自动阈值算法是三角算法,该算法被认为是基于形状的方法,因为它可以分析直方图的结构(或形状)(例如,尝试查找谷值,峰值和其他形状直方图特征)。 该算法分三步工作。 第一步,在灰度轴上的直方图最大值b_max
与灰度轴上的最小值b_min
之间计算一条线 。 在第二步中,对于b[b_min - b_max]
的所有值,计算直线(在第一步中计算出)到直方图的距离。 最后,在第三步中,将直方图和直线之间的距离最大的级别选择为阈值。
在 OpenCV 中使用三角二值化算法的方式与大津的算法非常相似。 实际上,仅应适当更改一个标志。 在大津市二值化的情况下,设置了cv2.THRESH_OTSU
标志。 对于三角二值化算法,标记为cv2.THRESH_TRIANGLE
,如下所示:
ret1, th1 = cv2.threshold(gray_image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_TRIANGLE)
在下一个屏幕截图中,您可以看到将三角二值化算法应用于噪声图像(与上一节中大津的二值化示例中使用的图像相同)的输出。 该示例的完整代码可以在thresholding_triangle_filter_noise.py
脚本中看到:
您会看到,应用高斯过滤器是过滤噪声的好方法。 这样,三角二值化算法可以正确地分割叶子。
彩色图像的阈值
cv2.threshold()
函数也可以应用于多通道图像。 可以在thresholding_bgr.py
脚本中看到。 在这种情况下,cv2.threshold()
函数在 BGR 图像的每个通道中应用阈值操作。 这产生与在每个通道中应用此函数并合并阈值通道相同的结果:
ret1, thresh1 = cv2.threshold(image, 150, 255, cv2.THRESH_BINARY)
因此,上一行代码产生与执行以下操作相同的结果:
(b, g, r) = cv2.split(image)
ret2, thresh2 = cv2.threshold(b, 150, 255, cv2.THRESH_BINARY)
ret3, thresh3 = cv2.threshold(g, 150, 255, cv2.THRESH_BINARY)
ret4, thresh4 = cv2.threshold(r, 150, 255, cv2.THRESH_BINARY)
bgr_thresh = cv2.merge((thresh2, thresh3, thresh4))
结果可以在以下屏幕截图中看到:
尽管您可以在多通道图像(例如 BGR 图像)上执行cv2.threshold()
,但是此操作可能会产生奇怪的结果。 例如,在结果图像中,由于每个通道只能采用两个值(在这种情况下为0
和255
),因此最终图像仅具有2^3
个可能的颜色 。 在下一个屏幕截图中,我们还将这个颜色阈值应用于另一个测试图像:
如前面的屏幕快照所示,输出图像仅具有2^3
个可能的颜色。 因此,在 BGR 图像中执行阈值操作时,应考虑到这一点。
使用 scikit-image 的阈值算法
正如我们在第 1 章,“设置 OpenCV”中提到的那样,还有其他包可用于科学计算,数据科学,机器学习,深度学习和计算机视觉。 与计算机视觉有关,scikit-image 是图像处理算法的集合。 scikit-image 操纵的图像是 NumPy 数组。
在本节中,我们将结合阈值技术使用 Scikit-image 功能。 因此,如果要复制此处获得的结果,则第一步是安装它。 请参阅这里,以便在您的操作系统上正确安装 scikit-image。 在这里,我们将使用 Conda 来安装它,Conda 是一个开源包管理系统(也是环境管理系统)。 请参阅第 1 章,“设置 OpenCV”,以了解如何安装 Anaconda/Miniconda 发行版和 Conda。 要为基于 Conda 的发行版(Anaconda,Miniconda)安装 scikit-image,请执行以下代码:
conda install -c conda-forge scikit-image
使用 scikit-image 介绍阈值
为了测试 Scikit-image,我们将使用大津的二值化算法对测试图像进行阈值处理。 为了尝试此方法,第一步是导入所需的包。 在这种情况下,与 scikit-image 关联如下:
from skimage.filters import threshold_otsu
from skimage import img_as_ubyte
将大津的二值化算法与 scikit-image 结合使用的关键代码如下:
thresh = threshold_otsu(gray_image)
binary = gray_image > thresh
binary = img_as_ubyte(binary)
threshold_otsu(gray_image)
函数基于大津的二值化算法返回阈值。 然后,使用该值构造二进制图像(dtype= bool
),应将其转换为 8 位无符号整数格式(dtype= uint8
)以进行适当的可视化。 img_as_ubyte()
函数用于此目的。 该示例的完整代码可以在thresholding_scikit_image_otsu.py
脚本中看到。
在以下屏幕截图中可以看到输出:
现在,我们将尝试使用 scikit-image 进行一些阈值处理。
尝试使用 scikit-image 更多的阈值技术
我们将对比较大津,三角形,Niblack 和 Sauvola 的阈值处理技术的测试图像进行阈值处理。大津和三角形是全局阈值技术,而 Niblack 和 Sauvola 是局部阈值技术。 当背景不均匀时,局部阈值技术被认为是更好的方法。 有关 Niblack 和 Sauvola 阈值算法的更多信息,请分别参见《数字图像处理简介》(1986)和《自适应文档图像二值化》(2000)。 该示例的完整代码可以在thresholding_scikit_image_techniques.py
脚本中看到。 为了尝试这些方法,第一步是导入所需的包。 在这种情况下,与 scikit-image 关联如下:
from skimage.filters import (threshold_otsu, threshold_triangle, threshold_niblack, threshold_sauvola)
from skimage import img_as_ubyte
为了对 scikit-image 执行阈值化操作,我们调用每种阈值化方法(threshold_otsu()
,threshold_niblack()
,threshold_sauvola()
和threshold_triangle()
):
# Trying Otsu's scikit-image algorithm:
thresh_otsu = threshold_otsu(gray_image)
binary_otsu = gray_image > thresh_otsu
binary_otsu = img_as_ubyte(binary_otsu)
# Trying Niblack's scikit-image algorithm:
thresh_niblack = threshold_niblack(gray_image, window_size=25, k=0.8)
binary_niblack = gray_image > thresh_niblack
binary_niblack = img_as_ubyte(binary_niblack)
# Trying Sauvola's scikit-image algorithm:
thresh_sauvola = threshold_sauvola(gray_image, window_size=25)
binary_sauvola = gray_image > thresh_sauvola
binary_sauvola = img_as_ubyte(binary_sauvola)
# Trying triangle scikit-image algorithm:
thresh_triangle = threshold_triangle(gray_image)
binary_triangle = gray_image > thresh_triangle
binary_triangle = img_as_ubyte(binary_triangle)
输出可以在下一个屏幕截图中看到:
如您所见,当背景不均匀时,局部阈值方法可以提供更好的结果。 实际上,这些方法可以应用于文本识别。 最后,scikit-image 附带了更多可以尝试的阈值处理技术。 如有必要,请查阅 API 文档,以查看位于这个页面的所有可用方法。
总结
在本章中,我们回顾了可用于对图像进行阈值处理的主要阈值处理技术。 门限技术可用于许多计算机视觉任务(例如,文本识别和图像分割等)。 简单和自适应阈值处理技术都已被审查。 此外,我们已经了解了如何应用大津的二值化算法和三角形算法来自动选择全局阈值以对图像进行阈值处理。 最后,我们看到了如何使用 scikit-image 使用不同的阈值技术。 从这个意义上说,两种全局阈值技术(大津和三角算法)和两种局部阈值技术(Niblack 和 Sauvola 算法)已应用于测试图像。
在第 8 章,“轮廓检测,滤波和图形”中,我们将看到如何处理轮廓,轮廓对于形状分析以及对象检测和识别非常有用。
问题
- 使用具有阈值
100
的cv2.threshold()
和cv2.THRESH_BINARY
阈值类型应用阈值操作。 - 使用
cv2.adapativeThreshold()
,cv2.ADAPTIVE_THRESH_MEAN_C
,C=2
和blockSize=9
应用自适应阈值运算。 - 使用
cv2.THRESH_BINARY
阈值类型应用大津的阈值。 - 使用
cv2.THRESH_BINARY
阈值类型应用三角形阈值。 - 使用 scikit-image 应用大津的阈值。
- 使用 scikit-image 应用三角形阈值。
- 使用 scikit-image 应用 Niblack 的阈值。
- 使用 scikit-image 和
25
的窗口大小应用 Sauvola 的阈值。 - 修改
thresholding_example.py
脚本以使用np.arange()
,目的是定义要应用于cv2.threshold()
函数的阈值。 然后,使用定义的阈值调用cv2.threshold()
函数,并将所有阈值图像存储在数组中。 最后,显示每个调用show_img_with_matplotlib()
的数组中的所有图像。 将脚本重命名为thresholding_example_arange.py
。
进一步阅读
以下参考资料将帮助您深入研究阈值处理和其他图像处理技术:
八、轮廓检测,过滤和绘图
轮廓可以定义为定义图像中对象边界的点序列。 因此,轮廓线传达有关对象边界的关键信息,并对有关对象形状的主要信息进行编码。 该信息用作图像描述符(例如 SIFT,傅立叶描述符或形状上下文等)的基础,并且可用于形状分析以及对象检测和识别。
在本章中,您将看到如何处理轮廓,轮廓用于形状分析以及对象检测和识别。
在本章中,与轮廓相关的关键点将在以下主题中解决:
- 轮廓介绍
- 压缩轮廓
- 图像的矩
- 与轮廓有关的更多函数
- 过滤轮廓
- 识别轮廓
- 匹配轮廓
技术要求
技术要求如下:
- Python 和 OpenCV
- 特定于 Python 的 IDE
- NumPy 和 Matplotlib 包
- Git 客户端
有关如何安装这些要求的更多详细信息,请参见第 1 章,“设置 OpenCV”。 “精通 Python OpenCV 4”的 GitHub 存储库,其中包含所有支持的项目文件,这是从第一章到最后一章学习本书所必需的,可以在下一个 URL 中访问。
轮廓介绍
轮廓可以看作是一条曲线,它沿着特定形状的边界连接所有点。 当它们定义形状的边界时,对这些点的分析可以揭示用于形状分析以及对象检测和识别的关键信息。 OpenCV 提供了许多功能来正确检测和处理轮廓。 但是,在深入探讨这些功能之前,我们将了解样本轮廓的结构。 例如,以下函数模拟检测假设图像中的轮廓:
def get_one_contour():
"""Returns a 'fixed' contour"""
cnts = [np.array(
[[[600, 320]], [[563, 460]], [[460, 562]], [[320, 600]], [[180, 563]], [[78, 460]], [[40, 320]], [[77, 180]], [[179, 78]], [[319, 40]], [[459, 77]], [[562, 179]]], dtype=np.int32)]
return cnts
如您所见,轮廓是由np.int32
类型的许多点组成的数组(整数在[-2147483648, 2147483647]
范围内)。 现在,我们可以调用此函数来获取轮廓数组。 在这种情况下,此数组只有一个detected
轮廓:
contours = get_one_contour()
print("'detected' contours: '{}' ".format(len(contours)))
print("contour shape: '{}'".format(contours[0].shape))
此时,我们可以应用 OpenCV 提供的所有功能来播放轮廓。 请注意,定义get_one_contour()
函数很有趣,因为它为您提供了一种简单的方法来准备使用轮廓,以便调试和测试与轮廓相关的其他功能。 在许多情况下,实际图像中检测到的轮廓具有数百个点,因此很难调试代码。 因此,请随时使用此函数。
为了完成对轮廓的介绍,OpenCV 提供了cv2.drawContours()
,它可以在图像中绘制轮廓轮廓。 因此,我们可以调用该函数来查看轮廓。 此外,我们还对draw_contour_points()
函数进行了编码,该函数在图像中绘制轮廓点。 同样,我们已经使用np.squeeze()
函数来摆脱一维数组,例如使用[1,2,3]
而不是[[[1,2,3]]]
。 例如,如果打印上一个函数中定义的轮廓,则将得到以下内容:
[[[600 320]]
[[563 460]]
[[460 562]]
[[320 600]]
[[180 563]]
[[ 78 460]]
[[ 40 320]]
[[ 77 180]]
[[179 78]]
[[319 40]]
[[459 77]]
[[562 179]]]
执行以下代码行之后:
squeeze = np.squeeze(cnt)
如果打印squeeze
,将得到以下输出:
[[600 320]
[563 460]
[460 562]
[320 600]
[180 563]
[ 78 460]
[ 40 320]
[ 77 180]
[179 78]
[319 40]
[459 77]
[562 179]]
此时,我们可以遍历此数组的所有点。
因此,draw_contour_points()
函数的代码如下:
def draw_contour_points(img, cnts, color):
"""Draw all points from a list of contours"""
for cnt in cnts:
squeeze = np.squeeze(cnt)
for p in squeeze:
p = array_to_tuple(p)
cv2.circle(img, p, 10, color, -1)
return img
另一个考虑因素是,在先前的函数中,我们使用了array_to_tuple()
函数,该函数将数组转换为元组:
def array_to_tuple(arr):
"""Converts array to tuple"""
return tuple(arr.reshape(1, -1)[0])
这样,轮廓的第一个点[600 320]
转换为(600, 320)
,可以在cv2.circle()
内部将其用作中心。 可以在contours_introduction.py
中看到有关轮廓的先前介绍的完整代码。 下一个屏幕截图中可以看到此脚本的输出:
为了完成对轮廓的介绍,我们还编写了脚本contours_introduction_2.py
。 在这里,我们已经编码了函数build_sample_image()
和build_sample_image_2()
。 这些函数在图像中绘制基本形状,其目的是提供一些可预测(或预定义)的形状。
这两个函数与上一个脚本中定义的get_one_contour()
函数具有相同的目的,即,它们有助于我们理解与轮廓有关的关键概念。 build_sample_image()
函数的代码如下:
def build_sample_image():
"""Builds a sample image with basic shapes"""
# Create a 500x500 gray image (70 intensity) with a rectangle and a circle inside:
img = np.ones((500, 500, 3), dtype="uint8") * 70
cv2.rectangle(img, (100, 100), (300, 300), (255, 0, 255), -1)
cv2.circle(img, (400, 400), 100, (255, 255, 0), -1)
return img
如您所见,此函数绘制两个填充的形状(一个矩形和一个圆形)。 因此,此函数创建具有两个(外部)轮廓的图像。 build_sample_image_2()
函数的代码如下:
def build_sample_image_2():
"""Builds a sample image with basic shapes"""
# Create a 500x500 gray image (70 intensity) with a rectangle and a circle inside (with internal contours):
img = np.ones((500, 500, 3), dtype="uint8") * 70
cv2.rectangle(img, (100, 100), (300, 300), (255, 0, 255), -1)
cv2.rectangle(img, (150, 150), (250, 250), (70, 70, 70), -1)
cv2.circle(img, (400, 400), 100, (255, 255, 0), -1)
cv2.circle(img, (400, 400), 50, (70, 70, 70), -1)
此函数绘制两个填充的矩形(一个在另一个内部)和两个填充的圆(一个在另一个内部)。 此函数创建具有两个外部轮廓和两个内部轮廓的图像。
在contours_introduction_2.py
中,在将图像加载之后,我们将其转换为灰度并设置了阈值以获得二进制图像。 此二进制图像稍后将用于使用cv2.findContours()
函数查找轮廓。 如前所述,创建的图像仅具有圆形和正方形。 因此,调用cv2.findContours()
将找到所有这些创建的轮廓。 cv2.findContours()
方法的签名如下:
cv2.findContours(image, mode, method[, contours[, hierarchy[, offset]]]) -> image, contours, hierarchy
OpenCV 提供cv2.findContours()
,可用于检测二进制图像(例如,阈值运算后生成的图像)中的轮廓。 该函数实现了中通过边界进行数字化二进制图像的拓扑结构分析中定义的算法。 应当注意,在 OpenCV 3.2 之前,源图像将已被修改,并且自 OpenCV 3.2 起,在调用此函数后将不再修改源图像。 源图像被视为二进制图像,其中非零像素被视为 1。 该函数返回检测到的轮廓,每个轮廓包含所有检索到的定义边界的点。
检索到的轮廓可以以不同的模式输出-cv2.RETR_EXTERNAL
(仅在轮廓外部输出),cv2.RETR_LIST
(不带任何层次关系输出所有轮廓)和cv2.RETR_TREE
(通过建立层次关系输出所有轮廓) 。 输出向量hierarchy
包含有关此层次关系的信息,为每个检测到的轮廓提供一个条目。 对于每个第i
个轮廓contours[i]
,hierarchy[i][j]
和j
在[0,3]
范围内的轮廓包含以下内容:
hierarchy[i][0]
:同一层级上的下一个轮廓的索引hierarchy[i][1]
:在相同层次级别上的先前轮廓的索引hierarchy[i][2]
:第一个子轮廓的索引hierarchy[i][3]
:父轮廓的索引
hierarchy[i][j]
中的负值表示没有下一个轮廓j=0
,没有上一个轮廓j=1
,没有子轮廓j=2
或没有父轮廓j=3
。 最后,method
参数设置检索与每个检测到的轮廓有关的点时使用的近似方法。 下一部分将进一步说明此参数。
如果执行contours_introduction_2.py
脚本,则可以看到以下屏幕:
在此屏幕截图中,通过调用cv2.findContours()
来计算外部(cv2.RETR_EXTERNAL
)和外部和内部(cv2.RETR_LIST
)。
压缩轮廓
检测到的轮廓可以压缩以减少点数。 从这个意义上讲,OpenCV 提供了几种减少点数的方法。 可以使用参数method
进行设置。 另外,可以通过将标志设置为cv2.CHAIN_APPROX_NONE
(所有边界点都存储在其中)来禁用此压缩; 因此,不执行压缩。
cv2.CHAIN_APPROX_SIMPLE
方法可用于压缩检测到的轮廓,因为它压缩轮廓的水平,垂直和对角线部分,仅保留端点。 例如,如果我们使用cv2.CHAIN_APPROX_SIMPLE
压缩矩形的轮廓,则它将仅由四个点组成。
最后,OpenCV 提供了另外两个基于 Teh-Chin 算法的轮廓压缩标志,这是一种非参数方法。 该算法的第一步是根据每个点的局部属性确定其支持区域(ROS)。
接下来,该算法计算每个点的相对重要性的度量。 最后,通过非极大值抑制过程来检测优势点。 他们使用三种不同的有效度量,分别对应于离散曲率度量的不同精确度:
- 余弦度量
- K 曲率度量
- 一种曲率度量(
2
的k = 1
)
因此,结合离散曲率量度,OpenCV 提供了两个标记-cv2.CHAIN_APPROX_TC89_L1
和cv2.CHAIN_APPROX_TC89_KCOS
。 有关此算法的更详细说明,请参见出版物《关于检测数字曲线上的优势点》(1989)。 只是为了澄清起见,_CT89_
对该名称的作者(Teh 和 Chin)的首字母以及出版年份(1989)进行了编码。
在contours_approximation_method.py
中,用于method
参数的上述四个标记(cv2.CHAIN_APPROX_NONE
,cv2.CHAIN_APPROX_SIMPLE
,cv2.CHAIN_APPROX_TC89_L1
和cv2.CHAIN_APPROX_TC89_KCOS
)用于编码图像中的两个检测到的轮廓。 下一个屏幕截图中可以看到此脚本的输出:
可以看出,定义轮廓的点以白色显示,显示了四种方法(cv2.CHAIN_APPROX_NONE
,cv2.CHAIN_APPROX_SIMPLE
,cv2.CHAIN_APPROX_TC89_L1
和cv2.CHAIN_APPROX_TC89_KCOS
)如何压缩两个提供形状的检测轮廓。
图像的矩
在数学中,矩可以看作是函数形状的特定定量度量。 图像矩可以看作是图像像素强度的加权平均值,或者是此类矩的函数,可以对某些有趣的属性进行编码。 从这个意义上讲,图像矩可用于描述检测到的轮廓的某些属性(例如,对象的质心或对象的面积等)。
cv2.moments()
可用于计算直到向量形状或栅格化形状的三阶的所有矩。
此方法的签名如下:
retval = cv.moments(array[, binaryImage])
因此,为了计算检测到的轮廓(例如,第一个检测到的轮廓)的矩,请执行以下操作:
M = cv2.moments(contours[0])
如果我们打印M
,则会得到以下信息:
{'m00': 235283.0, 'm10': 75282991.16666666, 'm01': 75279680.83333333, 'm20': 28496148988.333332, 'm11': 24089788592.25, 'm02': 28492341886.0, 'm30': 11939291123446.25, 'm21': 9118893653727.8, 'm12': 9117775940692.967, 'm03': 11936167227424.852, 'mu20': 4408013598.184406, 'mu11': 2712402.277420044, 'mu02': 4406324849.628765, 'mu30': 595042037.7265625, 'mu21': -292162222.4824219, 'mu12': -592577546.1586914, 'mu03': 294852334.5449219, 'nu20': 0.07962727021646843, 'nu11': 4.8997396280458296e-05, 'nu02': 0.07959676431294238, 'nu30': 2.2160077537124397e-05, 'nu21': -1.0880470778779139e-05, 'nu12': -2.2068296922023203e-05, 'nu03': 1.0980653771087236e-05}
如您所见,存在三种不同类型的矩(m[ji], mu[ji], nu[ji])
。
如下计算空间矩m[ji]
:
中心矩mu[ji]
的计算如下:
在此适用以下条件:
前面的等式对应于质心。
根据定义,中心矩对于翻译而言是不变的。 因此,中心矩适合描述物体的形式。 然而,空间和中心矩的缺点是它们依赖于物体的大小。 它们不是尺度不变的。
归一化中心矩nu[ji]
的计算如下:
归一化的中心矩从定义上来说是平移和尺度不变的。
接下来的矩值计算如下:
mu[00] = m[00], nu[00] = 1, nu[10] = mu[10] = mu[01] = mu[10] = 0
因此,这些矩不被存储。
通常根据矩的顺序对矩进行分类,矩的阶数是基于矩m[ji]
的索引j, i
的总和(j + i
)来计算的。
在接下来的小节中,将提供有关图像矩的更多信息。 更具体地,将基于矩来计算一些物体特征(例如,中心,偏心率或轮廓的面积等)。 此外,还将看到胡矩不变式。 最后,还介绍了 Zernike 矩。
一些基于矩的对象特征
如前所述,矩是根据轮廓计算的特征,允许对对象进行几何重构。 尽管没有直接可理解的几何含义,但是可以基于矩来计算一些有趣的几何属性和参数。
在contours_analysis.py
中,我们将首先计算检测到的轮廓的矩,然后,将计算一些物体特征:
M = cv2.moments(contours[0])
print("Contour area: '{}'".format(cv2.contourArea(contours[0])))
print("Contour area: '{}'".format(M['m00']))
如您所见,矩m00
给出轮廓的面积,该面积等于函数cv2.contourArea()
。 为了计算轮廓的质心,必须执行以下操作:
print("center X : '{}'".format(round(M['m10'] / M['m00'])))
print("center Y : '{}'".format(round(M['m01'] / M['m00'])))
圆度 κ
是轮廓接近完美圆轮廓的量度。 轮廓的圆度可以根据以下公式计算:
P
是轮廓的周长,A
是相应的面积。 在正圆的情况下,结果为1
; 获得的值越高,则圆形越小。
可以使用roundness()
函数来计算:
def roundness(contour, moments):
"""Calculates the roundness of a contour"""
length = cv2.arcLength(contour, True)
k = (length * length) / (moments['m00'] * 4 * np.pi)
return k
偏心率(也称为伸长率)是轮廓可以伸长的量度。 偏心率ε
可以直接根据对象的半长轴和半短轴a
和b
得出,其公式如下:
因此,一种计算轮廓的偏心率的方法是计算适合轮廓的椭圆,然后从所计算的椭圆中导出a
和b
。 最后,根据上式计算ε
。
下一个代码执行此操作:
def eccentricity_from_ellipse(contour):
"""Calculates the eccentricity fitting an ellipse from a contour"""
(x, y), (MA, ma), angle = cv2.fitEllipse(contour)
a = ma / 2
b = MA / 2
ecc = np.sqrt(a ** 2 - b ** 2) / a
return ecc
另一种方法是通过下一个公式使用轮廓矩来计算偏心率:
这可以通过eccentricity_from_moments()
执行:
def eccentricity_from_moments(moments):
"""Calculates the eccentricity from the moments of the contour"""
a1 = (moments['mu20'] + moments['mu02']) / 2
a2 = np.sqrt(4 * moments['mu11'] ** 2 + (moments['mu20'] - moments['mu02']) ** 2) / 2
ecc = np.sqrt(1 - (a1 - a2) / (a1 + a2))
return ecc
为了完成可用于描述轮廓的特征,可以计算其他属性。 例如,可以基于最小边界矩形的尺寸(使用cv2.boundingRect()
计算)轻松计算出宽高比。 长宽比是轮廓的边界矩形的宽度与高度之比:
def aspect_ratio(contour):
"""Returns the aspect ratio of the contour based on the dimensions of the bounding rect"""
x, y, w, h = cv2.boundingRect(contour)
res = float(w) / h
return res
如前所述,所有这些属性都是在contours_analysis.py
脚本中计算的。 下一个屏幕截图中可以看到此脚本的输出:
在上一个屏幕截图中,通过打印脚本中计算出的所有属性来显示轮廓分析。
在前面的示例中,仅使用直到第二阶的矩来计算简单的对象特征。 为了更精确地描述复杂的对象,应使用高阶矩或更复杂的矩(例如 Zernike,Legendre)。 从这个意义上说,对象越复杂,应该计算出矩的阶次越高,以最小化从矩重构对象的误差。 有关更多信息,请参见《通过矩的简单图像分析》。
为了完成本节,还对脚本contours_ellipses.py
进行了编码。 在此脚本中,我们首先构建要使用的图像。 在这种情况下,图像中会绘制不同的椭圆。 这是通过build_image_ellipses()
执行的。 在这种情况下,使用 OpenCV 函数cv2.ellipse()
绘制了六个椭圆。 之后,在阈值图像中检测绘制的椭圆的轮廓,并计算一些特征。 更具体地,计算圆度和偏心度。 在结果图像中,仅显示了偏心率。
在下一个屏幕截图中,可以看到此脚本的输出。 如您所见,偏心率值绘制在每个轮廓的质心的中心。 此功能通过函数get_position_to_draw()
执行:
def get_position_to_draw(text, point, font_face, font_scale, thickness):
"""Gives the coordinates to draw centered"""
text_size = cv2.getTextSize(text, font_face, font_scale, thickness)[0]
text_x = point[0] - text_size[0] / 2
text_y = point[1] + text_size[1] / 2
return round(text_x), round(text_y)
此函数返回x
,y
坐标以绘制point
为中心的位置text
,并绘制text
的特定特征,这些特征对于计算[ text
size-字体由参数font_face
设置,字体比例由参数font_scale
设置,粗细由参数thickness
设置。
下一个屏幕截图中可以看到此脚本的输出:
可以看出,显示了使用上述函数eccentricity_from_moments()
计算的偏心率。 应该注意的是,我们已经使用两个提供的公式计算了偏心率,获得了非常相似的结果。
胡矩不变量
胡矩不变量相对于平移,缩放和旋转是不变的,并且所有矩(第七个矩除外)对于反射都是不变的。 在第七种情况下,符号已通过反射进行了更改,从而使其能够区分图像。 OpenCV 提供cv2.HuMoments()
来计算七个胡矩不变量。
此方法的签名如下:
cv2.HuMoments(m[, hu]) → hu
在此,m
对应于使用cv2.moments()
计算的矩。 输出hu
对应于七个胡矩不变量。
七个胡矩不变量定义如下:
η[ji]
代表nu[ji]
。
在contours_hu_moments.py
脚本中,计算了七个胡矩不变量。 如前所述,我们必须首先使用cv2.moments()
计算矩。 为了计算矩,该参数可以既是向量形状又是图像。 此外,如果binaryImage
参数为true
(仅用于图像),则输入图像中的所有非零像素将被视为 1。 在此脚本中,我们同时使用向量形状和图像来计算弯矩。 最后,利用计算出的矩,我们将计算出胡矩不变性。
接下来说明键码。 我们首先加载图像,将其转换为灰度,然后应用cv2.threshold()
获得二进制图像:
# Load the image and convert it to grayscale:
image = cv2.imread("shape_features.png")
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# Apply cv2.threshold() to get a binary image
ret, thresh = cv2.threshold(gray_image, 70, 255, cv2.THRESH_BINARY)
在这一点上,我们通过使用阈值图像来计算矩。 之后,计算质心,最后计算出胡矩不变量:
# Compute moments:
M = cv2.moments(thresh, True)
print("moments: '{}'".format(M))
# Calculate the centroid of the contour based on moments:
x, y = centroid(M)
# Compute Hu moments:
HuM = cv2.HuMoments(M)
print("Hu moments: '{}'".format(HuM))
现在,我们重复该过程,但是在这种情况下,将传递轮廓而不是二进制图像。 因此,我们首先计算二进制图像中轮廓的坐标:
# Find contours
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
# Compute moments:
M2 = cv2.moments(contours[0])
print("moments: '{}'".format(M2))
# Calculate the centroid of the contour based on moments:
x2, y2 = centroid(M2)
# Compute Hu moments:
HuM2 = cv2.HuMoments(M2)
print("Hu moments: '{}'".format(HuM2))
最后,质心显示如下:
print("('x','y'): ('{}','{}')".format(x, y))
print("('x2','y2'): ('{}','{}')".format(x2, y2))
如您所见,计算出的矩,胡矩不变性和质心非常相似,但并不相同。 例如,获得的质心如下:
('x','y'): ('613','271')
('x2','y2'): ('613','270')
如您所见,y
坐标相差一个像素。 这样做的原因是光栅分辨率有限。 为轮廓估计的矩与为相同栅格化轮廓计算的矩略有不同。 在下一个屏幕截图中可以看到此脚本的输出,其中显示了两个质心,以突出显示y
坐标中的这一小差异:
在contours_hu_moments_properties.py
中,我们加载了三个图像。 第一个是原始的。 第二个与原件相对应,但旋转了 180 度。 第三个对应于原件的垂直反射。 这可以在脚本的输出中看到。 此外,我们打印从上述三个图像得出的计算得出的胡矩不变量。
该脚本的第一步是使用cv2.imread()
加载图像,并使用cv2.cvtColor()
将其转换为灰度。 第二步是应用cv2.threshold()
获得二进制图像。 最后,使用cv2.HuMoments()
计算胡矩:
# Load the images (cv2.imread()) and convert them to grayscale (cv2.cvtColor()):
image_1 = cv2.imread("shape_features.png")
image_2 = cv2.imread("shape_features_rotation.png")
image_3 = cv2.imread("shape_features_reflection.png")
gray_image_1 = cv2.cvtColor(image_1, cv2.COLOR_BGR2GRAY)
gray_image_2 = cv2.cvtColor(image_2, cv2.COLOR_BGR2GRAY)
gray_image_3 = cv2.cvtColor(image_3, cv2.COLOR_BGR2GRAY)
# Apply cv2.threshold() to get a binary image:
ret_1, thresh_1 = cv2.threshold(gray_image_1, 70, 255, cv2.THRESH_BINARY)
ret_2, thresh_2 = cv2.threshold(gray_image_2, 70, 255, cv2.THRESH_BINARY)
ret_2, thresh_3 = cv2.threshold(gray_image_3, 70, 255, cv2.THRESH_BINARY)
# Compute Hu moments cv2.HuMoments():
HuM_1 = cv2.HuMoments(cv2.moments(thresh_1, True)).flatten()
HuM_2 = cv2.HuMoments(cv2.moments(thresh_2, True)).flatten()
HuM_3 = cv2.HuMoments(cv2.moments(thresh_3, True)).flatten()
# Show calculated Hu moments for the three images:
print("Hu moments (original): '{}'".format(HuM_1))
print("Hu moments (rotation): '{}'".format(HuM_2))
print("Hu moments (reflection): '{}'".format(HuM_3))
# Plot the images:
show_img_with_matplotlib(image_1, "original", 1)
show_img_with_matplotlib(image_2, "rotation", 2)
show_img_with_matplotlib(image_3, "reflection", 3)
# Show the Figure:
plt.show()
计算出的色相矩不变性如下:
Hu moments (original): '[ 1.92801772e-01 1.01173781e-02 5.70258405e-05 1.96536742e-06 2.46949980e-12 -1.88337981e-07 2.06595472e-11]'
Hu moments (rotation): '[ 1.92801772e-01 1.01173781e-02 5.70258405e-05 1.96536742e-06 2.46949980e-12 -1.88337981e-07 2.06595472e-11]'
Hu moments (reflection): '[ 1.92801772e-01 1.01173781e-02 5.70258405e-05 1.96536742e-06 2.46949980e-12 -1.88337981e-07 -2.06595472e-11]'
您可以看到,除了第七种情况外,三种情况下计算出的胡矩不变性都相同。 在之前显示的输出中,此差异以粗体突出显示。 如您所见,符号已更改。
以下屏幕截图显示了用于计算胡矩不变性的三个图像:
Zernike 矩
自从引入胡矩以来,矩就已用于图像处理以及对象分类和识别。 从该出版物开始,已经开发了与矩有关的更强大的矩技术。
一个典型的例子是 Zernike 矩。 蒂格基于正交 Zernike 多项式的基础集提出了 Zernike 矩。 OpenCV 不提供计算 Zernike 矩的函数。 但是,其他 Python 包也可以用于此目的。
从这个意义上讲,mahotas
包提供了zernike_moments()
函数,该函数可用于计算 Zernike 矩。 zernike_moments()
的签名如下:
mahotas.features.zernike_moments(im, radius, degree=8, cm={center_of_mass(im)})
此函数计算以cm
为中心(如果不使用cm
则为图像质心)的radius
圆上的 Zernike 矩。 使用的最大程度由degree
设置(默认为8
)。
例如,如果使用默认值,则可以如下计算 Zernike 矩:
moments = mahotas.features.zernike_moments(image, 21)
在这种情况下,使用21
的半径。 Zernike 矩特征向量具有 25 维。
与轮廓有关的更多函数
到目前为止,我们已经看到了一些来自图像矩的轮廓属性(例如,质心,面积,圆度或偏心距等)。 此外,OpenCV 提供了一些与轮廓有关的有趣功能,这些功能也可以用于进一步描述轮廓。
在contours_functionality.py
中,我们主要使用五个与轮廓相关的 OpenCV 函数和一个计算给定轮廓的极值的函数。
在描述每个函数的计算结果之前,最好先显示此脚本的输出,因为生成的图像可以帮助我们理解上述每个函数:
cv2.boundingRect()
返回包含轮廓的所有点的最小边界矩形:
x, y, w, h = cv2.boundingRect(contours[0])
cv2.minAreaRect()
返回包含轮廓的所有点的最小旋转(如果需要)的矩形:
rotated_rect = cv2.minAreaRect(contours[0])
为了提取旋转矩形的四个点,可以使用cv2.boxPoints()
函数,该函数返回旋转矩形的四个顶点:
box = cv2.boxPoints(rotated_rect)
cv2.minEnclosingCircle()
返回包含轮廓的所有点的最小圆(它返回中心和半径):
(x, y), radius = cv2.minEnclosingCircle(contours[0])
cv2.fitEllipse()
返回符合(具有最小的最小平方误差)轮廓的所有点的椭圆:
ellipse = cv2.fitEllipse(contours[0])
cv2.approxPolyDP()
根据给定的精度返回给定轮廓的轮廓近似值。 此函数使用 Douglas-Peucker 算法。
epsilon
参数确定精度,确定原始曲线与其近似之间的最大距离。 因此,所得轮廓是与给定轮廓相似的抽取轮廓,其点更少:
approx = cv2.approxPolyDP(contours[0], epsilon, True)
extreme_points()
计算定义给定轮廓的四个极限点:
def extreme_points(contour):
"""Returns extreme points of the contour"""
index_min_x = contour[:, :, 0].argmin()
index_min_y = contour[:, :, 1].argmin()
index_max_x = contour[:, :, 0].argmax()
index_max_y = contour[:, :, 1].argmax()
extreme_left = tuple(contour[index_min_x][0])
extreme_right = tuple(contour[index_max_x][0])
extreme_top = tuple(contour[index_min_y][0])
extreme_bottom = tuple(contour[index_max_y][0])
return extreme_left, extreme_right, extreme_top, extreme_bottom
np.argmin()
返回沿轴的最小值的索引。 在多次出现最小值的情况下,返回与第一次出现相对应的索引。 np.argmax()
返回最大值的索引。 一旦计算出索引(例如index
),我们将获得数组的相应组件(例如contour[index]
-[[ 40 320]]
),然后访问第一个组件(例如contour[index][0]
-[ 40 320]
)。 最后,我们将其转换为元组(例如tuple(contour[index][0])
-(40,320)
)。
如您所见,您可以以更紧凑的方式执行这些计算:
index_min_x = contour[:, :, 0].argmin()
extreme_left = tuple(contour[index_min_x][0])
该代码可以重写如下:
extreme_left = tuple(contour[contour[:, :, 0].argmin()][0])
过滤轮廓
在前面的部分中,我们已经看到了如何计算检测到的轮廓的大小。 可以根据图像矩或使用 OpenCV 函数cv2.contourArea()
计算检测到的轮廓的大小。 在此示例中,我们将基于每个轮廓的计算大小对检测到的轮廓进行排序。
因此,sort_contours_size()
函数是关键:
def sort_contours_size(cnts):
""" Sort contours based on the size"""
cnts_sizes = [cv2.contourArea(contour) for contour in cnts]
(cnts_sizes, cnts) = zip(*sorted(zip(cnts_sizes, cnts)))
return cnts_sizes, cnts
在解释该代码的功能之前,我们将介绍一些关键点。 *
运算符可以与zip()
结合使用以解压缩列表:
coordinate = ['x', 'y', 'z']
value = [5, 4, 3]
result = zip(coordinate, value)
print(list(result))
c, v = zip(*zip(coordinate, value))
print('c =', c)
print('v =', v)
输出如下:
[('x', 5), ('y', 4), ('z', 3)]
c = ('x', 'y', 'z')
v = (5, 4, 3)
让我们合并sorted
函数:
coordinate = ['x', 'y', 'z']
value = [5, 4, 3]
print(sorted(zip(value, coordinate)))
c, v = zip(*sorted(zip(value, coordinate)))
print('c =', c)
print('v =', v)
输出如下:
[(3, 'z'), (4, 'y'), (5, 'x')]
c = (3, 4, 5)
v = ('z', 'y', 'x')
因此,sort_contours_size()
函数根据尺寸对轮廓进行分类。 同样,脚本在轮廓的中心输出序号。 contours_sort_size.py
的输出可以在下一个屏幕截图中看到:
如您所见,在屏幕截图的上部显示了原始图像,而在屏幕截图的下部显示了原始图像,以在每个轮廓的中心包括序号。
识别轮廓
我们先前已经介绍了cv2.approxPolyDP()
,使用道格拉斯-皮克算法(Douglas-Peucker algorithm)可以用较少的点将一个轮廓与另一个轮廓近似。 此函数的关键参数是epsilon
,它设置近似精度。 在contours_shape_recognition.py
中,我们将使用cv2.approxPolyDP()
来基于抽取的轮廓中检测到的顶点数量(例如三角形,正方形,矩形,五边形或六边形等)识别轮廓。 cv2.approxPolyDP()
的输出)。 为了减少给定轮廓的点数,我们首先计算轮廓的周长。 基于周长,将建立epsilon
参数。 这样,抽取的轮廓就不会缩放。 ε
参数的计算如下:
epsilon = 0.03 * perimeter
常数0.03
经过多次测试后建立。 例如,如果该常数较大(例如0.1
),则ε
参数也将较大,因此近似精度将降低。
这导致轮廓具有更少的点,并且获得了丢失的顶点。 因此,轮廓识别不正确,因为它基于检测到的顶点数。 另一方面,如果该常数较小(例如0.001
),则ε
参数也将较小,因此近似精度将增加,从而导致具有更多点的近似轮廓。 在这种情况下,轮廓的识别也被错误地执行,因为获得了错误的顶点。
下一个屏幕截图中可以看到contours_shape_recognition.py
脚本的输出:
在上一个屏幕截图中,显示了关键步骤(阈值,轮廓近似和轮廓识别)。
匹配轮廓
胡矩不变量可用于对象匹配和识别。 在本节中,我们将了解如何基于胡矩不变性来匹配轮廓。 OpenCV 提供cv2.matchShapes()
,可以使用三种比较方法来比较两个轮廓。 所有这些方法都使用胡矩不变式。 三种实现的方法是cv2.CONTOURS_MATCH_I1
,cv2.CONTOURS_MATCH_I2
和cv2.CONTOURS_MATCH_I3
。
如果A
表示第一个对象,B
表示第二个对象,则以下条件适用:
分别是
A
和B
的胡矩。
最后,请参阅以下内容:
cv2.CONTOURS_MATCH_I1
:
cv2.CONTOURS_MATCH_I2
:
cv2.CONTOURS_MATCH_I3
:
在contours_matching.py
中,我们利用cv2.matchShapes()
将几个轮廓与一个完美的圆轮廓匹配。
首先,我们使用 OpenCV 函数cv2.circle()
在图像中绘制一个完美的圆。 这将是参考图像。 为了构建此图像,调用build_circle_image()
。 然后,我们加载图像match_shapes.png
,其中绘制了许多不同的形状。 一旦准备好两个图像,下一步就是在上述两个图像的每一个中找到轮廓:
- 使用
cv2.cvtColor()
将其转换为灰度 - 使用
cv2.threshold()
将其二值化 - 使用
cv2.findContours()
查找轮廓
此时,我们准备将从match_shapes.png
提取的所有轮廓与从使用build_circle_image()
函数构建的图像中提取的轮廓进行比较:
for contour in contours:
# Compute the moment of contour:
M = cv2.moments(contour)
# The center or centroid can be calculated as follows:
cX = int(M['m10'] / M['m00'])
cY = int(M['m01'] / M['m00'])
# We match each contour against the circle contour using the three matching modes:
ret_1 = cv2.matchShapes(contours_circle[0], contour, cv2.CONTOURS_MATCH_I1, 0.0)
ret_2 = cv2.matchShapes(contours_circle[0], contour, cv2.CONTOURS_MATCH_I2, 0.0)
ret_3 = cv2.matchShapes(contours_circle[0], contour, cv2.CONTOURS_MATCH_I3, 0.0)
# Get the positions to draw:
(x_1, y_1) = get_position_to_draw(str(round(ret_1, 3)), (cX, cY), cv2.FONT_HERSHEY_SIMPLEX, 1.2, 3)
(x_2, y_2) = get_position_to_draw(str(round(ret_2, 3)), (cX, cY), cv2.FONT_HERSHEY_SIMPLEX, 1.2, 3)
(x_3, y_3) = get_position_to_draw(str(round(ret_3, 3)), (cX, cY), cv2.FONT_HERSHEY_SIMPLEX, 1.2, 3)
# Write the obtainted scores in the result images:
cv2.putText(result_1, str(round(ret_1, 3)), (x_1, y_1), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (255, 0, 0), 3)
cv2.putText(result_2, str(round(ret_2, 3)), (x_2, y_2), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 255, 0), 3)
cv2.putText(result_3, str(round(ret_3, 3)), (x_3, y_3), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 0, 255), 3)
在以下屏幕截图中可以看到contours_matching.py
脚本的输出:
可以看出,图像result_1
使用匹配模式cv2.CONTOURS_MATCH_I1
显示匹配分数,图像result_2
使用匹配模式cv2.CONTOURS_MATCH_I2
显示匹配分数,最后result_3
使用匹配模式result_3
显示匹配分数result_1
cv2.CONTOURS_MATCH_I3
。
总结
在本章中,我们回顾了 OpenCV 提供的与轮廓相关的主要功能。 此外,在比较和描述轮廓时,我们还编写了一些有用的函数。 此外,我们还提供了一些有趣的功能,这些功能在调试代码时很有用。 从这个意义上讲,提供了用于创建缩小轮廓和创建具有简单形状的图像的函数。 在本章中,我们完成了与图像处理技术有关的四章-第 5 章,“图像处理技术”,回顾了图像处理的关键点; 第 6 章,“构造和构建直方图”,介绍了直方图; 第 7 章,“阈值技术”涵盖了阈值技术; 最后,在本章中,我们解释了如何处理轮廓。
在下一章中,我们将提供对增强现实的介绍,这是当前最热门的趋势之一,可以定义为增强的现实版本,通过叠加计算机生成的元素可以增强对现实世界的看法。
问题
- 如果要检测二进制图像中的轮廓,应该使用什么函数?
- OpenCV 提供哪些四个标志来压缩轮廓?
- OpenCV 提供什么函数来计算图像矩?
- 什么矩提供轮廓的大小?
- OpenCV 提供什么函数来计算七个胡矩不变量?
- 如果要获得给定轮廓的轮廓近似值,应该使用什么函数?
- 如本章所述,可以以更紧凑的方式覆盖
contour_functionality.py
脚本中定义的extreme_points()
函数。 因此,请相应地覆盖它。 - 如果要使用胡矩不变量作为特征来匹配轮廓,应该使用什么函数?
进一步阅读
以下参考资料将帮助您更深入地研究轮廓和其他图像处理技术:
九、增强现实
增强现实是目前最热门的趋势之一。 增强现实的概念可以定义为现实的改进版本,其中通过叠加的计算机生成元素(例如,图像,视频或 3D 模型等)增强对现实世界的看法。 为了覆盖和集成数字信息(增强现实),可以使用不同类型的技术,主要是基于位置的方法和基于识别的方法。
在本章中,我们将介绍与增强现实相关的主要概念,还将对一些有趣的应用进行编码,以了解该技术的潜力。 在本章中,您将学习如何构建第一个增强现实应用。 在本章的最后,您将掌握使用 OpenCV 创建增强现实应用的知识。
本章的主要部分如下:
- 增强现实简介
- 基于无标记的增强现实
- 基于标记的增强现实
- 基于 Snapchat 的增强现实
- QR 码检测
技术要求
技术要求在这里列出:
- Python 和 OpenCV
- 特定于 Python 的 IDE
- NumPy 和 Matplotlib 包
- Git 客户端
有关如何安装这些要求的更多详细信息,请参见第 1 章,“设置 OpenCV”。
《精通 Python OpenCV 4》的 GitHub 存储库,包含从第一章到最后一章的所有本书所需的支持项目文件,可以在下一个 URL 中访问。
增强现实简介
基于位置和基于识别的增强现实是增强现实的两种主要类型。 两种类型都尝试得出用户正在寻找的位置。 该信息是增强现实过程中的关键,并且依赖于正确计算相机姿态估计。 为了完成此任务,以下简要描述了两种类型:
- 基于位置的增强现实依赖于通过从多个传感器中读取数据来检测用户的位置和方向,这些传感器在智能手机设备中非常常见(例如 GPS,数字罗盘和加速度计),以得出正在搜索的用户的位置。 此信息用于在屏幕上叠加计算机生成的元素。
- 另一方面,基于识别的增强现实使用图像处理技术来推导用户正在看的地方。 从图像获得照相机姿势需要找到环境中的已知点与其对应的照相机投影之间的对应关系。 为了找到这些对应关系,可以在文献中找到两种主要方法:
- 基于标记的姿势估计:此方法依赖于使用平面标记(基于方形标记的标记已经获得普及,尤其是在增强现实领域),从四个角计算相机姿势。 使用正方形标记的一个主要缺点是与相机姿态的计算有关,这取决于对标记的四个角的精确确定。 在阻塞的情况下,该任务可能非常困难。 但是,一些基于标记检测的方法也可以很好地处理遮挡。 ArUco 就是这种情况。
- 基于无标记的姿势估计:当无法使用标记来准备场景以导出姿势估计时,可以将图像中自然存在的对象用于姿势估计。 一旦计算出一组
n
个 2D 点及其对应的 3D 坐标,就可以通过求解透视 N 点(PnP)问题。 由于这些方法依赖于点匹配技术,因此很少会排除输入数据的异常值。 这就是为什么可以在姿态估计过程中使用针对异常值的强大技术(例如 RANSAC)的原因。
在下一个屏幕截图中,结合图像处理技术显示了上述两种方法(基于标记的和基于无标记的增强现实):
在前面的屏幕快照的左侧,您可以看到一个基于标记的方法的示例,该标记用于从四个角计算摄像机的姿势。 此外,在右侧,您可以看到基于无标记方法的示例,其中 50 欧元的钞票用于计算相机姿态。 以下各节将说明这两种方法。
基于无标记的增强现实
如前所述,可以从图像中得出相机姿态估计值,以找到环境中已知点与其相机投影之间的对应关系。 在本节中,我们将看到如何从图像中提取特征以导出相机姿势。 基于这些特征及其匹配,我们将看到如何最终得出相机姿态估计,然后将其用于覆盖和整合数字信息。
特征检测
可以将特征描述为图像中的一个小块,这对于图像缩放,旋转和照明是不变的(尽可能多)。 这样,可以从具有不同视角的同一场景的不同图像中检测到同一特征。 因此,一个好的特征应该是:
- 可重复且精确(应从同一对象的不同图像中提取相同特征)
- 区别于图像(具有不同结构的图像将不具有此特征)
OpenCV 提供了许多算法和技术来检测图像中的特征。 其中包括:
- 哈里斯角点检测
- Shi-Tomasi 角点检测
- 尺度不变特征变换(SIFT)
- 加速鲁棒特征(SURF)
- 来自加速段测试的特征(FAST)
- 二进制鲁棒独立基本特征(BRIEF)
- 定向的 FAST 和旋转的 BRIEF(ORB)
在feature_detection.py
脚本中,我们将使用 ORB 进行图像中的特征检测和描述。 该算法来自 OpenCV Labs,在出版物《ORB:SIFT 或 SURF 的有效替代品》(2011)中进行了描述。 ORB 基本上是 FAST 关键点检测器和 BRIEF 描述符的组合,并进行了关键修改以增强表现。 第一步是检测keypoints
。
ORB 使用修改后的FAST-9
(带有radius = 9
像素的圆圈,并存储检测到的keypoints
的方向)来检测keypoints
(默认情况下为500
)。 一旦检测到keypoints
,下一步就是计算描述符,以获得与每个检测到的关键点相关的信息。 ORB 使用修改的BRIEF-32
描述符获取每个检测到的关键点的描述。 例如,检测到的keypoints
的描述符如下所示:
[103 4 111 192 86 239 107 66 141 117 255 138 81 92 62 101 123 148 91 62 3 177 61 205 31 12 129 68 165 203 116 116]
因此,第一点是创建 ORB 检测器:
orb = cv2.ORB_create()
下一步是检测已加载图像中的keypoints
:
keypoints = orb.detect(image, None)
一旦检测到keypoints
,下一步就是计算检测到的keypoints
的描述符:
keypoints, descriptors = orb.compute(image, keypoints)
注意,您还可以执行orb.detectAndCompute(image, None)
来检测keypoints
并计算检测到的keypoints
的描述符。 最后,我们可以使用cv2.drawKeypoints()
函数绘制检测到的keypoints
:
image_keypoints = cv2.drawKeypoints(image, keypoints, None, color=(255, 0, 255), flags=0)
下一个屏幕截图中可以看到此脚本的输出:
可以看出,右边的结果显示了 ORB 关键点检测器已检测到的 ORB 检测关键点。
特征匹配
在下一个示例中,我们将看到如何匹配检测到的特征。 OpenCV 提供了两个匹配器,如下所示:
- 暴力(BF)匹配器:此匹配器采用为第一个集合中每个检测到的特征计算的每个描述符,以及第二组中的所有其他描述符,并对其进行匹配。 最后,它返回距离最近的匹配项。
- 近似最近邻的快速库(FLANN)匹配器:对于大型数据集,此匹配器比 BF 匹配器工作更快。 它包含用于最近邻搜索的优化算法。
在feature_matching.py
脚本中,我们将使用 BF 匹配器来查看如何匹配检测到的特征。 因此,第一步是要检测keypoints
并计算描述符:
orb = cv2.ORB_create()
keypoints_1, descriptors_1 = orb.detectAndCompute(image_query, None)
keypoints_2, descriptors_2 = orb.detectAndCompute(image_scene, None)
下一步是使用cv2.BFMatcher()
创建 BF 匹配器对象:
bf_matcher = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
第一个参数normType
将距离测量默认设置为使用cv2.NORM_L2
。 在使用 ORB 描述符(或其他基于二进制的描述符,例如 Brief 或 BRISK)的情况下,要使用的距离度量为cv2.NORM_HAMMING
。 可以将第二个参数crossCheck
(默认情况下为False
)设置为True
,以便在匹配过程中仅返回一致的对(两组中的两个特征应相互匹配)。 创建完成后,下一步就是使用BFMatcher.match()
方法匹配检测到的描述符:
bf_matches = bf_matcher.match(descriptors_1, descriptors_2)
descriptors_1
和descriptors_2
是应该预先计算的描述符; 这样,我们可以在两张图像中获得最佳匹配。 此时,我们可以按距离的升序对匹配项进行排序:
bf_matches = sorted(bf_matches, key=lambda x: x.distance)
最后,我们可以使用cv2.drawMatches()
函数绘制匹配项。 在这种情况下,仅显示前20
个匹配项(出于可见性考虑):
result = cv2.drawMatches(image_query, keypoints_1, image_scene, keypoints_2, bf_matches[:20], None, matchColor=(255, 255, 0), singlePointColor=(255, 0, 255), flags=0)
cv2.drawMatches()
函数水平连接两个图像,并从显示匹配的第一张图像到第二张图像绘制线条。
下一个屏幕截图中可以看到feature_matching.py
脚本的输出:
如您在上一个屏幕截图中所见,绘制了两个图像(image_query
,image_scene
)之间的匹配。
用于查找对象的特征匹配和单应性计算
为了完成本节,我们将看到查找对象的最后一步。 一旦特征匹配,下一步就是使用cv2.findHomography()
函数在两个图像中匹配的keypoints
的位置之间找到透视变换。
OpenCV 提供了几种计算单应性矩阵的方法-RANSAC
,最低中位数(LMEDS
)和 PROSAC(RHO
)。 在此示例中,我们使用RANSAC
,如下所示:
M, mask = cv2.findHomography(pts_src, pts_dst, cv2.RANSAC, 5.0)
这里,pts_src
是源图像中匹配的关键点的位置,pts_dst
是查询图像中匹配的keypoints
的位置。
第四个参数ransacReprojThreshold
设置最大重投影误差,以将一个点对视为一个惯常值。 在这种情况下,如果重投影误差大于5.0
,则将相应的点对视为异常值。 此函数计算并返回由keypoints
位置定义的源平面和目标平面之间的透视变换矩阵M
。
最后,基于透视变换矩阵M
,我们将计算查询图像中对象的四个角。 为此,我们将基于原始图像的形状计算其四个角,然后使用cv2.perspectiveTransform()
函数将其转换为目标角:
pts_corners_dst = cv2.perspectiveTransform(pts_corners_src, M)
这里,pts_corners_src
包含原始图像的四个角,M
是透视变换矩阵; pts_corners_dst
输出包含查询图像中对象的四个角。 我们可以使用cv2.polyline()
函数绘制检测到的物体的轮廓:
img_obj = cv2.polylines(image_scene, [np.int32(pts_corners_dst)], True, (0, 255, 255), 10)
最后,我们还可以使用cv2.drawMatches()
函数绘制匹配,如下所示:
img_matching = cv2.drawMatches(image_query, keypoints_1, img_obj, keypoints_2, best_matches, None, matchColor=(255, 255, 0), singlePointColor=(255, 0, 255), flags=0)
下一个屏幕截图中可以看到feature_matching_object_recognition.py
脚本的输出:
在前面的屏幕截图中,您可以看到特征匹配和单应性计算,这是对象识别的两个关键步骤。
基于标记的增强现实
在本节中,我们将了解基于标记的增强现实的工作原理。 您可以使用许多库,算法或包来生成和检测标记。 从这个意义上说,提供最先进的检测标记表现的是 ArUco。
ArUco 自动检测标记并纠正可能的错误。 此外,ArUco 通过将多个标记与通过颜色分割计算的遮挡遮罩相结合,提出了一种遮挡问题的解决方案。
如前所述,姿势估计是增强现实应用中的关键过程。 可以基于标记执行姿势估计。 使用标记的主要好处是可以在可以精确得出标记的四个角的图像中高效且鲁棒地检测到它们。 最后,可以从先前计算出的标记的四个角获得相机姿态。 因此,在接下来的小节中,我们将从创建标记和字典开始,了解如何创建基于标记的增强现实应用。
创建标记和字典
使用 ArUco 的第一步是创建标记和字典。 首先, ArUco 标记是由外部和内部单元(也称为位)组成的正方形标记。 外部单元设置为黑色,从而可以快速,可靠地检测到外部边界。 其余的单元(内部单元)用于编码标记。 还可以创建具有不同大小的 ArUco 标记。 标记的大小指示与内部矩阵相关的内部单元数。 例如,5 x 5
(n=5
)的标记大小由 25 个内部单元组成。 此外,您还可以设置标记边界中的位数。
其次,标记字典是被认为在特定应用中使用的标记集。 虽然以前的库仅考虑固定字典,但 ArUco 提出了一种自动方法,用于生成具有所需数量和位数的标记。 从这个意义上讲,ArUco 包括一些预定义的词典,涵盖了与标记数量和标记大小有关的许多配置。
创建基于标记的增强现实应用时要考虑的第一步是打印要使用的标记。
在aruco_create_markers.py
脚本中,我们正在创建一些准备打印的标记。 第一步是创建字典对象。 ArUco 具有一些预定义的字典:DICT_4X4_50 = 0
,DICT_4X4_100 = 1
,DICT_4X4_250 = 2
,DICT_4X4_1000 = 3
,DICT_5X5_50 = 4
,DICT_5X5_100 = 5
,DICT_5X5_250 = 6
,DICT_5X5_1000 = 7
,DICT_6X6_50 = 8
,DICT_6X6_100 = 9
,DICT_6X6_250 = 10
, DICT_6X6_1000 = 11
,DICT_7X7_50 = 12
,DICT_7X7_100 = 13
,DICT_7X7_250 = 14
和DICT_7X7_1000 = 15
。
在这种情况下,我们将使用由250
标记组成的cv2.aruco.Dictionary_get()
函数创建字典。 每个标记的大小为7 x 7
(n=7
):
aruco_dictionary = cv2.aruco.Dictionary_get(cv2.aruco.DICT_7X7_250)
此时,可以使用cv2.aruco.drawMarker()
函数绘制标记,该函数返回准备打印的标记。 cv2.aruco.drawMarker()
的第一个参数是dictionary
对象。 第二个参数是标记id
,其范围在0
和249
之间,因为我们的词典中包含250
标记。 第三个参数sidePixels
是创建的标记图像的大小(以像素为单位)。 第四个参数(可选,默认为1
)为borderBits
,它设置标记边界中的位数。
因此,在此示例中,我们将创建三个标记,这些标记会改变标记边界中的位数:
aruco_marker_1 = cv2.aruco.drawMarker(dictionary=aruco_dictionary, id=2, sidePixels=600, borderBits=1)
aruco_marker_2 = cv2.aruco.drawMarker(dictionary=aruco_dictionary, id=2, sidePixels=600, borderBits=2)
aruco_marker_3 = cv2.aruco.drawMarker(dictionary=aruco_dictionary, id=2, sidePixels=600, borderBits=3)
这些标记图像可以保存在磁盘上(使用cv2.imwrite()
):
cv2.imwrite("marker_DICT_7X7_250_600_1.png", aruco_marker_1)
cv2.imwrite("marker_DICT_7X7_250_600_2.png", aruco_marker_2)
cv2.imwrite("marker_DICT_7X7_250_600_3.png", aruco_marker_3)
在aruco_create_markers.py
脚本中,我们还显示了创建的标记。 输出可以在下一个屏幕截图中看到:
在前面的屏幕截图中,显示了三个创建的标记。
检测标记
您可以使用cv2.aruco.detectMarkers()
函数来检测图像中的标记:
corners, ids, rejected_corners = cv2.aruco.detectMarkers(gray_frame, aruco_dictionary, parameters=parameters)
cv2.aruco.detectMarkers()
的第一个参数是要在其中检测标记的灰度图像。 第二个参数是字典对象,它应该先前已经创建。 第三个参数建立了在检测过程中可以自定义的所有参数。 此函数返回以下信息:
- 返回检测到的标记的角列表。 对于每个标记,将返回其四个角(左上,右上,右下和左下)。
- 返回检测到的标记的标识符列表。
- 返回被拒绝的候选者列表,该列表由找到的所有正方形组成,但是它们没有正确的编码。 此拒绝候选者列表对于调试目的很有用。 每个被拒绝的候选人都由其四个角组成。
aruco_detect_markers.py
脚本从网络摄像头检测标记。 首先,使用上述cv2.aruco.detectMarkers()
函数检测标记,然后,使用cv2.aruco.drawDetectedMarkers()
函数绘制检测到的标记和拒绝的候选者,如下所示:
# Draw detected markers:
frame = cv2.aruco.drawDetectedMarkers(image=frame, corners=corners, ids=ids, borderColor=(0, 255, 0))
# Draw rejected markers:
frame = cv2.aruco.drawDetectedMarkers(image=frame, corners=rejected_corners, borderColor=(0, 0, 255))
如果执行aruco_detect_markers.py
脚本,检测到的标记将以绿色边框绘制,而被拒绝的候选者将以红色边框绘制,如以下屏幕截图所示:
在前面的屏幕截图中,您可以看到检测到一个标记(带有id=2
),该标记以绿色边框绘制。 此外,您还可以看到两个带有红色边框的被拒绝候选人。
相机校准
在使用检测到的标记获取相机姿态之前,有必要了解相机的校准参数。 从这个意义上讲,ArUco 提供了执行此任务所需的校准程序。 请注意,校准过程仅执行一次,因为未更改相机光学器件。 校准过程中使用的主要函数是cv2.aruco.calibrateCameraCharuco()
。
前述函数使用从板上提取的几个视图中的一组角来校准摄像机。 校准过程完成后,此函数返回相机矩阵(3 x 3
浮点相机矩阵)和包含失真系数的向量。 更具体地说,3 x 3
矩阵对焦距和相机中心坐标(也称为固有参数)进行编码。 失真系数模拟了相机产生的失真。
该函数的签名如下:
calibrateCameraCharuco(charucoCorners, charucoIds, board, imageSize, cameraMatrix, distCoeffs[, rvecs[, tvecs[, flags[, criteria]]]]) -> retval, cameraMatrix, distCoeffs, rvecs, tvecs
此处,charucoCorners
是包含检测到的 charuco 角的向量,charucoIds
是标识符的列表,board
代表电路板布局,imageSize
是输入图像尺寸。 输出向量rvecs
包含为每个板视图估计的旋转向量的向量,tvecs
是为每个图案视图估计的平移向量的向量。 如前所述,相机矩阵cameraMatrix
和失真系数distCoeffs
也将返回。
该板是使用cv2.aruco.CharucoBoard_create()
函数创建的:
签名如下:
CharucoBoard_create(squaresX, squaresY, squareLength, markerLength, dictionary) -> retval
此处,squareX
是在x
方向上的平方数,squaresY
是在y
方向上的平方数,squareLength
是棋盘方边的长度(通常 markerLength
是标记的边长(与squareLength
相同的单位),dictionary
设置字典中要使用的第一个标记,以便在板上创建标记。 例如,为了创建一个木板,我们可以使用以下代码行:
dictionary = cv2.aruco.Dictionary_get(cv2.aruco.DICT_7X7_250)
board = cv2.aruco.CharucoBoard_create(3, 3, .025, .0125, dictionary)
img = board.draw((200 * 3, 200 * 3))
在下一个屏幕截图中可以看到创建的木板:
稍后,cv2.aruco.calibrateCameraCharuco()
函数将在校准过程中使用此板:
cal = cv2.aruco.calibrateCameraCharuco(all_corners, all_ids, board, image_size, None, None)
校准过程完成后,我们会将相机矩阵和失真系数都保存到磁盘中。 为此,我们使用 Pickle,它可以用于对 Python 对象结构进行序列化和反序列化。
校准过程完成后,我们现在可以执行相机姿态估计。
aruco_camera_calibration.py
脚本执行校准过程。 请注意,通过使用先前创建和印刷的电路板,此脚本可用于创建电路板和执行校准过程。
相机姿态估计
为了估计摄像机姿态,可以使用cv2.aruco.estimatePoseSingleMarkers()
函数,该函数可以估计单个标记的姿态。 姿势由旋转和平移向量组成。 签名如下:
cv.aruco.estimatePoseSingleMarkers( corners, markerLength, cameraMatrix, distCoeffs[, rvecs[, tvecs[, _objPoints]]] ) -> rvecs, tvecs, _objPoints
这里,cameraMatrix
和distCoeffs
分别是相机矩阵和失真系数; 应向他们提供在校准过程之后获得的值。 corners
参数是一个向量,其中包含每个检测到的标记的四个角。 markerLength
参数是标记边的长度。 请注意,返回的翻译向量将以相同的单位表示。 此函数为每个检测到的标记返回rvecs
(旋转向量),tvecs
(平移向量)和_objPoints
(所有检测到的标记角的对象点数组)。
标记坐标系以标记的中心为中心。 因此,标记的四个角的坐标(在其自己的坐标系中)如下:
(-markerLength/2, markerLength/2, 0)
(markerLength/2, markerLength/2, 0)
(markerLength/2, -markerLength/2, 0)
(-markerLength/2, -markerLength/2, 0)
最后,ArUco 还提供cv.aruco.drawAxis()
函数,该函数可用于绘制每个检测到的标记的系统轴。
签名如下:
cv.aruco.drawAxis( image, cameraMatrix, distCoeffs, rvec, tvec, length ) -> image
除length
参数以外,所有参数先前都已在以前的函数中引入,该参数设置绘制轴的长度(与tvec
相同的单位)。 下一个屏幕截图显示了脚本aruco_detect_markers_pose.py
的输出:
在上一个屏幕截图中,您可以看到仅检测到一个标记,并且已绘制了该标记的系统轴。
相机姿态估计和基本增强
此时,我们可以叠加一些图像,形状或 3D 模型,以查看完整的增强现实应用。 在第一个示例中,我们将使用标记的大小覆盖一个矩形。 执行此功能的代码如下:
if ids is not None:
# rvecs and tvecs are the rotation and translation vectors respectively
rvecs, tvecs, _ = cv2.aruco.estimatePoseSingleMarkers(corners, 1, cameraMatrix, distCoeffs)
for rvec, tvec in zip(rvecs, tvecs):
# Define the points where you want the image to be overlaid (remember: marker coordinate system):
desired_points = np.float32(
[[-1 / 2, 1 / 2, 0], [1 / 2, 1 / 2, 0], [1 / 2, -1 / 2, 0], [-1 / 2, -1 / 2, 0]])
# Project the points:
projected_desired_points, jac = cv2.projectPoints(desired_points, rvecs, tvecs, cameraMatrix, distCoeffs)
# Draw the projected points:
draw_points(frame, projected_desired_points)
第一步是定义要覆盖图像或模型的点。 由于我们希望将矩形覆盖在检测到的标记上,因此这些坐标为[[-1 / 2, 1 / 2, 0], [1 / 2, 1 / 2, 0], [1 / 2, -1 / 2, 0], [-1 / 2, -1 / 2, 0]]
。
请记住,我们必须在标记坐标系中定义这些坐标。 下一步是使用cv2.projectPoints()
函数投影这些点:
projected_desired_points, jac = cv2.projectPoints(desired_points, rvecs, tvecs, cameraMatrix, distCoeffs)
最后,我们使用draw_points()
函数绘制这些点:
def draw_points(img, pts):
""" Draw the points in the image"""
pts = np.int32(pts).reshape(-1, 2)
img = cv2.drawContours(img, [pts], -1, (255, 255, 0), -3)
for p in pts:
cv2.circle(img, (p[0], p[1]), 5, (255, 0, 255), -1)
return img
下一个屏幕截图中可以看到aruco_detect_markers_draw_square.py
脚本的输出:
在前面的屏幕截图中,您可以看到一个青色矩形已覆盖在检测到的标记上。 此外,您还可以看到以洋红色绘制的矩形的四个角。
相机姿态估计和高级增强
可以轻松修改aruco_detect_markers_draw_square.py
脚本,以覆盖更高级的增强功能。
在这种情况下,我们将覆盖一棵树的图像,可以在下一个屏幕截图中看到:
为了执行此扩充,我们对draw_augmented_overlay()
函数进行了编码,如下所示:
def draw_augmented_overlay(pts_1, overlay_image, image):
"""Overlay the image 'overlay_image' onto the image 'image'"""
# Define the squares of the overlay_image image to be drawn:
pts_2 = np.float32([[0, 0], [overlay_image.shape[1], 0], [overlay_image.shape[1], overlay_image.shape[0]], [0, overlay_image.shape[0]]])
# Draw border to see the limits of the image:
cv2.rectangle(overlay_image, (0, 0), (overlay_image.shape[1], overlay_image.shape[0]), (255, 255, 0), 10)
# Create the transformation matrix:
M = cv2.getPerspectiveTransform(pts_2, pts_1)
# Transform the overlay_image image using the transformation matrix M:
dst_image = cv2.warpPerspective(overlay_image, M, (image.shape[1], image.shape[0]))
# cv2.imshow("dst_image", dst_image)
# Create the mask:
dst_image_gray = cv2.cvtColor(dst_image, cv2.COLOR_BGR2GRAY)
ret, mask = cv2.threshold(dst_image_gray, 0, 255, cv2.THRESH_BINARY_INV)
# Compute bitwise conjunction using the calculated mask:
image_masked = cv2.bitwise_and(image, image, mask=mask)
# cv2.imshow("image_masked", image_masked)
# Add the two images to create the resulting image:
result = cv2.add(dst_image, image_masked)
return result
draw_augmented_overlay()
函数首先定义叠加图像的正方形。 然后,计算变换矩阵,该变换矩阵用于变换覆盖图像以获得dst_image
图像。 接下来,我们创建mask
并使用先前创建的mask
计算按位运算以获得image_masked
图像。 最后一步是在dst_image
和image_masked
之间执行加法以获得result
图像,最后将其返回。
下一个屏幕截图中可以看到aruco_detect_markers_augmented_reality.py
脚本的输出:
为了覆盖更复杂和高级的 3D 模型,可以使用 OpenGL。 开放图形库(OpenGL)是用于渲染 2D 和 3D 模型的跨平台 API。
从这个意义上讲,PyOpenGL 是最常见且标准的跨平台 Python 绑定到 OpenGL。
基于 Snapchat 的增强现实
在本节中,我们将了解如何创建一些有趣的基于 Snapchat 的过滤器。 在这种情况下,我们将创建两个过滤器。 第一个在检测到的脸上的鼻子和嘴之间覆盖了一个大胡子。 第二个在检测到的面部上覆盖一副眼镜。
在以下小节中,您将看到如何实现此功能。
基于 Snapchat 的增强现实 OpenCV 胡须覆盖
在snapchat_augmeted_reality_moustache.py
脚本中,我们在检测到的面部上覆盖了胡须。 从网络摄像头连续捕获图像。 我们还提供了使用测试图像代替从网络摄像头捕获的图像的可能性。 这对于调试算法很有用。 在解释此脚本的关键步骤之前,我们将看下一个屏幕截图,它是使用测试图像时算法的输出:
第一步是检测图像中的所有面部。 如您所见,青色矩形表示图像中检测到的脸部的位置和大小。 该算法的下一步是遍历图像中所有检测到的面部,在其区域内搜索鼻子。 洋红色矩形表示图像中检测到的鼻子。 一旦检测到鼻子,下一步就是调整要覆盖小胡子的区域,该区域是根据之前计算出的鼻子的位置和大小计算得出的。 在这种情况下,蓝色矩形表示将要覆盖胡须的位置。 您还可以看到图像中检测到两个鼻子,并且只有一个胡须覆盖。 这是因为执行基本检查是为了知道所检测到的鼻子是否有效。 一旦我们检测到有效的鼻子,小胡子就会被覆盖,如果离开我们会继续在检测到的面部上进行迭代,否则将分析另一帧。
因此,在此脚本中,面部和鼻子都被检测到。 为了检测这些对象,创建了两个分类器,一个用于检测人脸,另一个用于检测鼻子。 要创建这些分类器,以下代码是必需的:
face_cascade = cv2.CascadeClassifier("haarcascade_frontalface_default.xml")
nose_cascade = cv2.CascadeClassifier("haarcascade_mcs_nose.xml")
创建分类器后,下一步就是检测图像中的这些对象。 在这种情况下,将使用cv2.detectMultiScale()
函数。 此函数检测输入灰度图像中大小不同的对象,并将检测到的对象作为矩形列表返回。 例如,为了检测面部,可以使用以下代码:
faces = face_cascade.detectMultiScale(gray, 1.3, 5)
在这一点上,我们遍历检测到的面部,尝试检测鼻子:
# Iterate over each detected face:
for (x, y, w, h) in faces:
# Draw a rectangle to see the detected face (debugging purposes):
# cv2.rectangle(frame, (x, y), (x + w, y + h), (255, 255, 0), 2)
# Create the ROIS based on the size of the detected face:
roi_gray = gray[y:y + h, x:x + w]
roi_color = frame[y:y + h, x:x + w]
# Detects a nose inside the detected face:
noses = nose_cascade.detectMultiScale(roi_gray)
一旦检测到鼻子,我们将在所有检测到的鼻子上进行迭代,并计算出胡须覆盖的区域。 执行基本检查以滤除假鼻子位置。 如果成功,则胡子将基于先前计算的区域覆盖在图像上:
for (nx, ny, nw, nh) in noses:
# Draw a rectangle to see the detected nose (debugging purposes):
# cv2.rectangle(roi_color, (nx, ny), (nx + nw, ny + nh), (255, 0, 255), 2)
# Calculate the coordinates where the moustache will be placed:
x1 = int(nx - nw / 2)
x2 = int(nx + nw / 2 + nw)
y1 = int(ny + nh / 2 + nh / 8)
y2 = int(ny + nh + nh / 4 + nh / 6)
if x1 < 0 or x2 < 0 or x2 > w or y2 > h:
continue
# Draw a rectangle to see where the moustache will be placed (debugging purposes):
# cv2.rectangle(roi_color, (x1, y1), (x2, y2), (255, 0, 0), 2)
# Calculate the width and height of the image with the moustache:
img_moustache_res_width = int(x2 - x1)
img_moustache_res_height = int(y2 - y1)
# Resize the mask to be equal to the region were the glasses will be placed:
mask = cv2.resize(img_moustache_mask, (img_moustache_res_width, img_moustache_res_height))
mask_inv = cv2.bitwise_not(mask)
img = cv2.resize(img_moustache, (img_moustache_res_width, img_moustache_res_height))
# Take ROI from the BGR image:
roi = roi_color[y1:y2, x1:x2]
# Create ROI background and ROI foreground:
roi_bakground = cv2.bitwise_and(roi, roi, mask=mask_inv)
roi_foreground = cv2.bitwise_and(img, img, mask=mask)
# Show both roi_bakground and roi_foreground (debugging purposes):
# cv2.imshow('roi_bakground', roi_bakground)
# cv2.imshow('roi_foreground', roi_foreground)
# Add roi_bakground and roi_foreground to create the result:
res = cv2.add(roi_bakground, roi_foreground)
# Set res into the color ROI:
roi_color[y1:y2, x1:x2] = res
break
关键是img_moustache_mask
图像。 使用要叠加的图像的 Alpha 通道创建此图像。
这样,将仅在图像中绘制覆盖图像的前景。 在以下屏幕截图中,您可以看到基于覆盖图像的 Alpha 通道创建的胡须遮罩:
要创建此掩码,我们执行以下操作:
img_moustache = cv2.imread('moustache.png', -1)
img_moustache_mask = img_moustache[:, :, 3]
下一个屏幕截图中可以看到snapchat_augmeted_reality_moustache.py
脚本的输出:
以下屏幕快照中包含的所有胡须都可以在您的增强现实应用中使用:
实际上,我们还创建了moustaches.svg
文件,其中包含了这六个不同的胡须。
基于 Snapchat 的增强现实 OpenCV 眼镜覆盖
以类似的方式,我们还对snapchat_agumeted_reality_glasses.py
脚本进行了编码,以在检测到的面部的眼睛区域上覆盖一副眼镜。 在这种情况下,为了检测图像中的眼睛,使用了眼睛对检测器。
因此,应该相应地创建分类器:
eyepair_cascade = cv2.CascadeClassifier("haarcascade_mcs_eyepair_big.xml")
在下一个屏幕截图中,可以看到使用测试图像时算法的输出:
青色矩形表示图像中检测到的脸部的位置和大小。 洋红色矩形表示图像中检测到的眼睛对。 黄色矩形表示将要覆盖眼镜的位置,该位置是根据眼睛对区域的位置和大小计算得出的。 如您所见,已将某些透明度添加到眼镜覆盖的图像中,以使其更加逼真。
这也可以在眼镜图像遮罩中看到,该图像在下一个屏幕截图中显示:
下一个屏幕截图中可以看到snapchat_augmeted_reality_glasses.py
脚本的输出:
所有这些眼镜可以在以下屏幕截图中看到:
最后,我们还创建了glasses.svg
文件,其中包含六种不同的眼镜。 因此,您可以在增强现实应用中播放和使用所有这些眼镜。
QR 码检测
为了完成本章,我们将学习如何检测图像中的 QR 码。 这样,QR 码也可以用作我们的增强现实应用的标记。 cv2.detectAndDecode()
函数可检测并解码包含 QR 码的图像中的 QR 码。 图像可以是灰度或彩色(BGR)。
该函数返回以下内容:
- 返回找到的 QR 码的顶点数组。 如果未找到 QR 码,则此数组可以为空。
- 已校正并二值化的 QR 码被返回。
- 返回与此 QR 码关联的数据。
在qr_code_scanner.py
脚本中,我们利用上述函数来检测和解码 QR 码。 接下来重点说明要点。
首先,加载图像,如下所示:
image = cv2.imread("qrcode_rotate_45_image.png")
接下来,我们使用以下代码创建 QR 码检测器:
qr_code_detector = cv2.QRCodeDetector()
然后,我们应用cv2.detectAndDecode()
函数,如下所示:
data, bbox, rectified_qr_code = qr_code_detector.detectAndDecode(image)
我们在解码数据之前检查是否找到 QR 码,并使用show_qr_detection()
函数显示检测到的代码:
if len(data) > 0:
print("Decoded Data : {}".format(data))
show_qr_detection(image, bbox)
show_qr_detection()
函数绘制检测到的 QR 码的线条和角点:
def show_qr_detection(img, pts):
"""Draws both the lines and corners based on the array of vertices of the found QR code"""
pts = np.int32(pts).reshape(-1, 2)
for j in range(pts.shape[0]):
cv2.line(img, tuple(pts[j]), tuple(pts[(j + 1) % pts.shape[0]]), (255, 0, 0), 5)
for j in range(pts.shape[0]):
cv2.circle(img, tuple(pts[j]), 10, (255, 0, 255), -1)
下一个屏幕截图中可以看到qr_code_scanner.py
脚本的输出:
在前面的屏幕截图中,您可以看到经过校正和二值化的 QR 码(左)和检测到的标记(右),带有蓝色边框,洋红色正方形点突出显示检测结果。
总结
在本章中,我们介绍了增强现实技术的介绍。 我们编写了一些示例,以了解如何构建标记和无标记增强现实应用。 此外,我们看到了如何叠加简单的模型(形状或图像等)。
如前所述,要覆盖更复杂的模型,可以使用 PyOpenGL(Python 的标准 OpenGL 绑定)。 在本章中,为简化起见,未解决该库。
我们还看到了如何创建一些有趣的基于 Snapchat 的过滤器。 应当注意,在第 11 章,“人脸检测,跟踪和识别”中,将介绍用于人脸检测,跟踪和人脸标志定位的更高级算法。 因此,可以轻松修改本章中编码的基于 Snapchat 的过滤器,以包括更健壮的管道,以得出眼镜和胡须应重叠的位置。 最后,我们已经了解了如何检测 QR 码,可以将其用作增强现实应用中的标记。
在第 10 章,“使用 OpenCV 的机器学习”中,将向您介绍机器学习的世界,并且您将了解如何在计算机视觉项目中使用机器学习。
问题
- 初始化 ORB 检测器,找到
keypoints
,并使用 ORB 在加载的图像image
中计算描述符 - 画出先前检测到的
keypoints
- 创建
BFMatcher
对象,并匹配先前计算出的descriptors_1
和descriptors_2
- 对之前计算出的匹配进行排序,并绘制第一个
20
匹配 - 使用 ArUco 在
gray_frame
图像中检测标记 - 使用 ArUco 绘制检测到的标记
- 使用 ArUco 绘制拒绝的标记
- 检测并解码图像
image
中包含的 QR 码
进一步阅读
以下参考将帮助您更深入地了解增强现实:
十、使用 OpenCV 的机器学习
机器学习是人工智能的一种应用,它为计算机(以及具有一定计算能力的其他系统)提供了自动根据经验进行预测或决策的能力,而无需进行明确编程即可执行任务。 机器学习的概念已经存在了很长时间,但是在过去的几年中,它的发展势头强劲,这主要归因于以下三个关键因素:
- 数据量大大增加。
- 有明显改进的算法。
- 实质上有更强大的计算机硬件。 虚拟个人助理(例如,智能扬声器或移动应用),通勤时的预测(交通预测或导航服务),视频系统(监控摄像头系统或车牌识别系统)以及电子商务应用(推荐系统或自动价格识别系统) 比较应用)只是我们日常生活中机器学习应用的一些示例。
在本章中,我们将看到 OpenCV 提供的一些最常见的机器学习算法和技术,它们可以解决计算机视觉项目中的实际问题,例如分类和回归问题。
我们将涵盖以下主题:
- 机器学习入门
- K 均值聚类
- K 最近邻
- 支持向量机
技术要求
技术要求如下:
- Python 和 OpenCV
- 特定于 Python 的 IDE
- NumPy 和 Matplotlib 包
- Git 客户端
有关如何安装这些要求的更多详细信息,请参见第 1 章,“设置 OpenCV”。 可通过 Github 访问《精通 Python OpenCV 4》的 GitHub 存储库,其中包含从本书第一章到最后的所有必要的支持项目文件。
机器学习入门
在第 1 章,“设置 OpenCV”中,我们介绍了计算机视觉,人工智能,机器学习,神经网络和深度学习的概念,这些概念可以按层次结构进行构建, 如下所示:
可以看出,人工智能主题包括所有其他主题。 在本章中,我们将专注于机器学习。
如果您想对这些概念进行复习,请参阅第 1 章,“设置 OpenCV”。
机器学习是对计算机进行编程以从历史数据中学习以对新数据进行预测的过程。 机器学习是人工智能的一个子学科,是指统计技术,通过这些技术,机器可以在学习到的相互关系的基础上执行操作。 基于收集或收集的数据,算法是由计算机独立学习的。
在机器学习的上下文中,有三种主要方法-监督机器学习,无监督机器学习和半监督的机器学习技术。 这些方法可以在下图中看到。 为了完成它,我们包含了三种最常见的技术来解决分类,回归和聚类问题:
这些方法之间的主要区别是学习过程,我们将在下面讨论。
监督机器学习
使用样本集合进行监督学习,每个样本具有相应的输出值(期望的输出)。 这些机器学习方法称为监督,因为我们知道每个训练示例的正确答案,并且有监督的学习算法会分析训练数据,以便对训练数据做出预测。 此外,可以基于预测与相应的期望输出之间的差异来校正这些预测。 基于这些更正,该算法可以从误差中学习以调整其内部参数。 这样,在监督学习中,算法会迭代地调整一个函数,该函数可以最佳地近似样本集合与相应所需输出之间的关系。
监督学习问题可以进一步分为以下几类:
- 分类:当输出变量是类别(例如颜色(红色,绿色或蓝色),尺寸(大,中或小)或性别(男性或女性))时,可能被视为分类问题。 在分类问题中,该算法将输入映射到输出标签。
- 回归:当输出变量是真实值(例如年龄或体重)时,监督学习问题可以归类为回归问题。 在回归问题中,该算法将输入映射到连续输出。
在监督学习中,有一些主要问题需要考虑,为了完整起见,接下来将进行评论:
- 偏差方差权衡:偏差方差折衷是机器学习中的一个常用术语,指的是模型-数据不足的模型具有较高的偏差,而模型的数据过拟合则偏高。 数据差异很大:
- 偏差可以看作是学习算法中错误假设产生的误差,可以定义为模型预测与我们尝试预测的正确值之间的差异。 这导致算法通过不考虑数据中的所有信息(拟合不足)来学习错误的东西。 因此,具有高偏差的模型无法在数据中找到所有模式,因此它不太适合训练集,也不太适合测试集。
- 方差可以定义为算法通过拟合模型来密切学习数据中的误差/噪声(过拟合),而该趋势倾向于学习错误的事物,而与真实信号无关。 因此,具有高方差的模型非常适合训练集,但是由于它也已获悉数据中的误差/噪声,因此无法推广到测试集。 请查看下图,以更好地理解:
- 函数复杂度和训练数据量:模型复杂度是指机器学习算法正尝试以与多项式次数相似的方式学习函数的复杂性。 模型复杂性的适当级别通常由训练数据的性质决定。 例如,如果您需要少量的数据来训练模型,则低复杂度的模型是可取的。 这是因为高复杂度模型将适合较小的训练集。
- 输入空间的维数:在处理高/非常高维的特征空间时,学习问题可能会非常困难,因为许多额外的特征会混淆学习过程, 结果差异很大。 因此,当处理高/非常高维的特征空间时,一种常见的方法是将学习算法修改为具有高偏差和低方差。 此问题与维度诅咒有关,后者指的是在分析和组织在低维空间中找不到的高维空间中的数据时出现的各个方面。
- 输出值中的噪声:如果所需的输出值不正确(由于人为或传感器误差),则学习算法尝试过于紧密地拟合数据时,可能会发生过拟合。 有几种常见的策略可用于减轻输出值中的误差/噪声影响。 例如,在训练算法之前检测并去除嘈杂的训练示例是一种常见的方法。 另一个策略是尽早停止,可以用来防止过拟合。
无监督机器学习
在无监督学习中,没有标记输出。 从这个意义上说,这里有一个样本集合,但是每个样本的相应输出值都丢失了(样本集合没有被标记,分类或分类)。 无监督学习的目标是对样本集合中的基础结构或分布进行建模和推断。 因此,在无监督学习中,该算法无法找到正确的输出,但是可以探索数据并可以从数据中进行推断,以试图揭示其中的隐藏结构。 聚类或降维是无监督学习中最常用的两种算法。
半监督机器学习
顾名思义,半监督学习可以看作是监督学习和无监督学习之间的折衷,因为它使用标记和未标记的数据进行训练。 从这个意义上讲,您拥有大量输入数据并且仅对其中一些数据进行了标记的问题可以归类为半监督学习问题。
许多现实世界中的机器学习问题可以归类为半监督问题,因为正确标记所有数据可能非常困难,昂贵或耗时,而未标记的数据更易于收集。
在这些情况下,仅标记了少量的训练数据,您可以探索有监督和无监督学习技术:
- 您可以使用无监督学习技术来发现和学习输入变量中的结构。
- 您可以使用监督学习技术来使用标记的数据训练分类器,然后使用此模型对未标记的数据进行预测。 此时,您可以将该数据作为训练数据反馈到监督学习算法中,以迭代地增加标记数据的大小,并使用重新训练的模型对新的未标记数据进行预测。
K 均值聚类
OpenCV 提供cv2.kmeans()
函数,该函数实现了 K 均值聚类算法,该算法查找聚类的中心并对聚类周围的输入样本进行分组。
K 均值聚类算法的目标是将n
个样本划分(或聚类)为K
聚类,其中每个样本将属于具有最均值的聚类。 cv2.kmeans()
函数的签名如下:
retval, bestLabels, centers=cv.kmeans(data, K, bestLabels, criteria, attempts, flags[, centers])
data
代表用于聚类的输入数据。 它应为np.float32
数据类型,并且每个特征都应放在单个列中。 K
指定最后所需的群集数。 使用criteria
参数指定算法终止标准,该参数设置最大迭代次数和/或所需的精度。 当满足这些条件时,算法终止。 criteria
是三个参数type
,max_iterm
和epsilon
的元组:
type
:这是终止条件的类型。 它具有三个标志:cv2.TERM_CRITERIA_EPS
:当达到指定的精度epsilon
时,算法停止。cv2.TERM_CRITERIA_MAX_ITER
:当达到指定的迭代次数max_iterm
时,算法停止。cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER
:达到两个条件中的任何一个时,算法将停止。
max_iterm
:这是最大迭代次数。epsilon
:这是必需的精度。
条件的示例如下:
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 20, 1.0)
在这种情况下,最大迭代次数设置为20
(max_iterm = 20
),所需的精度为1.0
(epsilon = 1.0
)。
attempts
参数指定使用不同的初始标签执行算法的次数。 该算法返回产生最佳紧密度的标签。 flags
参数指定如何获取初始中心。 cv2.KMEANS_RANDOM_CENTERS
标志在每次尝试中选择随机的初始中心。 cv2.KMEANS_PP_CENTERS
标志使用 Arthur 和 Vassilvitskii 提出的 K 均值++ 中心初始化(请参阅《K 均值++:精心播种的优势》(2007))。
cv2.kmeans()
返回以下内容:
bestLabels
:一个整数数组,存储每个样本的聚类索引centers
:一个数组,其中包含每个集群的中心compactness
:每个点到其相应中心的距离的平方之和
在本节中,我们将看到两个如何在 OpenCV 中使用 K 均值聚类算法的示例。
在第一个示例中,期望实现对 K 均值聚类的直观理解,而在第二个示例中,K 均值聚类将应用于颜色量化问题。
了解 K 均值聚类
在此示例中,我们将使用 K 均值聚类算法对一组 2D 点进行聚类。 这组 2D 点可以看作是对象的集合,已使用两个特征对其进行了描述。 可以使用k_means_clustering_data_visualization.py
脚本创建和显示这组 2D 点。
下一个屏幕截图中可以看到此脚本的输出:
这组 2D 点由150
点组成,它们是通过以下方式创建的:
data = np.float32(np.vstack(
(np.random.randint(0, 40, (50, 2)), np.random.randint(30, 70, (50, 2)), np.random.randint(60, 100, (50, 2)))))
这将代表用于聚类的数据。 如前所述,它应为np.float32
类型,并且每个特征都应放在单个列中。
在这种情况下,每个点都有对应于(x, y)
坐标的两个特征。 这些坐标可以表示例如每个150
人的身高和体重,或每个150
房屋的卧室数量和大小。 在第一种情况下,K 均值聚类算法将决定 T 恤的尺寸(例如,如果K=3
为小,中或大),而在第二种情况下,K 均值聚类算法将决定房子的价格(例如K = 4
,便宜,中等,昂贵或非常昂贵)。 总之,data
将是我们的聚类算法的输入。
在接下来的脚本中,我们将看到如何使用K
的不同值及其相应的可视化效果对其进行聚类。 为此,我们编写了三个脚本:
k_means_clustering_k_2.py
:在此脚本中,data
已分为两个组(K = 2
)。k_means_clustering_k_3.py
:在此脚本中,data
已分为三个组(K = 3
)。k_means_clustering_k_4.py
:在此脚本中,data
已分为四个组(K = 4
)。
在k_means_clustering_k_2.py
脚本中,数据已集群为2
集群。 第一步是定义算法终止标准。 在这种情况下,最大迭代次数设置为20
(max_iterm = 20
),ε设置为1.0
(epsilon = 1.0
):
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 20, 1.0)
下一步是使用cv2.kmeans()
函数应用 K 均值算法:
ret, label, center = cv2.kmeans(data, 2, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS)
此时,我们可以使用label
输出分离数据,该输出存储每个样本的聚类索引。 因此,我们可以根据标签将数据分为不同的群集:
A = data[label.ravel() == 0]
B = data[label.ravel() == 1]
最后一步是在不进行聚类的情况下绘制A
和B
以及原始的data
,以便更好地了解聚类过程:
# Create the dimensions of the figure and set title:
fig = plt.figure(figsize=(12, 6))
plt.suptitle("K-means clustering algorithm", fontsize=14, fontweight='bold')
fig.patch.set_facecolor('silver')
# Plot the 'original' data:
ax = plt.subplot(1, 2, 1)
plt.scatter(data[:, 0], data[:, 1], c='c')
plt.title("data")
# Plot the 'clustered' data and the centroids
ax = plt.subplot(1, 2, 2)
plt.scatter(A[:, 0], A[:, 1], c='b')
plt.scatter(B[:, 0], B[:, 1], c='g')
plt.scatter(center[:, 0], center[:, 1], s=100, c='m', marker='s')
plt.title("clustered data and centroids (K = 2)")
# Show the Figure:
plt.show()
下一个屏幕截图中可以看到此脚本的输出:
您可以看到我们还绘制了center
,它是一个包含每个群集中心的数组。
在k_means_clustering_k_3.py
脚本中,采用了相同的步骤对数据进行聚类,但是我们决定将数据分组为3
聚类(K = 3
)。 因此,在调用cv2.kmeans()
函数时,K
参数设置为3
:
ret, label, center = cv2.kmeans(data, 3, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS)
此外,使用label
输出分离数据时,将获得三组:
A = data[label.ravel() == 0]
B = data[label.ravel() == 1]
C = data[label.ravel() == 2]
最后一步是显示A
,B
和C
,以及质心和原始数据:
# Create the dimensions of the figure and set title:
fig = plt.figure(figsize=(12, 6))
plt.suptitle("K-means clustering algorithm", fontsize=14, fontweight='bold')
fig.patch.set_facecolor('silver')
# Plot the 'original' data:
ax = plt.subplot(1, 2, 1)
plt.scatter(data[:, 0], data[:, 1], c='c')
plt.title("data")
# Plot the 'clustered' data and the centroids
ax = plt.subplot(1, 2, 2)
plt.scatter(A[:, 0], A[:, 1], c='b')
plt.scatter(B[:, 0], B[:, 1], c='g')
plt.scatter(C[:, 0], C[:, 1], c='r')
plt.scatter(center[:, 0], center[:, 1], s=100, c='m', marker='s')
plt.title("clustered data and centroids (K = 3)")
# Show the Figure:
plt.show()
在上一个代码段中,我们在同一图中绘制了原始数据和“聚类”数据以及质心。 下一个屏幕截图中可以看到此脚本的输出:
为了完整起见,我们还对k_means_clustering_k_4.py
脚本进行了编码,其输出可以在下一个屏幕截图中看到:
可以看出,群集的数量被设置为4
(K = 4
)。
使用 K 均值聚类的颜色量化
在本小节中,我们将 K 均值聚类算法应用于颜色量化问题,可以将其定义为减少图像中颜色数量的过程。 对于在只能显示有限数量的颜色(通常是由于内存限制)的某些设备上显示图像,颜色量化是至关重要的一点。 因此,通常需要在相似度和颜色数量减少之间进行权衡。 这种权衡是通过正确设置K
参数来建立的,我们将在下面的示例中看到。
在k_means_color_quantization.py
脚本中,我们执行 K 均值聚类算法以执行颜色量化。 在这种情况下,数据的每个元素都由3
特征组成,这些特征对应于图像每个像素的B
,G
和R
值。 因此,关键步骤是通过以下方式将图像转换为data
:
data = np.float32(image).reshape((-1, 3))
在这里,image
是我们先前加载的图像。
在此脚本中,我们使用K
(3
,5
,10
,20
和40
)的几个值执行了聚类过程,以查看生成的图像如何变化。 例如,如果我们希望生成的图像仅具有3
颜色(K = 3
),则必须执行以下操作:
- 加载 BGR 图片:
img = cv2.imread('landscape_1.jpg')
- 使用
color_quantization()
函数执行色彩量化:
color_3 = color_quantization(img, 3)
- 同时显示两个图像以查看结果。
color_quantization()
函数执行颜色量化过程:
def color_quantization(image, k):
"""Performs color quantization using K-means clustering algorithm"""
# Transform image into 'data':
data = np.float32(image).reshape((-1, 3))
# print(data.shape)
# Define the algorithm termination criteria (maximum number of iterations and/or required accuracy):
# In this case the maximum number of iterations is set to 20 and epsilon = 1.0
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 20, 1.0)
# Apply K-means clustering algorithm:
ret, label, center = cv2.kmeans(data, k, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS)
# At this point we can make the image with k colors
# Convert center to uint8:
center = np.uint8(center)
# Replace pixel values with their center value:
result = center[label.flatten()]
result = result.reshape(img.shape)
return result
在上一个函数中,关键是要使用cv2.kmeans()
方法。 最后,我们可以用k
种颜色构建图像,用每种颜色的像素值替换其相应的中心值。 下一个屏幕截图中可以看到此脚本的输出:
可以将先前的脚本扩展为包括有趣的功能,该功能显示分配给每个中心值的像素数。 可以在k_means_color_quantization_distribution.py
脚本中看到。
color_quantization()
函数已被修改为包括以下功能:
def color_quantization(image, k):
"""Performs color quantization using K-means clustering algorithm"""
# Transform image into 'data':
data = np.float32(image).reshape((-1, 3))
# print(data.shape)
# Define the algorithm termination criteria (the maximum number of iterations and/or the desired accuracy):
# In this case the maximum number of iterations is set to 20 and epsilon = 1.0
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 20, 1.0)
# Apply K-means clustering algorithm:
ret, label, center = cv2.kmeans(data, k, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS)
# At this point we can make the image with k colors
# Convert center to uint8:
center = np.uint8(center)
# Replace pixel values with their center value:
result = center[label.flatten()]
result = result.reshape(img.shape)
# Build the 'color_distribution' legend.
# We will use the number of pixels assigned to each center value:
counter = collections.Counter(label.flatten())
print(counter)
# Calculate the total number of pixels of the input image:
total = img.shape[0] * img.shape[1]
# Assign width and height to the color_distribution image:
desired_width = img.shape[1]
# The difference between 'desired_height' and 'desired_height_colors'
# will be the separation between the images
desired_height = 70
desired_height_colors = 50
# Initialize the color_distribution image:
color_distribution = np.ones((desired_height, desired_width, 3), dtype="uint8") * 255
# Initialize start:
start = 0
for key, value in counter.items():
# Calculate the normalized value:
value_normalized = value / total * desired_width
# Move end to the right position:
end = start + value_normalized
# Draw rectangle corresponding to the current color:
cv2.rectangle(color_distribution, (int(start), 0), (int(end), desired_height_colors), center[key].tolist(), -1)
# Update start:
start = end
return np.vstack((color_distribution, result))
如您所见,我们利用collections.Counter()
来计算分配给每个中心值的像素数:
counter = collections.Counter(label.flatten())
例如,如果K = 3
— Counter({0: 175300, 2: 114788, 1: 109912})
。 构建颜色分布图像后,最后一步是将两个图像连接起来:
np.vstack((color_distribution, result))
下一个屏幕截图中可以看到此脚本的输出:
在上一个屏幕截图中,您可以看到使用 K 均值聚类算法通过更改参数k
(3
,5
,10
,20
和40
)应用颜色量化的结果。 k
的值越大,表示图像越真实。
K 最近邻
K 最近邻(kNN)被认为是有监督学习类别中最简单的算法之一。 kNN 可用于分类和回归问题。 在训练阶段,kNN 同时存储所有训练样本的特征向量和类别标签。 在分类阶段,将未标记向量(与训练示例位于同一多维特征空间中的查询或测试向量)分类为最接近要分类的未标记向量的k
个训练样本中最频繁的类别标签,其中k
是用户定义的常数。
在下图中可以以图形方式看到:
在上图中,如果k = 3
,则绿色圆圈(未标记的测试样本)将归类为三角形,因为在内圈内有两个三角形,并且只有一个正方形 。 如果k = 5
,则绿色圆圈将归类为正方形,因为虚线圆内只有三个正方形,而只有两个三角形。
在 OpenCV 中,使用此分类器的第一步是创建分类器。 cv2.ml.KNearest_create()
方法创建一个空的 kNN 分类器,应使用train()
方法对其进行训练,以同时提供数据和标签。 最后,findNearest()
方法用于查找邻居。 此方法的签名如下:
retval, results, neighborResponses, dist=cv2.ml_KNearest.findNearest(samples, k[, results[, neighborResponses[, dist]]])
这里,samples
是按行存储的输入样本,k
设置最近邻的数量(应大于 1),results
存储每个输入样本的预测,neighborResponses
存储相应的邻居,并且 dist
存储从输入样本到相应邻居的距离。
在本节中,我们将看到两个示例,以了解如何在 OpenCV 中使用 kNN 算法。 在第一个示例中,有望实现对 kNN 的直观理解,而在第二个示例中,kNN 将应用于手写数字识别问题。
了解 K 最近邻
knn_introduction.py
脚本对 kNN 进行了简单介绍,其中随机创建了一组点并分配了一个标签(0
或1
)。 标签0
将代表红色三角形,而标签1
将代表蓝色正方形。 我们将使用 kNN 算法基于k
最近邻对样本点进行分类。
因此,第一步是创建带有相应标签的点集和用于分类的样本点:
# The data is composed of 16 points:
data = np.random.randint(0, 100, (16, 2)).astype(np.float32)
# We create the labels (0: red, 1: blue) for each of the 16 points:
labels = np.random.randint(0, 2, (16, 1)).astype(np.float32)
# Create the sample point to be classified:
sample = np.random.randint(0, 100, (1, 2)).astype(np.float32)
下一步是创建 kNN 分类器,训练分类器,并找到k
最近的邻居:
# KNN creation:
knn = cv2.ml.KNearest_create()
# KNN training:
knn.train(data, cv2.ml.ROW_SAMPLE, labels)
# KNN find nearest:
k = 3
ret, results, neighbours, dist = knn.findNearest(sample, k)
# Print results:
print("result: {}".format(results))
print("neighbours: {}".format(neighbours))
print("distance: {}".format(dist))
在这种情况下,并且与以下屏幕截图相对应,获得的结果如下:
result: [[0.]]
neighbours: [[0\. 0\. 0.]]
distance: [[ 80\. 100\. 196.]]
因此,绿点被分类为红色三角形。 在下图中可以看到:
前面的屏幕快照使您对 kNN 有了直观的了解。 在下一个示例中,我们将把 kNN 应用于手写数字识别问题。
使用 K 最近邻识别手写数字
我们将看到如何使用 kNN 分类器执行手写数字识别。 我们将从获得可接受的准确率的基本脚本开始,我们将对其进行修改以提高其表现。
在这些脚本中,训练数据由手写数字组成。 OpenCV 提供了很多大图像,里面没有手写数字,而不是包含很多图像。 该图像的尺寸为2,000 x 1,000
像素。 每个数字为20 x 20
像素。 因此,我们总共有 5,000 位数(100 x 50
):
在knn_handwritten_digits_recognition_introduction.py
脚本中,我们将执行第一种方法,尝试使用 kNN 分类器识别数字。 在第一种方法中,我们将使用原始像素值作为特征。 这样,每个描述符的大小将为 400(20 x 20
)。
第一步是从大图像中加载所有数字,并为每个数字分配相应的标签。 这是通过load_digits_and_labels()
函数执行的:
digits, labels = load_digits_and_labels('digits.png')
load_digits_and_labels()
函数的代码如下:
def load_digits_and_labels(big_image):
"""Returns all the digits from the 'big' image and creates the corresponding labels for each image"""
# Load the 'big' image containing all the digits:
digits_img = cv2.imread(big_image, 0)
# Get all the digit images from the 'big' image:
number_rows = digits_img.shape[1] / SIZE_IMAGE
rows = np.vsplit(digits_img, digits_img.shape[0] / SIZE_IMAGE)
digits = []
for row in rows:
row_cells = np.hsplit(row, number_rows)
for digit in row_cells:
digits.append(digit)
digits = np.array(digits)
# Create the labels for each image:
labels = np.repeat(np.arange(NUMBER_CLASSES), len(digits) / NUMBER_CLASSES)
return digits, labels
在上一个函数中,我们首先加载“大”图像,然后,获取其中的所有数字。 上一个函数的最后一步是为每个数字创建标签。
在脚本中执行的下一步是为每个图像计算描述符。 在这种情况下,原始像素是特征描述符:
# Compute the descriptors for all the images.
# In this case, the raw pixels are the feature descriptors
raw_descriptors = []
for img in digits:
raw_descriptors.append(np.float32(raw_pixels(img)))
raw_descriptors = np.squeeze(raw_descriptors)
此时,我们将数据分为训练和测试(各占 50%)。 因此,将使用 2500 位数字来训练分类器,并使用 2500 位数字来测试经过训练的分类器:
partition = int(0.5 * len(raw_descriptors))
raw_descriptors_train, raw_descriptors_test = np.split(raw_descriptors, [partition])
labels_train, labels_test = np.split(labels, [partition])
现在,我们可以使用knn.train()
方法训练 kNN 模型,并使用get_accuracy()
函数对其进行测试:
# Train the KNN model:
print('Training KNN model - raw pixels as features')
knn = cv2.ml.KNearest_create()
knn.train(raw_descriptors_train, cv2.ml.ROW_SAMPLE, labels_train)
# Test the created model:
k = 5
ret, result, neighbours, dist = knn.findNearest(raw_descriptors_test, k)
# Compute the accuracy:
acc = get_accuracy(result, labels_test)
print("Accuracy: {}".format(acc))
如我们所见,k = 5
。 我们获得92.60
的精度,但我认为它可以提高。
我们要做的第一件事就是尝试使用k
的不同值,这是 kNN 分类器中的关键参数。 此修改在knn_handwritten_digits_recognition_k.py
脚本中执行。
在此脚本中,我们将创建一个字典来存储在测试k
的不同值时的准确率:
results = defaultdict(list)
请注意,我们已经从collections
导入了defaultdict
:
from collections import defaultdict
下一步是计算knn.findNearest()
方法,改变k
参数(在这种情况下,在(1-9)
的范围内)并将结果存储在字典中:
for k in np.arange(1, 10):
ret, result, neighbours, dist = knn.findNearest(raw_descriptors_test, k)
acc = get_accuracy(result, labels_test)
print(" {}".format("%.2f" % acc))
results['50'].append(acc)
最后一步是绘制结果:
# Show all results using matplotlib capabilities:
fig, ax = plt.subplots(1, 1)
ax.set_xlim(0, 10)
dim = np.arange(1, 10)
for key in results:
ax.plot(dim, results[key], linestyle='--', marker='o', label="50%")
plt.legend(loc='upper left', title="% training")
plt.title('Accuracy of the KNN model varying k')
plt.xlabel("number of k")
plt.ylabel("accuracy")
plt.show()
为了显示结果,我们让您使用 matplotlib 功能来绘制图形。 下一个屏幕截图中可以看到此脚本的输出:
如您在上一个屏幕截图中所见,通过更改k
参数获得的精度为-k=1
-93.72
,k=2
-91.96
,k=3
-93.00
,k=4
-92.64
,k=5
-92.60
,k=6
-92.40
,k=7
-92.28
,k=8
-92.44
和k=9
-91.96
。
如前所述,获得的精度存在一些差异。 因此,不要忘记在应用中适当调整k
参数。
在这些示例中,我们一直在训练和测试每个 2500 位数字的模型。
在机器学习中,使用更多数据训练分类器通常是一个好主意,因为分类器可以更好地学习特征的结构。 结合 kNN 分类器,增加训练数字的数量也将增加在特征空间中找到测试数据正确匹配的可能性。
在knn_handwritten_digits_recognition_k_training_testing.py
脚本中,我们修改了图像的百分比以训练和测试模型,如下所示:
# Split data into training/testing:
split_values = np.arange(0.1, 1, 0.1)
for split_value in split_values:
# Split the data into training and testing:
partition = int(split_value * len(raw_descriptors))
raw_descriptors_train, raw_descriptors_test = np.split(raw_descriptors, [partition])
labels_train, labels_test = np.split(labels, [partition])
# Train KNN model
print('Training KNN model - raw pixels as features')
knn.train(raw_descriptors_train, cv2.ml.ROW_SAMPLE, labels_train)
# Store the accuracy when testing:
for k in np.arange(1, 10):
ret, result, neighbours, dist = knn.findNearest(raw_descriptors_test, k)
acc = get_accuracy(result, labels_test)
print(" {}".format("%.2f" % acc))
results[int(split_value * 100)].append(acc)
可以看出,训练算法的数字百分比为 10%,20%,...,90%,测试算法的数字百分比为 90%,80%,...,10%。
最后,我们绘制结果:
# Show all results using matplotlib capabilities:
# Create the dimensions of the figure and set title:
fig = plt.figure(figsize=(12, 5))
plt.suptitle("KNN handwritten digits recognition", fontsize=14, fontweight='bold')
fig.patch.set_facecolor('silver')
ax = plt.subplot(1, 1, 1)
ax.set_xlim(0, 10)
dim = np.arange(1, 10)
for key in results:
ax.plot(dim, results[key], linestyle='--', marker='o', label=str(key) + "%")
plt.legend(loc='upper left', title="% training")
plt.title('Accuracy of the KNN model varying both k and the percentage of images to train/test')
plt.xlabel("number of k")
plt.ylabel("accuracy")
plt.show()
下一个屏幕截图中可以看到knn_handwritten_digits_recognition_k_training_testing.py
脚本的输出:
随着训练图像数量的增加,准确率也会提高。 此外,当我们用 90% 的数字训练分类器时,我们将用剩余的 10% 的数字测试分类器,这等效于用500
数字测试分类器,这是一个可观的数字。
到目前为止,我们一直在使用原始像素值作为特征来训练分类器。 在机器学习中,训练分类器之前的常见过程是对输入数据进行某种预处理,以在训练时帮助分类器。 在knn_handwritten_digits_recognition_k_training_testing_preprocessing.py
脚本中,我们正在进行预处理,以减少输入数字的可变性。
此预处理在deskew()
函数中执行:
def deskew(img):
"""Pre-processing of the images"""
m = cv2.moments(img)
if abs(m['mu02']) < 1e-2:
return img.copy()
skew = m['mu11'] / m['mu02']
M = np.float32([[1, skew, -0.5 * SIZE_IMAGE * skew], [0, 1, 0]])
img = cv2.warpAffine(img, M, (SIZE_IMAGE, SIZE_IMAGE), flags=cv2.WARP_INVERSE_MAP | cv2.INTER_LINEAR)
return img
deskew()
函数通过使用二阶矩来使数字偏斜。 更具体地,可以通过两个中心矩之比(mu11/mu02
)来计算偏斜的量度。 计算出的偏斜度用于计算仿射变换,从而使数字偏斜。 请参阅下一个屏幕截图,以欣赏此预处理的效果。 屏幕截图的顶部显示了原始数字(蓝色边框),屏幕截图的底部显示了预处理的数字(绿色边框):
通过应用此预处理,可以提高识别率,如下面的屏幕快照所示,图中绘制了识别率:
如果将在输入数据中执行预处理的此脚本与不执行任何预处理的前一个脚本进行比较,则可以看到整体准确率有所提高。
在所有这些脚本中,我们一直使用原始像素值作为特征描述符。 在机器学习中,一种常见的方法是使用更高级的描述符。 定向梯度直方图(HOG)是一种流行的图像描述符。
特征描述符是图像的表示,通过提取描述基本特征(例如形状,颜色,纹理或运动)的有用信息来简化图像。 通常,特征描述符将图像转换为长度为n
的特征向量/数组。
HOG 是一种用于计算机视觉的流行特征描述符,最早用于人类在静态图像中的检测。 在knn_handwritten_digits_recognition_k_training_testing_preprocessing_hog.py
脚本中,我们将使用 HOG 特征代替原始像素值。
我们定义了get_hog()
函数,该函数获取 HOG 描述符:
def get_hog():
"""Get hog descriptor"""
# cv2.HOGDescriptor(winSize, blockSize, blockStride, cellSize, nbins, derivAperture, winSigma, histogramNormType,
# L2HysThreshold, gammaCorrection, nlevels, signedGradient)
hog = cv2.HOGDescriptor((SIZE_IMAGE, SIZE_IMAGE), (8, 8), (4, 4), (8, 8), 9, 1, -1, 0, 0.2, 1, 64, True)
print("hog descriptor size: '{}'".format(hog.getDescriptorSize()))
return hog
在这种情况下,每个图像的特征描述符都是144
大小。 为了计算每个图像的 HOG 描述符,我们必须执行以下操作:
# Compute the descriptors for all the images.
# In this case, the HoG descriptor is calculated
hog_descriptors = []
for img in digits:
hog_descriptors.append(hog.compute(deskew(img)))
hog_descriptors = np.squeeze(hog_descriptors)
如您所见,我们将hog.compute()
应用于每个不倾斜的数字。
结果可以在下一个屏幕截图中看到:
当k=2
和 90% 的数字用于训练,而 10% 的数字用于测试时,则达到 98.60% 的精度。 因此,我们将识别率从 92.60%(在本小节的第一个脚本中获得)提高到 98.60%(在上一个脚本中获得)。
在编写机器学习模型和应用时,一种好的方法是从一个基本近似开始,尝试尽快解决该问题。 然后,如果获得的精度不够好,可以通过添加更好的预处理,更高级的特征描述符或其他机器学习技术来迭代地改进模型。 最后,如果必要,不要忘记收集更多数据来训练和测试模型。
支持向量机
支持向量机(SVM)是一种监督式学习技术,通过根据分配的类最佳地分离训练示例,在高维空间中构造一个超平面或一组超平面 。
可以在下一张图中看到,其中绿线表示最能将两个类别分开的超平面的表示,因为到两个类别中每个类别的最近元素的距离最大:
在第一种情况下,决策边界是一条线,而在第二种情况下,决策边界是一条圆周。 虚线和虚线虚线表示其他决策边界,但是它们不能最好地将这两个类别分开。
OpenCV 中的 SVM 实现基于《LIBSVM:支持向量机库》(2011)。 要创建空模型,请使用cv2.ml.SVM_create()
函数。 接下来,应将主要参数分配给模型:
svmType
:这设置 SVM 的类型。 有关详细信息,请参见 LibSVM。 可能的值如下:SVM_C_SVC
:可用于n
类分类的 C-支持向量分类(n ≥ 2
)NU_SVC
:ν-支持向量分类ONE_CLASS
:分布估计(一类 SVM)EPS_SVR
:ε-支持向量回归NU_SVR
:ν-支持向量回归
kernelType
:设置 SVM 的核类型。 有关详细信息,请参见 LibSVM。 可能的值如下:LINEAR
:线性核POLY
:多项式核RBF
:径向基函数(RBF),在大多数情况下是个不错的选择SIGMOID
:Sigmoid 核CHI2
:指数 Chi2 核,类似于RBF
核INTER
:直方图交点核; 快速核
核函数选择可能很棘手,并且取决于数据集。 从这个意义上讲,RBF
核通常被认为是一个不错的首选,因为该核将样本非线性地映射到一个高维空间,以处理类标签和属性之间的关系为非线性的情况。 有关更多详细信息,请参见《支持向量分类的实用指南》(2003)。
-
degree
:核函数的参数度(POLY
) -
gamma
:核函数的γ
参数(POLY
/RBF
/SIGMOID
/CHI2
) -
coef0
:核函数的coef0
参数(POLY
/SIGMOID
) -
Cvalue
:SVM 优化问题的C
参数(C_SVC
/EPS_SVR
/NU_SVR
) -
nu
:SVM 优化问题的ν
参数(NU_SVC
/ONE_CLASS
/NU_SVR
) -
p
:SVM 优化问题(EPS_SVR
)的ε
参数 -
classWeights
:C_SVC
问题中的可选权重,分配给特定类别 -
termCrit
:SVM 迭代训练过程的终止标准
默认构造器使用以下值初始化结构:
svmType: C_SVC, kernelType: RBF, degree: 0, gamma: 1, coef0: 0, C: 1, nu: 0, p: 0, classWeights: 0, termCrit: TermCriteria(MAX_ITER+EPS, 1000, FLT_EPSILON )
在本节中,我们将看到两个如何在 OpenCV 中使用 SVM 的示例。 在第一个示例中,将给出对 SVM 的直观理解,在第二个示例中,SVM 将应用于手写数字识别问题。
了解 SVM
svm_introduction.py
脚本执行一个简单的示例,以了解如何在 OpenCV 中使用 SVM。 首先,我们创建训练数据和标签:
# Set up training data:
labels = np.array([1, 1, -1, -1, -1])
data = np.matrix([[500, 10], [550, 100], [300, 10], [500, 300], [10, 600]], dtype=np.float32)
如您所见,创建了五个点。 前两个点被分配为1
类,而其他三个点被分配为-1
类。 下一步是使用svm_init()
函数初始化 SVM 模型:
# Initialize the SVM model:
svm_model = svm_init(C=12.5, gamma=0.50625)
svm_init()
函数创建一个空模型并分配主要参数并返回模型:
def svm_init(C=12.5, gamma=0.50625):
"""Creates empty model and assigns main parameters"""
model = cv2.ml.SVM_create()
model.setGamma(gamma)
model.setC(C)
model.setKernel(cv2.ml.SVM_LINEAR)
model.setType(cv2.ml.SVM_C_SVC)
model.setTermCriteria((cv2.TERM_CRITERIA_MAX_ITER, 100, 1e-6))
return model
在这种情况下,SVM 核类型设置为LINEAR
(不执行任何映射),而 SVM 的类型设置为C_SVC
(可用于n
类分类,其中n ≥ 2
)。
然后,我们使用svm_train()
函数训练 SVM:
# Train the SVM:
svm_train(svm_model, data, labels)
在这里,svm_train()
函数使用样本和响应来训练模型,然后返回训练后的模型:
def svm_train(model, samples, responses):
"""Trains the model using the samples and the responses"""
model.train(samples, cv2.ml.ROW_SAMPLE, responses)
return model
下一步是创建将在其中绘制 SVM 响应的图像:
# Create the canvas (black image with three channels)
# This image will be used to show the prediction for every pixel:
img_output = np.zeros((640, 640, 3), dtype="uint8")
最后,我们使用show_svm_response()
函数显示 SVM 响应:
# Show the SVM response:
show_svm_response(svm_model, img_output)
因此,img_ouput
图像显示了 SVM 响应。 show_svm_response()
函数的代码如下:
def show_svm_response(model, image):
"""Show the prediction for every pixel of the image, the training data and the support vectors"""
colors = {1: (255, 255, 0), -1: (0, 255, 255)}
# Show the prediction for every pixel of the image:
for i in range(image.shape[0]):
for j in range(image.shape[1]):
sample = np.matrix([[j, i]], dtype=np.float32)
response = svm_predict(model, sample)
image[i, j] = colors[response.item(0)]
# Show the training data:
# Show samples with class 1:
cv2.circle(image, (500, 10), 10, (255, 0, 0), -1)
cv2.circle(image, (550, 100), 10, (255, 0, 0), -1)
# Show samples with class -1:
cv2.circle(image, (300, 10), 10, (0, 255, 0), -1)
cv2.circle(image, (500, 300), 10, (0, 255, 0), -1)
cv2.circle(image, (10, 600), 10, (0, 255, 0), -1)
# Show the support vectors:
support_vectors = model.getUncompressedSupportVectors()
for i in range(support_vectors.shape[0]):
cv2.circle(image, (support_vectors[i, 0], support_vectors[i, 1]), 15, (0, 0, 255), 6)
可以看出,该函数显示以下内容:
- 图像每个像素的预测
- 所有五个训练数据点
- 支持向量(定义超平面的向量称为支持向量)
下一个屏幕截图中可以看到此脚本的输出:
如您所见,已使用训练数据和由五个点组成的标签(对两个点分配了类别1
,而对其他三个点分配了类别-1
)对 SVM 进行了训练,之后将其用于分类图片中的所有像素。 这种分类导致将图像划分为黄色和青色区域。 此外,您可以看到两个区域之间的边界对应于两个类别之间的最佳间隔,因为到两个类别中每个类别的最近元素的距离最大。 支持向量以红线边框显示。
使用 SVM 的手写数字识别
我们刚刚看到了如何使用 kNN 分类器执行手写数字识别。 通过对数字进行预处理(调用deskew()
函数)并计算 HOG 描述符作为用于描述每个数字的特征向量,可以获得最佳的精度。 因此,为简单起见,接下来将要使用 SVM 对数字进行分类的脚本将使用上述近似值(预处理和 HOG 特征)。
svm_handwritten_digits_recognition_preprocessing_hog.py
脚本使用 SVM 分类执行手写数字识别。 关键代码如下所示:
# Load all the digits and the corresponding labels:
digits, labels = load_digits_and_labels('digits.png')
# Shuffle data
# Constructs a random number generator:
rand = np.random.RandomState(1234)
# Randomly permute the sequence:
shuffle = rand.permutation(len(digits))
digits, labels = digits[shuffle], labels[shuffle]
# HoG feature descriptor:
hog = get_hog()
# Compute the descriptors for all the images.
# In this case, the HoG descriptor is calculated
hog_descriptors = []
for img in digits:
hog_descriptors.append(hog.compute(deskew(img)))
hog_descriptors = np.squeeze(hog_descriptors)
# At this point we split the data into training and testing (50% for each one):
partition = int(0.5 * len(hog_descriptors))
hog_descriptors_train, hog_descriptors_test = np.split(hog_descriptors, [partition])
labels_train, labels_test = np.split(labels, [partition])
print('Training SVM model ...')
model = svm_init(C=12.5, gamma=0.50625)
svm_train(model, hog_descriptors_train, labels_train)
print('Evaluating model ... ')
svm_evaluate(model, hog_descriptors_test, labels_test)
在这种情况下,我们使用了RBF
核:
def svm_init(C=12.5, gamma=0.50625):
"""Creates empty model and assigns main parameters"""
model = cv2.ml.SVM_create()
model.setGamma(gamma)
model.setC(C)
model.setKernel(cv2.ml.SVM_RBF)
model.setType(cv2.ml.SVM_C_SVC)
model.setTermCriteria((cv2.TERM_CRITERIA_MAX_ITER, 100, 1e-6))
return model
使用仅 50% 的数字来训练算法,所获得的精度为 98.60%。
另外,使用RBF
核时,有两个重要参数-C
和γ
。 在这种情况下,为C=12.5
和γ=0.50625
。 和以前一样,对于给定的问题(取决于数据集),C
和γ
并不为人所知。 因此,必须进行某种参数搜索。 因此,目标是确定推荐使用C
和γ
的网格搜索的良好(C
和γ
)。
与svm_handwritten_digits_recognition_preprocessing_hog.py
脚本相比,在svm_handwritten_digits_recognition_preprocessing_hog_c_gamma.py
脚本中进行了两次修改。 第一个是使用 90% 的数字训练模型,其余 10% 用于测试。 第二个修改是对C
和γ
进行网格搜索:
# Create a dictionary to store the accuracy when testing:
results = defaultdict(list)
for C in [1, 10, 100, 1000]:
for gamma in [0.1, 0.3, 0.5, 0.7, 0.9, 1.1, 1.3, 1.5]:
model = svm_init(C, gamma)
svm_train(model, hog_descriptors_train, labels_train)
acc = svm_evaluate(model, hog_descriptors_test, labels_test)
print(" {}".format("%.2f" % acc))
results[C].append(acc)
最后,这是结果:
# Create the dimensions of the figure and set title:
fig = plt.figure(figsize=(10, 6))
plt.suptitle("SVM handwritten digits recognition", fontsize=14, fontweight='bold')
fig.patch.set_facecolor('silver')
# Show all results using matplotlib capabilities:
ax = plt.subplot(1, 1, 1)
ax.set_xlim(0, 1.5)
dim = [0.1, 0.3, 0.5, 0.7, 0.9, 1.1, 1.3, 1.5]
for key in results:
ax.plot(dim, results[key], linestyle='--', marker='o', label=str(key))
plt.legend(loc='upper left', title="C")
plt.title('Accuracy of the SVM model varying both C and gamma')
plt.xlabel("gamma")
plt.ylabel("accuracy")
plt.show()
下一个屏幕截图中可以看到此脚本的输出:
如图所示,在某些情况下,可获得 99.20% 的精度。
通过比较 kNN 分类器和 SVM 进行手写数字识别,我们可以得出结论,SVM 优于 kNN 分类器。
总结
在本章中,我们涵盖了机器学习的完整介绍。
在第一部分中,我们将机器学习的概念以及它与其他热门话题(如人工智能,神经网络和深度学习)的关联性进行了背景研究。 此外,我们总结了机器学习的三种主要方法,并讨论了解决分类,回归和聚类问题的三种最常用的技术。然后,我们应用了最常用的机器学习技术来解决了一些现实世界中的问题。 更具体地说,我们研究了 K 均值聚类算法,K 最近邻分类器和 SVM。
在下一章中,我们将探讨如何使用与人脸检测,跟踪和识别相关的最新算法来创建人脸处理项目。
问题
- 机器学习中的三种主要方法是什么?
- 分类和回归问题有什么区别?
- OpenCV 提供什么函数来实现 K 均值聚类算法?
- OpenCV 提供什么函数来创建 kNN 分类器?
- OpenCV 提供什么函数来找到最近的邻居?
- OpenCV 提供什么函数来创建 SVM 分类器?
- SVM 核的合理首选是什么?
进一步阅读
如果您想深入研究机器学习,请查看以下资源:
十一、人脸检测,跟踪和识别
人脸处理是人工智能领域的热门话题,因为可以使用计算机视觉算法从面部自动提取很多信息。 面部在视觉交流中起着重要作用,因为可以从人脸中提取大量非语言信息,例如身份,意图和情感。 对于计算机视觉学习者来说,面部处理是一个非常有趣的主题,因为它涉及到不同的专业领域,例如对象检测,图像处理,标志检测或对象跟踪。
在本章中,将向您介绍与使用最新算法和技术进行面部处理有关的主要主题,以达到令人印象深刻的效果。
我们将涵盖以下主题:
- 人脸处理简介
- 人脸检测
- 检测人脸标志
- 人脸追踪
- 人脸识别
在本章中,您将学习如何使用与人脸检测,跟踪和识别相关的最新算法来创建人脸处理项目。 在第 12 章,“深度学习简介”中,将向您介绍使用 OpenCV 进行深度学习的领域以及一些深度学习 Python 库(TensorFlow 和 Keras)。
技术要求
技术要求如下:
- Python 和 OpenCV
- 特定于 Python 的 IDE
- NumPy 和 Matplotlib 包
- Git 客户端
- Dlib 包
face_processing
包
有关如何安装这些要求的更多详细信息,请参见第 1 章,“设置 OpenCV”。 《精通 Python OpenCV 4》的 GitHub 存储库,其中包含贯穿本书的所有必要的支持项目文件,从第一章到最后一章,都可以在这里访问。
安装 Dlib
Dlib 是一个 C++ 软件库,包含计算机视觉,机器学习和深度学习算法。 Dlib 也可以在您的 Python 应用中使用。 为了使用 PIP 安装dlib
,请使用以下命令:
$ pip install dlib
另外,如果您想自己编译dlib
,请进入dlib
根文件夹并运行以下命令:
$ python setup.py install
该命令运行完毕后,就可以使用 Python 的dlib
。
请注意,您需要同时安装 CMake 和 C++ 编译器才能正常工作。 还要注意,各种可选功能(例如 GUI 支持(例如dlib.image_window
)和 CUDA 加速)将根据计算机上的可用状态启用或禁用。
安装dlib
的第三个选项是访问这里并安装所需的dlib
车轮包装。 就我而言,我已经下载了dlib-19.8.1-cp36-cp36m-win_amd64.whl
文件并使用以下命令进行了安装:
$ pip install dlib-19.8.1-cp36-cp36m-win_amd64.whl
车轮文件名是{distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl
。 例如,distribution-1.0-1-py27-none-any.whl
是名为distribution
的包的第一个版本,它在任何 CPU 架构上都与 Python 2.7(任何 Python 2.7 实现)兼容,而没有 ABI(纯 Python)。 请参阅这里了解有关 Wheel 二进制包格式的更多详细信息。
要确认安装是否正确执行,只需打开 Python shell 并尝试导入dlib
库:
python
import dlib
请记住,建议的方法是在虚拟环境中安装包。 有关如何创建和管理虚拟环境的信息,请参见第 1 章,“设置 OpenCV”。
例如,在这种情况下,我们将使用 Anaconda 提示符在虚拟环境中安装dlib
:
- 创建一个虚拟环境:
(base) $ conda create -n dlib-env python=3.6
- 激活环境:
(base) $ activate dlib-env
查看(dlib-env)
在此命令后的提示前的显示方式。 这表明虚拟环境已被激活。
- 使用以下命令安装
dlib
:
(dlib-env) $ pip install dlib
安装face_recognition
包
为了安装face_recognition
包 ,请执行以下命令:
$ pip install face_recognition
要检查安装是否正确执行,只需打开 Python shell 并尝试导入face_recognition
库:
python
import face_recognition
安装cvlib
包
要安装cvlib
包,请先安装所需的包(numpy
,opencv-python
,requests
,progressbar
, pillow
,tensorflow
,keras
),使用以下命令:
$ pip install -r requirements.txt
然后,安装cvlib
包:
$ pip install cvlib
要升级到最新版本,请输入以下命令:
pip install --upgrade cvlib
请注意,如果您使用的是 GPU,则可以编辑requirements.txt
文件以包括tensorflow-gpu
而不是tensorflow
。
要检查安装是否正确执行,只需打开 Python shell 并尝试导入face_recognition
库:
python
import cvlib
人脸处理简介
在本章中,我们将介绍与面部处理有关的主要主题。 为此,我们将使用 OpenCV 库,也将使用dlib
主页和 PyPI dlib
主页,Github dlib
主页,PyPI face_recognition
主页,Github face_recognition
主页和PyPI cvlib
主页,Github cvlib
主页,cvlib
主页 Python 包。 在上一节中,您了解了如何安装这些包。
为了介绍本章,我们将在所有部分中使用不同的方法来了解您解决具体的面部处理任务时可能会遇到的不同可能性,并且对所有这些都有一个较高的概述可能会有所帮助。 备择方案。
该图试图捕获前面提到的主题的概念:
如您所见,这里要解决四个要点:
- 人脸检测是对象检测的一种特殊情况,其任务是查找图像中所有人脸的位置和大小。
- 人脸标志检测是标志检测的一种特殊情况,其任务是在面部定位主要标志。
- 人脸跟踪是对象跟踪的一种特殊情况,其中的任务是通过考虑可在连续帧的帧中提取的额外信息来查找视频中所有移动脸的位置和大小。 视频。
- 人脸识别是对象识别的一种特殊情况,其中使用从人脸提取的信息从图像或视频中识别或验证人:
- 人脸识别(1:N):任务是在已知人脸的集合中查找与未知人物最接近的匹配项。
- 面部验证(1:1):该任务是检查该人是否是他们所声称的那个人。
如上图所示,本章将使用 OpenCV,dlib
,face_recognition
和cvlib
。
人脸检测
人脸检测可以定义为确定数字图像中人脸的位置和大小的任务,通常是构建人脸处理应用(例如,人脸表情识别,嗜睡检测,性别分类,人脸识别, 头姿势估计或人机交互)。 这是因为上述应用需要将所检测到的面部的位置和大小作为输入。 因此,自动面部检测起着至关重要的作用,并且是人工智能界研究最多的主题之一。
面部检测对于人类来说似乎是一项轻松的任务,但对计算机而言却是一项非常具有挑战性的任务,因为通常涉及许多问题/挑战(例如,外观变化,比例,旋转,面部表情,遮挡或光照条件)。 在 Viola 和 Jones 提出的工作之后,人脸检测取得了令人瞩目的进展。 在本节中,我们将看到 OpenCV 库以及dlib
和face_processing
包提供的一些最流行的面部检测技术,包括上述的 Viola 和 Jones 算法以及其他机器学习和深度学习方法。
使用 OpenCV 的人脸检测
OpenCV 提供了两种面部检测方法:
- 基于 Haar 级联的面部检测器
- 基于深度学习的面部检测器
Viola 和 Jones 提出的框架(请参见《使用简单特征的增强级联进行快速对象检测》(2001))是一种有效的对象检测方法。 该框架非常受欢迎,因为 OpenCV 提供了基于该框架的面部检测算法。 另外,该框架还可以用于检测其他物体而不是面部(例如,全身检测器,车牌号检测器,上身检测器或猫脸检测器)。 在本节中,我们将看到如何使用此框架检测人脸。
face_detection_opencv_haar.py
脚本使用基于 Haar 特征的级联分类器执行面部检测。 从这个意义上说,OpenCV 提供了四个用于(正面)人脸检测的级联分类器:
haarcascade_frontalface_alt.xml
(FA1):22 个阶段,20 x 20
Haar 特征haarcascade_frontalface_alt2.xml
(FA2):20 个阶段,20 x 20
Haar 特征haarcascade_frontalface_alt_tree.xml
(FAT):47 个阶段,20 x 20
Haar 特征haarcascade_frontalface_default.xml
(FD):25 个阶段,24 x 24
Haar 特征
在一些可用的出版物中,作者使用不同的标准和数据集评估了这些级联分类器的表现。 总体而言,可以得出结论,这些分类器达到了相似的准确率。 这就是为什么在此脚本中,我们将使用其中两个(以简化内容)。 更具体地说,在此脚本中,加载了两个层叠分类器(先前引入的FA2
和FD
):
# Load cascade classifiers:
cas_alt2 = cv2.CascadeClassifier("haarcascade_frontalface_alt2.xml")
cas_default = cv2.CascadeClassifier("haarcascade_frontalface_default.xml")
cv2.CascadeClassifier()
函数用于从文件加载分类器。 您可以从 OpenCV 信息库下载以下级联分类器文件。 此外,我们在 GitHub 存储库中包含了两个已加载的层叠分类器文件(haarcascade_frontalface_alt2.xml
和haarcascade_frontalface_default.xml
)。
下一步是执行检测:
faces_alt2 = cas_alt2.detectMultiScale(gray)
faces_default = cas_default.detectMultiScale(gray)
cv2.CascadeClassifier.detectMultiScale()
函数检测对象并将其作为矩形列表返回。 最后一步是使用show_detection()
函数关联结果:
img_faces_alt2 = show_detection(img.copy(), faces_alt2)
img_faces_default = show_detection(img.copy(), faces_default)
show_detection()
函数在每个检测到的面部上绘制一个矩形:
def show_detection(image, faces):
"""Draws a rectangle over each detected face"""
for (x, y, w, h) in faces:
cv2.rectangle(image, (x, y), (x + w, y + h), (255, 0, 0), 5)
return image
OpenCV 还提供cv2.face.getFacesHAAR()
函数来检测面部:
retval, faces_haar_alt2 = cv2.face.getFacesHAAR(img, "haarcascade_frontalface_alt2.xml")
retval, faces_haar_default = cv2.face.getFacesHAAR(img, "haarcascade_frontalface_default.xml")
应当注意,cv2.CascadeClassifier.detectMultiScale()
需要灰度图像,而cv2.face.getFacesHAAR()
需要 BGR 图像作为输入。 此外,cv2.CascadeClassifier.detectMultiScale()
将检测到的脸部输出为矩形列表。 例如,两个检测到的面部的输出将如下所示:
[[332 93 364 364] [695 104 256 256]]
cv2.face.getFacesHAAR()
函数以相似的格式返回人脸:
[[[298 524 61 61]] [[88 72 315 315]]
要摆脱无用的一维数组,请调用np.squeeze()
:
faces_haar_alt2 = np.squeeze(faces_haar_alt2)
faces_haar_default = np.squeeze(faces_haar_default)
用于检测和绘制已加载图像中的面部的完整代码如下:
# Load image and convert to grayscale:
img = cv2.imread("test_face_detection.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Load cascade classifiers:
cas_alt2 = cv2.CascadeClassifier("haarcascade_frontalface_alt2.xml")
cas_default = cv2.CascadeClassifier("haarcascade_frontalface_default.xml")
# Detect faces:
faces_alt2 = cas_alt2.detectMultiScale(gray)
faces_default = cas_default.detectMultiScale(gray)
retval, faces_haar_alt2 = cv2.face.getFacesHAAR(img, "haarcascade_frontalface_alt2.xml")
faces_haar_alt2 = np.squeeze(faces_haar_alt2)
retval, faces_haar_default = cv2.face.getFacesHAAR(img, "haarcascade_frontalface_default.xml")
faces_haar_default = np.squeeze(faces_haar_default)
# Draw face detections:
img_faces_alt2 = show_detection(img.copy(), faces_alt2)
img_faces_default = show_detection(img.copy(), faces_default)
img_faces_haar_alt2 = show_detection(img.copy(), faces_haar_alt2)
img_faces_haar_default = show_detection(img.copy(), faces_haar_default)
最后一步是使用 OpenCV 或 Matplotlib 在这种情况下显示四个创建的图像。 完整的代码可以在face_detection_opencv_haar.py
脚本中看到。 在以下屏幕截图中可以看到此脚本的输出:
如您所见,通过使用基于 Haar 特征的叶栅分类器,使用上述四个近似值,检测到的面部会发生变化。 最后,还应该指出,cv2.CascadeClassifier.detectMultiScale()
函数具有minSize
和maxSize
参数,以便确定最小大小(不会检测到小于minSize
的对象)和最大大小(不会检测到大于maxSize
的对象)。 相反,cv2.face.getFacesHAAR()
函数不提供这种可能性。
基于 Haar 特征的级联分类器可用于检测人脸以外的对象。 OpenCV 库还提供了两个用于猫脸检测的级联文件。
为了完整起见,cat_face_detection_opencv_haar.py
脚本加载了两个层叠文件,这些文件经过训练可以检测图像中的正面猫脸。 该脚本与face_detection_opencv_haar.py
脚本非常相似。 确实,关键的修改是已加载的两个层叠文件。 在这种情况下,这是两个已加载的层叠文件:
haarcascade_frontalcatface.xml
:一种正面猫脸检测器,使用具有 20 个阶段的基本 Haar 特征集和24 x 24
Haar 特征haarcascade_frontalcatface_extended.xml
:正面猫脸检测器,使用全套 20 个阶段的 Haar 特征和24 x 24
Haar 特征
有关这些级联文件的更多信息,请参阅 Joseph Howse 的《面向秘密特工的 OpenCV》。 您可以从 OpenCV 信息库下载以下级联分类器文件。 此外,我们已经在 GitHub 存储库中包含了这两个层叠分类器文件。
在以下屏幕截图中可以看到此脚本的输出:
此外,OpenCV 提供了基于深度学习的面部检测器。 更具体地说,OpenCV 深度神经网络(DNN)面部检测器基于单发多盒检测器(SSD)框架, ResNet-10 网络。
从 OpenCV 3.1 开始,提供了 DNN 模块,该模块使用流行的深度学习框架(例如 Caffe,TensorFlow,Torch 和 Darknet)使用经过预训练的深度网络来实现前向传递(推理)。 在 OpenCV 3.3 中,该模块已从opencv_contrib
存储库升级到主存储库,并得到了显着加速。 这意味着我们可以使用经过预训练的网络来执行完整的前向传递,并利用输出在我们的应用中进行预测,而不必花费数小时来训练网络。 在第 12 章,“深度学习简介”中,我们将进一步探索 DNN 模块; 在本章中,我们将重点介绍深度学习人脸检测器。
在本节中,我们将使用库中包含的预训练深度学习人脸检测器模型执行人脸检测。
OpenCV 为此面部检测器提供了两种模型:
- 人脸检测器(FP16):原始 Caffe 实现的浮点 16 版本(5.1 MB)
- 人脸检测器(UINT8):使用 TensorFlow 的 8 位量化版本(2.6 MB)
在每种情况下,您都需要两套文件:模型文件和配置文件。 对于 Caffe 模型,这些文件如下:
res10_300x300_ssd_iter_140000_fp16.caffemodel
:此文件包含实际层的权重。 可以从这里下载,它也包含在 GitHub 仓库中。deploy.prototxt
:此文件定义模型架构。 可以从这里下载,并包含在该书的 GitHub 存储库中。
如果您使用 TensorFlow 模型,则需要以下文件:
opencv_face_detector_uint8.pb
:此文件包含实际层的权重。 可以从这里下载该文件,该文件包含在本书的 GitHub 存储库中。opencv_face_detector.pbtxt
:此文件定义模型架构。 可以从这里下载,并包含在该书的 GitHub 存储库中。
face_detection_opencv_dnn.py
脚本向您展示如何通过使用面部检测和预训练的深度学习面部检测器模型来检测面部。 第一步是加载预训练的模型:
# Load pre-trained model:
net = cv2.dnn.readNetFromCaffe("deploy.prototxt", "res10_300x300_ssd_iter_140000_fp16.caffemodel")
# net = cv2.dnn.readNetFromTensorflow("opencv_face_detector_uint8.pb", "opencv_face_detector.pbtxt")
如您所见,在此示例中,原始 Caffe 实现的浮点 16 版本已加载。 为了获得最佳精度,我们必须在大小分别为300 x 300
的 BGR 图像上运行模型,方法是分别对蓝色,绿色和红色通道应用(104, 177, 123)
值的均值减法。 此预处理是使用cv2.dnn.blobFromImage()
OpenCV 函数执行的:
blob = cv2.dnn.blobFromImage(image, 1.0, (300, 300), [104., 117., 123.], False, False)
在第 12 章,“深度学习简介”中,我们将更深入地研究此函数。
下一步是将 BLOB 设置为输入以获取结果,并对整个网络执行前向传递以计算输出:
# Set the blob as input and obtain the detections:
net.setInput(blob)
detections = net.forward()
最后一步是遍历所有检测并得出结果,仅当相应的置信度大于固定的最小阈值时才考虑检测:
# Iterate over all detections:
for i in range(0, detections.shape[2]):
# Get the confidence (probability) of the current detection:
confidence = detections[0, 0, i, 2]
# Only consider detections if confidence is greater than a fixed minimum confidence:
if confidence > 0.7:
# Increment the number of detected faces:
detected_faces += 1
# Get the coordinates of the current detection:
box = detections[0, 0, i, 3:7] * np.array([w, h, w, h])
(startX, startY, endX, endY) = box.astype("int")
# Draw the detection and the confidence:
text = "{:.3f}%".format(confidence * 100)
y = startY - 10 if startY - 10 > 10 else startY + 10
cv2.rectangle(image, (startX, startY), (endX, endY), (255, 0, 0), 3)
cv2.putText(image, text, (startX, y), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2)
下一个屏幕截图中可以看到face_detection_opencv_dnn.py
脚本的输出:
可以看出,以高置信度检测到这三个脸。
使用 Dlib 的人脸检测
您可以使用dlib.get_frontal_face_detector()
创建正面检测器,该检测器基于定向梯度直方图(HOG)特征和滑动窗口检测方法中的线性分类器。 特别是,HOG 训练器使用基于结构 SVM 的训练算法,该训练算法使训练器可以在每个训练图像中的所有子窗口中进行训练。 此人脸检测器已使用来自带标签的野外数据集中的 3,000 张图像进行了训练。 应当注意的是,该检测器也可以用于发现脸部以外的物体。 您可以查看dlib
库中包含的train_object_detector.py
脚本,以了解如何轻松训练自己的对象检测器仅使用一些训练图像。 例如,您可以仅使用八个停车标志图像来训练一个出色的停车标志检测器。
face_detection_dlib_hog.py
脚本使用上述dlib
正面面部检测器检测面部。 第一步是从dlib
加载正面检测器:
detector = dlib.get_frontal_face_detector()
下一步是执行检测:
rects_1 = detector(gray, 0)
rects_2 = detector(gray, 1)
第二个参数表示在执行检测过程之前对图像进行了1
上采样,因为图像较大,因此检测器可以检测更多的人脸。 相反,执行时间将增加。 因此,出于表现考虑,应将其考虑在内。
在以下屏幕截图中可以看到此脚本的输出:
如您所见,如果我们使用原始灰度图像(rects_1 = detector(gray, 0)
)检测到面部,则只会发现两个面部。 但是,如果我们使用1
时间(rects_2 = detector(gray, 1)
)上采样的灰度图像检测到人脸,则可以正确检测到这三个人脸。
Dlib 库还提供了 CNN 人脸检测器。 您可以使用dlib.cnn_face_detection_model_v1()
创建 CNN 人脸检测器。 构造器从文件中加载人脸检测模型。 您可以从这里下载预训练的模型(712 KB)。 创建 CNN 人脸检测器时,应将相应的预训练模型传递给此方法:
cnn_face_detector = dlib.cnn_face_detection_model_v1("mmod_human_face_detector.dat")
至此,我们准备使用此检测器识别人脸:
rects = cnn_face_detector(img, 0)
该检测器发现了mmod_rectangles
对象,该对象是mmod_rectangle
对象的列表,并且mmod_rectangle
对象具有两个成员变量-dlib.rectangle
对象和confidence
分数。 因此,为了显示检测结果,对show_detection()
函数进行了编码:
def show_detection(image, faces):
"""Draws a rectangle over each detected face"""
# faces contains a list of mmod_rectangle objects
# The mmod_rectangle object has two member variables, a dlib.rectangle object, and a confidence score
# Therefore, we iterate over the detected mmod_rectangle objects accessing dlib.rect to draw the rectangle
for face in faces:
cv2.rectangle(image, (face.rect.left(), face.rect.top()), (face.rect.right(), face.rect.bottom()), (255, 0, 0), 10)
return image
show_detection()
函数应按以下方式调用:
img_faces = show_detection(img.copy(), rects)
完整代码在face_detection_dlib_cnn.py
脚本中。 下一个屏幕截图中可以看到此脚本的输出:
dlib
CNN 面部检测器比dlib
HOG 面部检测器更加精确,但是运行起来需要更多的计算能力。 例如,对于600 x 400
图像,HOG 面部检测器大约需要0.25
秒,而 CNN 面部检测器大约需要5
秒。 实际上,CNN 面部检测器旨在在 GPU 上执行以获得合理的速度。
如果您具有 GPU,则可以启用 CUDA,这将加快执行速度。 为此,您需要从源代码编译dlib
。
使用face_recognition
的人脸检测
为了使用face_recognition
检测人脸,应调用face_locations()
函数:
rects_1 = face_recognition.face_locations(rgb, 0, "hog")
rects_2 = face_recognition.face_locations(rgb, 1, "hog")
第一个参数是输入(RGB)图像。 第二个参数设置在执行检测过程之前对输入图像进行上采样的次数。 第三个参数确定将使用哪种面部检测模型。
在这种情况下,将使用hog
检测模型。 该示例的完整代码可以在face_detection_fr_hog.py
脚本中看到。
另外,可以将face_processing
配置为使用cnn
面部检测器检测面部:
rects_1 = face_recognition.face_locations(rgb, 0, "cnn")
rects_2 = face_recognition.face_locations(rgb, 1, "cnn")
您可以看到face_detection_fr_hog.py
和face_detection_fr_cnn.py
脚本,如果需要更多详细信息,它们分别使用hog
和cnn
面部检测器执行面部识别。
请记住,face_processing
库内部使用 HOG 和 CNN dlib
人脸检测器。
使用cvlib
的人脸检测
为了完整起见,我们在本节中介绍cvlib
包,因为它还提供了面部检测算法。 该库是一个简单,高级且易于使用的 Python 开源计算机视觉库。 为了使用cvlib
检测人脸,可以使用detect_face()
函数,该函数将为所有检测到的人脸返回边界框和相应的置信度:
import cvlib as cv
faces, confidences = cv.detect_face(image)
在后台,此函数将 OpenCV DNN 面部检测器与经过预训练的 Caffe 模型一起使用。
有关更多详细信息,请参见face_detection_cvlib_dnn.py
脚本。
检测人脸标志
在计算机视觉中,基准面部关键点(也称为人脸标志)的定位通常是许多面部分析方法和算法中的关键步骤。 面部表情识别,头部姿势估计算法和嗜睡检测系统仅是几个示例,它们严重依赖于通过检测地标而提供的面部形状信息。
人脸标志检测算法旨在自动识别图像或视频中人脸标志点的位置。 更具体地,那些关键点或者是描述面部成分的唯一位置的优势点(例如,嘴角或眼睛的角),或者是连接这些围绕面部成分和面部轮廓的优势点的内插点。 形式上,给定表示为I
的面部图像,标志检测算法将检测D
标志x = {x1, y1, x2, y2, ..., xD, yD}
,其中x
和y
代表人脸标志的图像坐标。 在本节中,我们将看到如何使用 OpenCV 和 Dlib 来检测人脸标志。
使用 OpenCV 检测人脸标志
OpenCV 人脸标志性 API 称为 Facemark。 它基于三篇不同的论文,提供了三种不同的地标检测实现:
- FacemarkLBF
- FacemarkKamezi
- FacemarkAAM
以下示例显示了如何使用这些算法检测人脸标志:
# Import required packages:
import cv2
import numpy as np
# Load image:
image = cv2.imread("my_image.png",0)
# Find faces:
cas = cv2.CascadeClassifier("haarcascade_frontalface_alt2.xml")
faces = cas.detectMultiScale(image , 1.5, 5)
print("faces", faces)
# At this point, we create landmark detectors and test them:
print("testing LBF")
facemark = cv2.face.createFacemarkLBF()
facemark .loadModel("lbfmodel.yaml")
ok, landmarks = facemark.fit(image , faces)
print ("landmarks LBF", ok, landmarks)
print("testing AAM")
facemark = cv2.face.createFacemarkAAM()
facemark .loadModel("aam.xml")
ok, landmarks = facemark.fit(image , faces)
print ("landmarks AAM", ok, landmarks)
print("testing Kazemi")
facemark = cv2.face.createFacemarkKazemi()
facemark .loadModel("face_landmark_model.dat")
ok, landmarks = facemark.fit(image , faces)
print ("landmarks Kazemi", ok, landmarks)
此示例应使用 OpenCV 提供的三种不同算法来检测人脸标志。 但是,为fit()
函数生成的 Python 包装是不正确的。 因此,在编写本文并使用OpenCV 4.0
时,此脚本在 Python 中不起作用。
要解决此问题,我们需要修改fit()
函数的 C++ 代码并从源代码安装 OpenCV。 例如,以下是FacemarkLBFImpl::fit()
方法的实际代码:
// C++ code
bool FacemarkLBFImpl::fit( InputArray image, InputArray roi, OutputArrayOfArrays _landmarks )
{
// FIXIT
std::vector<Rect> & faces = *(std::vector<Rect> *)roi.getObj();
if (faces.empty()) return false;
std::vector<std::vector<Point2f> > & landmarks =
*(std::vector<std::vector<Point2f> >*) _landmarks.getObj();
landmarks.resize(faces.size());
for(unsigned i=0; i<faces.size();i++){
params.detectROI = faces[i];
fitImpl(image.getMat(), landmarks[i]);
}
return true;
}
应该使用以下代码对其进行修改:
// C++ code
bool FacemarkLBFImpl::fit( InputArray image, InputArray roi, OutputArrayOfArrays _landmarks )
{
Mat roimat = roi.getMat();
std::vector<Rect> faces = roimat.reshape(4,roimat.rows);
if (faces.empty()) return false;
std::vector<std::vector<Point2f> > landmarks(faces.size());
for (unsigned i=0; i<faces.size();i++){
params.detectROI = faces[i];
fitImpl(image.getMat(), landmarks[i]);
}
if (_landmarks.isMatVector()) { // python
std::vector<Mat> &v = *(std::vector<Mat>*) _landmarks.getObj();
for (size_t i=0; i<faces.size(); i++)
v.push_back(Mat(landmarks[i]));
} else { // c++, java
std::vector<std::vector<Point2f> > &v = *(std::vector<std::vector<Point2f> >*) _landmarks.getObj();
v = landmarks;
}
return true;
}
这样,为fit()
函数生成的 Python 包装器应该是正确的。 应该注意的是,使用这三种算法提供的用于检测人脸标志的 Python 代码是正确的,只有 Python 包装器无法生成正确的代码。 有关此问题的更多信息,请参见以下两个链接:
使用 Dlib 检测人脸标志
另一种选择是使用dlib
库来检测人脸标志。 在landmarks_detection_dlib.py
脚本中,我们使用dlib
检测了人脸标志。 更具体地说,我们使用从网络摄像头拍摄的图像使用dlib
正面人脸检测进行人脸检测。 我们还提供从测试图像中获取图像的可能性。 下一步是使用形状预测器获得形状:
p = "shape_predictor_68_face_landmarks.dat"
predictor = dlib.shape_predictor(p)
shape = predictor(gray, rect)
下一步是将shape
转换为numpy
数组。 从这个意义上讲,shape
是 Dlib full_object_detection
对象,它表示图像中对象的位置以及所有部分的位置。 shape_to_np()
函数执行以下转换:
def shape_to_np(dlib_shape, dtype="int"):
"""Converts dlib shape object to numpy array"""
# Initialize the list of (x,y) coordinates
coordinates = np.zeros((dlib_shape.num_parts, 2), dtype=dtype)
# Loop over all facial landmarks and convert them to a tuple with (x,y) coordinates:
for i in range(0, dlib_shape.num_parts):
coordinates[i] = (dlib_shape.part(i).x, dlib_shape.part(i).y)
# Return the list of (x,y) coordinates:
return coordinates
最后,我们在图像中绘制了 68 个人脸标志。 为了在图像中绘制标志,我们已经编码了几个函数,这些函数提供了一种灵活的方式来以所需格式绘制所需的标志。 下一个屏幕截图显示了绘制检测到的人脸标志时的不同可能性:
为了从左到右绘制每个图像中的地标,我们执行了以下操作:
- 第一张图片:
draw_shape_lines_all(shape, frame)
- 第二张图片:
draw_shape_lines_range(shape, frame, JAWLINE_POINTS)
- 第三张图片:
draw_shape_points_pos(shape, frame)
- 第四张图片:
draw_shape_points_pos_range(shape, frame, LEFT_EYE_POINTS + RIGHT_EYE_POINTS + NOSE_BRIDGE_POINTS)
应当注意,dlib
还提供了检测与两只眼睛和鼻尖位置相对应的5
人脸标志的可能性。 因此,如果要使用此形状预测器,则应相应地加载它:
p = "shape_predictor_5_face_landmarks.dat"
使用face_recognition
检测人脸标志
landmarks_detection_fr.py
脚本显示了如何使用face_recognition
包检测和绘制人脸标志。
为了检测标志,调用face_recognition.face_landmarks()
函数,如下所示:
# Detect 68 landmarks:
face_landmarks_list_68 = face_recognition.face_landmarks(rgb)
此函数为图像中的每个脸部返回人脸标志(例如,眼睛和鼻子)的字典。 例如,如果我们打印检测到的地标,则输出如下:
[{'chin': [(113, 251), (111, 283), (115, 315), (122, 346), (136, 376), (154, 402), (177, 425), (203, 442), (231, 447), (260, 442), (285, 426), (306, 403), (323, 377), (334, 347), (340, 315), (343, 282), (343, 251)], 'left_eyebrow': [(123, 223), (140, 211), (163, 208), (185, 211), (206, 220)], 'right_eyebrow': [(240, 221), (263, 212), (288, 209), (312, 211), (332, 223)], 'nose_bridge': [(225, 249), (225, 272), (225, 295), (226, 319)], 'nose_tip': [(201, 337), (213, 340), (226, 343), (239, 339), (252, 336)], 'left_eye': [(144, 248), (158, 239), (175, 240), (188, 254), (173, 255), (156, 254)], 'right_eye': [(262, 254), (276, 240), (293, 239), (308, 248), (295, 254), (278, 255)], 'top_lip': [(185, 377), (200, 370), (216, 364), (226, 367), (238, 364), (255, 370), (274, 377), (267, 378), (238, 378), (227, 380), (215, 379), (192, 378)], 'bottom_lip': [(274, 377), (257, 391), (240, 399), (228, 400), (215, 398), (200, 391), (185, 377), (192, 378), (215, 381), (227, 382), (239, 380), (267, 378)]}]
最后一步是绘制检测到的地标:
# Draw all detected landmarks:
for face_landmarks in face_landmarks_list_68:
for facial_feature in face_landmarks.keys():
for p in face_landmarks[facial_feature]:
cv2.circle(image_68, p, 2, (0, 255, 0), -1)
需要说明的是,face_recognition.face_landmarks()
方法的签名如下:
face_landmarks(face_image, face_locations=None, model="large")
因此,默认情况下会检测到 68 个特征点。 如果model="small"
,将仅检测到 5 个特征点:
# Detect 5 landmarks:
face_landmarks_list_5 = face_recognition.face_landmarks(rgb, None, "small")
如果打印face_landmarks_list_5
,则会得到以下输出:
[{'nose_tip': [(227, 343)], 'left_eye': [(145, 248), (191, 253)], 'right_eye': [(307, 248), (262, 252)]}]
在这种情况下,词典仅包含双眼和鼻尖的面部特征位置。
在以下屏幕截图中可以看到landmarks_detection_fr.py
脚本的输出:
在上面的屏幕截图中,您可以看到使用face_recognition
包绘制检测到的 68 个和 5 个人脸标志的结果。
人脸追踪
在仅知道目标的初始位置的情况下,对象跟踪会尝试估计整个视频序列中目标的轨迹。 由于多种因素,例如外观变化,遮挡,快速运动,运动模糊和比例变化,此任务确实具有挑战性。
在这种意义上,基于判别相关过滤器(DCF)的视觉跟踪器可提供最新的表现。 此外,这些跟踪器的计算效率很高,这在实时应用中至关重要。 实际上,可以在视觉对象跟踪(VOT)2014 挑战赛的结果中看到基于 DCF 的跟踪器的最新表现。 在 VOT2014 挑战赛中,排名前三的跟踪器均基于相关性过滤器。 VOT2014 评估了 38 个跟踪器(来自 VOT2014 委员会的 33 个跟踪器和 5 个基线。 因此,DCF 跟踪器是当前基于边界框的跟踪的一种非常流行的选择方法。
Dlib 库实现了基于 DCF 的跟踪器,该跟踪器易于用于对象跟踪。 在本节中,我们将看到如何将此跟踪器用于面部跟踪和跟踪用户选择的任意对象。 在文献中,此方法也称为判别尺度空间跟踪器(DSST)。 唯一需要的输入(原始视频除外)是第一帧(目标的初始位置)上的边界框,然后,跟踪器会自动预测目标的轨迹。
使用基于 Dlib DCF 的跟踪器的人脸跟踪
在face_tracking_correlation_filters.py
脚本中,我们使用 Dlib 正面人脸检测器进行初始化,并使用基于dlib
DCF 的跟踪器 DSST 进行人脸跟踪。 为了初始化相关跟踪器,我们执行以下命令:
tracker = dlib.correlation_tracker()
这将使用默认值(filter_size = 6
,num_scale_levels = 5
,scale_window_size = 23
,regularizer_space = 0.001
,nu_space = 0.025
,regularizer_scale = 0.001
,nu_scale = 0.025
和scale_pyramid_alpha = 1.020
)初始化跟踪器。 较高的filter_size
和num_scale_levels
值可以提高跟踪精度,但是它需要更多的计算能力,从而增加了 CPU 处理量。 filter_size
的推荐值为5
,6
和7
,推荐的值为num_scale_levels
,4
,5
和6
。
要开始跟踪该方法,请使用tracker.start_track()
。 在这种情况下,我们执行面部检测。 如果成功,我们将脸部位置传递给此方法,如下所示:
if tracking_face is False:
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# Try to detect a face to initialize the tracker:
rects = detector(gray, 0)
# Check if we can start tracking (if we detected a face):
if len(rects) > 0:
# Start tracking:
tracker.start_track(frame, rects[0])
tracking_face = True
这样,对象跟踪器将开始跟踪边界框内的内容,在这种情况下,边界框是检测到的脸部。
另外,为了更新被跟踪对象的位置,调用tracker.update()
方法:
tracker.update(frame)
此方法更新跟踪器并返回峰-旁瓣比,该比值是衡量跟踪器置信度的指标。 此度量标准的较大值表示高置信度。 此度量标准可用于通过正面人脸检测重新初始化跟踪器。
要获取被跟踪对象的位置,请调用tracker.get_position()
方法:
pos = tracker.get_position()
此方法返回被跟踪对象的位置。 最后,我们可以绘制脸部的预测位置:
cv2.rectangle(frame, (int(pos.left()), int(pos.top())), (int(pos.right()), int(pos.bottom())), (0, 255, 0), 3)
在此脚本中,我们编码了如果按下数字1
重新初始化跟踪器的选项。 如果按下此数字,我们将重新初始化跟踪器以尝试检测正面。 为了阐明此脚本的工作方式,包括以下两个屏幕截图。
在第一个屏幕截图中,跟踪算法正在等待,直到执行正面人脸检测以初始化跟踪为止:
在第二个屏幕截图中,跟踪算法当前正在跟踪先前检测到的面部:
在上一个屏幕截图中,您可以看到该算法当前正在跟踪检测到的面部。 您还可以看到您还可以按数字1
来重新初始化跟踪。
使用基于 Dlib DCF 的跟踪器的对象跟踪
可以修改face_tracking_correlation_filters.py
脚本以跟踪任意对象。 在这种情况下,我们将使用鼠标选择要跟踪的对象。 如果按1
,该算法将开始跟踪预定义边界框内的对象。 另外,如果我们按2
,则预定义的边界框将被清空,跟踪算法将停止,从而允许用户选择另一个边界框。
为了阐明face_tracking_correlation_filters.py
脚本的工作方式,我们提供了以下两个屏幕截图。 在第一个中,我们可以看到我们需要选择一个边界框来开始跟踪:
在第二篇文章中,我们可以看到算法跟踪对象时任意帧的输出:
如您在上一个屏幕截图中所见,该算法正在跟踪边界框内的对象。
人脸识别
随着计算机视觉,机器学习和深度学习的发展,人脸识别已成为热门话题。 人脸识别可广泛应用于各种用途,包括预防犯罪,监视,法医应用,生物识别,以及最近在社交网络中的使用。 自动人脸识别面临各种挑战,例如遮挡,外观变化,表情,老化和比例变化。 在对象识别方面取得成功之后,CNN 已被广泛用于面部识别。
在本章中,我们将看到 OpenCV 提供的与面部识别相关的功能,还将探索一些深度学习方法,这些方法可以轻松地集成到您的计算机视觉项目中,以执行最新的面部识别结果。
使用 OpenCV 的人脸识别
OpenCV 提供了执行面部识别的支持。 实际上,OpenCV 提供了三种不同的实现方式:
- Eigenfaces
- Fisherfaces
- 本地二进制模式直方图(LBPH)
这些实现以不同的方式执行识别。 但是,您可以通过仅更改识别器的创建方式来使用它们中的任何一个。 更具体地说,要创建这些识别器,需要以下代码:
face_recognizer = cv2.face.LBPHFaceRecognizer_create()
face_recognizer = cv2.face.EigenFaceRecognizer_create()
face_recognizer = cv2.face.FisherFaceRecognizer_create()
一旦创建,并且与特定的内部算法无关,OpenCV 将用于执行人脸识别,应使用train()
和predict()
这两个关键方法来进行人脸识别系统的训练和测试 ,并且应注意,我们使用这些方法的方式与创建的识别器无关。
因此,尝试使用三个识别器并为特定任务选择表现最佳的识别器非常容易。 话虽如此,当在通常涉及不同环境和光照条件的野外中识别图像时,LBPH 应该比其他两种方法提供更好的结果。 此外,LBPH 人脸识别器支持update()
方法,您可以在其中给定新数据来更新人脸识别器。 对于 Eigenfaces 和 Fisherfaces 方法,此功能是不可能的。
为了训练识别器,应调用train()
方法:
face_recognizer.train(faces, labels)
cv2.face_FaceRecognizer.train(src, labels)
方法训练特定的面部识别器,其中src
对应于图像(面部)的训练集,而参数标签为训练集中的每个图像设置相应的标签。
要识别新人脸,应调用predict()
方法:
label, confidence = face_recognizer.predict(face)
cv2.face_FaceRecognizer.predict(src)
方法通过输出预测的标签和关联的置信度来输出(预测)新src
图像的识别。
最后,OpenCV 还提供write()
和read()
方法来分别保存创建的模型和加载先前创建的模型。 对于这两种方法,filename
参数都设置要保存或加载的模型的名称:
cv2.face_FaceRecognizer.write(filename)
cv2.face_FaceRecognizer.read(filename)
如前所述,可以使用update()
方法更新 LBPH 人脸识别器:
cv2.face_FaceRecognizer.update(src, labels)
在这里,src
和labels
设置了新的训练示例,这些示例将用于更新 LBPH 识别器。
使用 Dlib 的人脸识别
Dlib 提供了基于深度学习的高质量人脸识别算法。 Dlib 实现了面部识别算法,可提供最先进的准确率。 更具体地说,该模型在野生数据库中带有标签的人脸上的准确率为 99.38%。
该算法的实现基于《用于图像识别的深度残差学习》(2016)中提出的 ResNet-34 网络,该网络使用 300 万张人脸进行了训练。 可以从这里下载创建的模型(21.4 MB)。
该网络以生成用于量化面部的 128 维(128D)描述符的方式进行训练。 使用三元组执行训练步骤。 一个三元组训练示例由三个图像组成。 其中两个对应于同一个人。 网络为每个图像生成 128D 描述符,略微修改神经网络权重,以使与同一个人相对应的两个向量更近,而与另一个人相对应的特征向量更远。 三元组损失函数将其形式化,并尝试将同一个人的两个图像的 128D 描述符推近,而将不同人的两个图像的 128D 描述符推向更远。
对于成千上万的人的数百万个图像,此过程将重复数百万次,最后,它可以为每个人生成 128D 描述符。 因此,由于以下原因,最终的 128D 描述符是良好的编码:
- 相同人的两个图像的生成的 128D 描述符彼此非常相似。
- 不同人的两个图像生成的 128D 描述符非常不同。
因此,利用dlib
函数,我们可以使用预先训练的模型将人脸映射到 128D 描述符中。 之后,我们可以使用这些特征向量来进行面部识别。
encode_face_dlib.py
脚本显示了如何计算用于量化人脸的 128D 描述符。 该过程非常简单,如以下代码所示:
# Load image:
image = cv2.imread("jared_1.jpg")
# Convert image from BGR (OpenCV format) to RGB (dlib format):
rgb = image[:, :, ::-1]
# Calculate the encodings for every face of the image:
encodings = face_encodings(rgb)
# Show the first encoding:
print(encodings[0])
您可以猜到,face_encodings()
函数为图像中的每个面部返回 128D 描述符:
pose_predictor_5_point = dlib.shape_predictor("shape_predictor_5_face_landmarks.dat")
face_encoder = dlib.face_recognition_model_v1("dlib_face_recognition_resnet_model_v1.dat")
detector = dlib.get_frontal_face_detector()
def face_encodings(face_image, number_of_times_to_upsample=1, num_jitters=1):
"""Returns the 128D descriptor for each face in the image"""
# Detect faces:
face_locations = detector(face_image, number_of_times_to_upsample)
# Detected landmarks:
raw_landmarks = [pose_predictor_5_point(face_image, face_location) for face_location in face_locations]
# Calculate the face encoding for every detected face using the detected landmarks for each one:
return [np.array(face_encoder.compute_face_descriptor(face_image, raw_landmark_set, num_jitters)) for
raw_landmark_set in raw_landmarks]
如您所见,关键是使用检测到的每个标志来计算每个检测到的脸部的脸部编码,并调用dlib
和face_encoder.compute_face_descriptor()
函数。
num_jitters
参数设置每个面部随机抖动的次数,并返回每次计算的平均 128D 描述符。 在这种情况下,输出(编码 128D 描述符)如下:
[-0.08550473 0.14213498 0.01144615 -0.05947386 -0.05831585 0.01127038 -0.05497809 -0.03466939 0.14322688 -0.1001832 0.17384697 0.02444006 -0.25994921 0.13708787 -0.08945534 0.11796272 -0.25426617 -0.0829383 -0.05489913 -0.10409787 0.07074109 0.05810066 -0.03349853 0.07649824 -0.07817822 -0.29932317 -0.15986916 -0.087205 0.10356752 -0.12659372 0.01795856 -0.01736169 -0.17094864 -0.01318233 -0.00201829 0.0104903 -0.02453734 -0.11754096 0.2014133 0.12671679 -0.0271306 -0.02350519 0.08327188 0.36815098 0.12599576 0.04692561 0.03585262 -0.03999642 0.23675609 -0.28394884 0.11896492 0.11870296 0.20243752 0.2106981 0.03092775 -0.14315812 0.07708532 0.16536239 -0.19648902 0.22793224 0.06825032 -0.00117573 0.00304667 -0.01902146 0.2539638 0.09768397 -0.13558105 -0.15079053 0.11357955 -0.14893037 -0.09028706 0.03625216 -0.13004847 -0.16567475 -0.21958281 0.08687183 0.35941613 0.16637127 -0.08334676 0.02806632 -0.09188357 -0.10760318 0.02889947 0.08376379 -0.11524356 -0.00998984 -0.05582509 0.09372396 0.30287758 -0.01063644 -0.07903813 0.30418509 -0.01998731 0.0752025 -0.00424637 0.07463965 -0.12972119 -0.04034984 -0.08435905 -0.01642537 0.00847361 -0.09549874 -0.07568903 0.06476583 -0.19202243 0.16904426 -0.01247451 0.03941975 -0.01960869 0.02145611 -0.25607404 -0.03039071 0.20248309 -0.25835767 0.21397503 0.19302645 0.07284702 0.07879912 0.06171442 0.02366752 0.06781606 -0.06446165 -0.14713687 -0.0714087 0.11978403 -0.01525984 -0.04687868 0.00167655]
面部被编码后,下一步就是执行识别。
使用使用 128D 描述符计算的某种距离度量可以轻松地计算出识别率。 实际上,如果两个面部描述符向量之间的欧式距离小于0.6
,则可以认为它们属于同一个人。 否则,他们来自不同的人。
欧几里德距离可以使用numpy.linalg.norm()
来计算。
在compare_faces_dlib.py
脚本中,我们将四个图像与另一个图像进行比较。 为了比较人脸,我们编写了两个函数:compare_faces()
和compare_faces_ordered()
。 compare_faces()
函数将面部编码列表与候选者进行比较时返回距离以进行检查:
def compare_faces(face_encodings, encoding_to_check):
"""Returns the distances when comparing a list of face encodings against a candidate to check"""
return list(np.linalg.norm(face_encodings - encoding_to_check, axis=1))
compare_faces_ordered()
函数在将人脸编码列表与候选者进行比较以进行检查时,返回排序的距离和相应的名称:
def compare_faces_ordered(face_encodings, face_names, encoding_to_check):
"""Returns the ordered distances and names when comparing a list of face encodings against a candidate to check"""
distances = list(np.linalg.norm(face_encodings - encoding_to_check, axis=1))
return zip(*sorted(zip(distances, face_names)))
因此,将四个图像与另一个图像进行比较的第一步是加载所有图像并转换为 RGB(dlib format
):
# Load images:
known_image_1 = cv2.imread("jared_1.jpg")
known_image_2 = cv2.imread("jared_2.jpg")
known_image_3 = cv2.imread("jared_3.jpg")
known_image_4 = cv2.imread("obama.jpg")
unknown_image = cv2.imread("jared_4.jpg")
# Convert image from BGR (OpenCV format) to RGB (dlib format):
known_image_1 = known_image_1[:, :, ::-1]
known_image_2 = known_image_2[:, :, ::-1]
known_image_3 = known_image_3[:, :, ::-1]
known_image_4 = known_image_4[:, :, ::-1]
unknown_image = unknown_image[:, :, ::-1]
# Crate names for each loaded image:
names = ["jared_1.jpg", "jared_2.jpg", "jared_3.jpg", "obama.jpg"]
下一步是计算每个图像的编码:
# Create the encodings:
known_image_1_encoding = face_encodings(known_image_1)[0]
known_image_2_encoding = face_encodings(known_image_2)[0]
known_image_3_encoding = face_encodings(known_image_3)[0]
known_image_4_encoding = face_encodings(known_image_4)[0]
known_encodings = [known_image_1_encoding, known_image_2_encoding, known_image_3_encoding, known_image_4_encoding]
unknown_encoding = face_encodings(unknown_image)[0]
最后,您可以使用以前的函数比较人脸。 例如,让我们使用compare_faces_ordered()
函数:
computed_distances_ordered, ordered_names = compare_faces_ordered(known_encodings, names, unknown_encoding)
print(computed_distances_ordered)
print(ordered_names)
这样做将为我们带来以下好处:
(0.3913191431497527, 0.39983264838593896, 0.4104153683230741, 0.9053700273411349)
('jared_3.jpg', 'jared_1.jpg', 'jared_2.jpg', 'obama.jpg')
前三个值(0.3913191431497527
,0.39983264838593896
,0.4104153683230741
)小于0.6
。 这意味着可以从与要检查的图像('jared_4.jpg'
)相同的人处考虑前三个图像('jared_3.jpg'
,'jared_1.jpg'
,'jared_2.jpg'
)。 获得的第四值(0.9053700273411349
)表示第四张图像('obama.jpg'
)与要检查的图像不是同一个人。
在下一个屏幕截图中可以看到:
在上一个屏幕截图中,您可以看到可以从同一个人考虑前三张图像(获取的值小于 0.6),而可以从另一个人考虑第四张图像(获取的值大于 0.6)。
使用face_recognition
的人脸识别
face_recognition
的人脸识别使用dlib
函数对人脸进行编码并计算编码人脸的距离。 因此,您无需编码face_encodings()
和compare_faces()
函数,而只需使用它们。
encode_face_fr.py
脚本显示了如何创建使用face_recognition.face_encodings()
函数的 128D 描述符:
# Load image:
image = cv2.imread("jared_1.jpg")
# Convert image from BGR (OpenCV format) to RGB (face_recognition format):
image = image[:, :, ::-1]
# Calculate the encodings for every face of the image:
encodings = face_recognition.face_encodings(image)
# Show the first encoding:
print(encodings[0])
要查看如何使用face_recognition
比较人脸,已对compare_faces_fr.py
脚本进行了编码。 代码如下:
# Load known images (remember that these images are loaded in RGB order):
known_image_1 = face_recognition.load_image_file("jared_1.jpg")
known_image_2 = face_recognition.load_image_file("jared_2.jpg")
known_image_3 = face_recognition.load_image_file("jared_3.jpg")
known_image_4 = face_recognition.load_image_file("obama.jpg")
# Crate names for each loaded image:
names = ["jared_1.jpg", "jared_2.jpg", "jared_3.jpg", "obama.jpg"]
# Load unknown image (this image is going to be compared against all the previous loaded images):
unknown_image = face_recognition.load_image_file("jared_4.jpg")
# Calculate the encodings for every of the images:
known_image_1_encoding = face_recognition.face_encodings(known_image_1)[0]
known_image_2_encoding = face_recognition.face_encodings(known_image_2)[0]
known_image_3_encoding = face_recognition.face_encodings(known_image_3)[0]
known_image_4_encoding = face_recognition.face_encodings(known_image_4)[0]
known_encodings = [known_image_1_encoding, known_image_2_encoding, known_image_3_encoding, known_image_4_encoding]
unknown_encoding = face_recognition.face_encodings(unknown_image)[0]
# Compare the faces:
results = face_recognition.compare_faces(known_encodings, unknown_encoding)
# Print the results:
print(results)
获得的结果为[True, True, True, False]
。 因此,前三个加载的图像("jared_1.jpg"
,"jared_2.jpg"
和"jared_3.jpg"
)被视为与未知图像("jared_4.jpg"
)是同一个人,而第四个加载的图像("obama.jpg"
)被视为一个不同的人。
总结
在本章中,我们介绍了用于面部检测,检测人脸标志,面部跟踪和面部识别的最新算法和技术。 我们回顾了主要的 Python 库和包提供的面部处理方法。 更具体地说,在面部处理的上下文中引入了 OpenCV,dlib
,face_processing
和cvlib
。 其中一些经过审查的方法是基于深度学习技术的。
在下一章中,我们将深入探讨深度学习。
问题
- 在本章中,有关面部处理的包和库有哪些评论?
- 人脸识别和人脸验证之间的主要区别是什么?
cv2.face.getFacesHAAR()
OpenCV 函数做什么?cv2.dnn.blobFromImage()
函数有什么作用?cvlib
包提供什么函数来检测人脸?face_recognition
提供什么函数来检测人脸标志?dlib
提供什么函数来初始化相关跟踪器?dlib
提供什么函数来启动相关跟踪器?dlib
提供什么函数来获取被跟踪对象的位置?- 计算 128D 描述符,以
dlib
对image
BGR 图像进行人脸识别。
进一步阅读
以下资源将帮助您更深入地使用 Python 进行面部处理:
十二、深度学习简介
如今,深度学习是机器学习中最受欢迎和增长最快的领域。 自 2012 年以来,深度学习已经超越了传统的机器学习应用方法。这就是为什么将许多深度学习架构应用于许多领域(包括计算机视觉)的原因。 深度学习的常见应用包括自动语音识别,图像识别,视觉艺术处理,自然语言处理,推荐系统,生物信息学和图像恢复。 大多数现代深度学习架构都基于人工神经网络,深度学习中的深度指的是架构的层数。
在本章中,将通过研究与传统机器学习方法的差异来向您介绍深度学习,这些差异在第 10 章,“使用 OpenCV 的机器学习”中进行了介绍。 此外,您还将看到一些适用于图像分类和对象检测的常见深度学习架构。 最后,将介绍两个深度学习 Python 库(TensorFlow 和 Keras)。
更具体地说,本章将讨论以下主题:
- 计算机视觉任务的深度学习概述
- OpenCV 中的深度学习
- TensorFlow 库
- Keras 库
在本章中,将向您介绍 OpenCV 的深度学习领域,以及一些深度学习的 Python 库(TensorFlow 和 Keras)。 在第 13 章,“使用 Python 和 OpenCV 的移动和 Web 计算机视觉”中,您将学习如何创建计算机视觉和深度学习 Web 应用。
技术要求
技术要求在这里列出:
- Python 和 OpenCV
- 特定于 Python 的 IDE
- NumPy 和 Matplotlib 包
- Git 客户端
- TensorFlow 库(请参阅以下有关如何安装 TensorFlow 的部分)
- Keras 库(请参阅以下有关如何安装 Keras 的部分)
有关如何安装这些要求的更多详细信息,请参见第 1 章,“设置 OpenCV”。 可以在 Github 中访问《精通 Python OpenCV 4》的 GitHub 存储库,其中包含从本书第一章到最后的所有必要的支持项目文件。
安装 TensorFlow
为了安装 TensorFlow,请使用以下命令:
$ pip install tensorflow
要检查安装是否已正确执行,只需打开 Python shell 并尝试导入 TensorFlow 库,如下所示:
python
import tensorflow
安装 Keras
为了安装 Keras,请使用以下命令:
$ pip install keras
要检查安装是否正确执行,只需打开一个 Python shell 并尝试导入 Keras 库,如下所示:
python
import keras
请记住,建议的方法是在虚拟环境中安装包。 请参阅第 1 章,“设置 OpenCV”,以了解如何创建和管理虚拟环境。
计算机视觉任务的深度学习概述
深度学习极大地促进了计算机视觉领域的发展。 在本章的这一部分中,将介绍一些关键概念,以向您介绍深度学习领域。
深度学习特征
与传统的机器学习方法相比,深度学习具有一些关键差异。 此外,在许多计算机视觉任务中,深度学习技术都超越了机器学习,但是应考虑一些关键因素,以便知道何时应用每种技术来完成特定的计算任务。 所有这些注意事项简要总结如下:
- 与可以在低端机器上运行的机器学习技术相反,深度学习算法需要具有高端基础架构才能正确训练。 实际上,深度学习算法固有地执行了大量计算,而这些计算可以使用 GPU 进行优化。
- 当对特征自省和工程都缺乏领域的了解时,深度学习技术会胜过其他技术,因为您不必担心特征工程。 特征工程可以定义为将领域知识应用于特征检测器和提取器创建的过程,目的是降低数据的复杂性,从而使传统的机器学习方法能够正确学习。 因此,这些机器学习算法的表现取决于识别和提取特征的准确率。 另一方面,深度学习技术试图从数据中提取高级特征,这使得深度学习比传统的机器学习方法先进得多。 在深度学习中,查找相关特征的任务是算法的一部分,并且通过减少每个问题的特征自省和工程任务来实现自动化。
- 机器学习和深度学习都能够处理海量数据集。 但是,在处理小型数据集时,机器学习方法更有意义。 从这个意义上说,这两种方法之间的主要区别在于其表现随着数据规模的增加而增加。 例如,当使用小型数据集时,深度学习算法很难在数据中找到模式,并且表现不佳,因为它们需要大量数据来调整其内部参数。 经验法则是,如果数据量很大,则深度学习要胜过其他技术,而当数据集较小时,传统的机器学习算法是可取的。
在下一张图中,我们尝试总结了上述关键点,以便轻松记住它们:
图中阐述的关键点如下:
- 计算资源(深度学习–高端机器与机器学习–低端机器)
- 特征工程(深度学习–同一步骤中的特征提取和分类与机器学习–单独步骤中的特征提取和分类)
- 数据集大小(深度学习–大/非常大的数据集与机器学习–中/大数据集)
深度学习爆发
深度学习的概念并不是什么新鲜事物,因为 Rina Dechter 于 1986 年以及 Igor Aizenberg 及其同事于 2000 年分别将其引入了机器学习领域和人工神经网络。 但是,直到 2012 年发生深度学习革命时,才出现了一些对研究界具有重大影响的杰出作品。 在计算机视觉方面, AlexNet 架构(作者设计的卷积神经网络的名称)是 ImageNet 大规模视觉识别挑战赛(ILSVRC)的获胜者)2012 年的错误率非常低,以巨大的错误率(15.3% 对 26.2%(第二名))击败了所有其他竞争对手。 ImageNet 是一个大型视觉数据库,包含超过 1400 万个带标签的高分辨率图像。 这些图像被人类标记。 ImageNet 包含 20,000 多个类别。 因此,AlexNet 在解决 ILSVRC 2012 方面的 2012 年突破通常被认为是 2010 年代深度学习革命的开始。
用于图像分类的深度学习
继 AlexNet 在此竞赛中获得成功之后,许多其他深度学习架构也已提交 ImageNet 挑战赛,以实现更好的表现。 从这个意义上讲,下一张图展示了提交给 ImageNet 挑战的最相关的深度学习方法的一站式准确率,包括最左侧的 AlexNet(2012)架构,以及迄今为止表现最佳的 Inception-V4(2016) 对:
这些深度学习架构的主要方面简要介绍如下,重点介绍了它们引入的关键方面。
此外,如果需要更多详细信息,我们还提供了对每个出版物的引用:
-
AlexNet(2012):
- 描述:AlexNet 是 LSVRC-2012 的获胜者,它是一种简单但功能强大的网络架构,其中卷积层和池层一个接一个,而顶层则是全连接层。 在将深度学习方法应用于计算机视觉任务时,通常将该架构用作起点。
- 参考:
Alex Krizhevsky, Ilya Sutskever, and Geoffrey E Hinton. ImageNet classification with deep convolutional neural networks. In Advances in neural information processing systems, pp. 1097–1105, 2012.
-
VGG-16 和 -19(2014):
- 描述:VGGNet 由牛津大学的视觉几何组(VGG)提出。 通过在整个网络中仅使用
3 x 3
过滤器,而不使用大型过滤器(例如7 x 7
和11 x 11
)。 这项工作的主要贡献在于,它表明网络深度是在卷积神经网络中实现更好的识别或分类精度的关键组成部分。 VGGNet 被认为是对特定任务进行基准测试的良好架构。 但是,它的主要缺点是训练速度非常慢,并且网络架构的权重很大(VGG-16 为 533 MB,VGG-19 为 574 MB)。 VGGNet-19 使用 1.38 亿个参数。 - 参考:
Simonyan, K., and Zisserman, A. (2014). Very deep convolutional networks for large-scale image recognition. arXiv preprint arXiv:1409.1556.
- 描述:VGGNet 由牛津大学的视觉几何组(VGG)提出。 通过在整个网络中仅使用
-
GoogLeNet/Inception V1(2014):
- 说明: GoogLeNet(也称为 Inception V1)是 LSVRC-2014 的获胜者,其前 5 名错误率达到 6.67%,这非常接近人类水平的表现。 该架构比 VGGNet 更深入。 但是,由于 9 个并行模块(初始模块)的架构是基于几个非常小的卷积的,因此它仅使用 AlexNet 参数数量的十分之一(从 6000 万到仅 400 万个参数),目的是减少参数数量。
- 参考:
Szegedy, C., Liu, W., Jia, Y., Sermanet, P., Reed, S., Anguelov, D., Dumitru, .E, Vincent, .V, and Rabinovich, A. (2015). Going deeper with convolutions.
-
ResNet-18,-34,-50,-101 和 -152(2015):
- 说明:Microsoft 的残差网络(ResNets)是 LSVRC-2015 的获胜者,并且是迄今为止最深的网络,共有 153 个卷积层达到了最高 5 个分类误差为 4.9%(这比人工精度略好)。 此架构包括跳跃连接,也称为门控单元或门控循环单元,可实现增量学习更改。 ResNet-34 使用 2180 万个参数,ResNet-50 使用 2560 万个参数,ResNet-101 使用 4450 万个参数,最后,ResNet-152 使用 6020 万个参数。
- 参考:
He, K., Zhang, X., Ren, S., and Sun, J. (2016). Deep residual learning for image recognition. In Proceedings of the IEEE conference on computer vision and pattern recognition (pp. 770-778).
-
Inception V3(2015):
- 描述:如前所示,初始架构被引入为 GoogLeNet(也称为 Inception V1)。 后来,对该架构进行了修改,以引入批量规范化(Inception-V2)。 Inception V3 架构包括其他分解思想,其目的是在不降低网络效率的情况下减少连接/参数的数量。
- 参考:
Szegedy, C., Vanhoucke, V., Ioffe, S., Shlens, J., and Wojna, Z. (2016). Rethinking the inception architecture for computer vision. In Proceedings of the IEEE conference on computer vision and pattern recognition (pp. 2818-2826).
-
Inception V4(2016):
- 说明:从 GoogLeNet 演变而来的 Inception V4。 另外,与 Inception-V3 相比,此架构具有更统一的简化架构和更多的 Inception 模块。 Inception-V4 在 LSVRC 上能够达到 80.2% 的 top-1 精度和 95.2% 的 top-5 精度。
- 参考:
Szegedy, C., Ioffe, S., Vanhoucke, V., and Alemi, A. A. (2017, February). Inception-V4, inception-resnet and the impact of residual connections on learning. In AAAI (Vol. 4, p. 12).
用于对象检测的深度学习
对象检测是深度学习中的一个热门话题,它适合于在单个图像中识别和定位多个相关对象。 为了对对象检测算法进行基准测试,通常使用三个数据库。 第一个是 PASCAL 视觉对象分类(PASCAL VOC)数据集,其中包括 20 个类别和 10,000 个用于训练和验证的图像,其中包含带有对象的边界框。 ImageNet 自 2013 年以来发布了一个对象检测数据集,它由大约 500,000 张仅用于训练的图像和 200 个类别组成。 最后,上下文中的常见对象(COCO)是大规模的对象检测,分割和字幕数据集,在 328,000 张图像中总共有 250 万个标记实例。 有关 COCO 数据集的更多信息,您可以阅读出版物《Microsoft COCO:上下文中的常见对象》(2014)。 为了评估对象检测算法,通常使用平均平均精度(mAP),其计算方法是对所有类别和/或所有交并比(IoU)阈值计算 mAP,具体取决于比赛。 在二分类中,平均精度(AP)指标对应于精度(正预测值)-召回(灵敏度)曲线的摘要,而 IoU 指标是预测框与地面真实框之间的重叠区域。 以下是此示例:
- PASCAL VOC2007 挑战–仅考虑了一个 IoU 阈值。 对于 PASCAL VOC 挑战,如果 IoU> 0.5,则预测为肯定。 因此,mAP 是对所有 20 个对象类平均的。
- 在 2017 年 COCO 挑战赛中,对所有 80 个物体类别和所有 10 个 IoU 阈值(从 0.5 到 0.95,步长为 0.05)平均了 mAP。
在 10 个 IoU 阈值(从 0.5 到 0.95,步长为 0.05)上求平均值,而不是仅考虑一个 IoU≥0.5 的阈值,倾向于奖励在精确定位方面更好的模型。
在下表中,您可以看到使用上述三个数据集评估的用于对象检测的最新深度学习算法,其中显示了 PASCAL VOC 和 COCO 数据集上的 mAP 得分:
下面包括有关用于对象检测的最新深度学习算法的介绍:
- R-CNN(2014):
- 描述:基于区域的卷积网络(R-CNN)是使用卷积神经网络进行对象检测的首批方法之一,表明与基于类似 HOG 的简单特征的系统相比,卷积神经网络可以提高目标检测表现。 该算法可以分解为以下三个步骤:
- 创建一组区域提议
- 对每个区域提议执行经过修订版的 AlexNet 的前向传递,以提取特征向量
- 潜在的对象通过几个 SVM 分类器进行检测,此外,线性回归器会更改边界框的坐标
- 参考:
Girshick, R., Donahue, J., Darrell, T., and Malik, J. (2014). Rich feature hierarchies for accurate object detection and semantic segmentation. In Proceedings of the IEEE conference on computer vision and pattern recognition (pp. 580-587).
- 描述:基于区域的卷积网络(R-CNN)是使用卷积神经网络进行对象检测的首批方法之一,表明与基于类似 HOG 的简单特征的系统相比,卷积神经网络可以提高目标检测表现。 该算法可以分解为以下三个步骤:
- Fast R-CNN(2015):
- 描述:基于快速区域的卷积网络(Fast R-CNN)方法是对先前方法的一种改进,可以有效地对目标提议进行分类。 此外,Fast R-CNN 采用了多项创新技术来提高训练和测试速度,同时还提高了检测精度。
- 参考:
Girshick, R. (2015). Fast r-cnn. In Proceedings of the IEEE international conference on computer vision and pattern recognition (pp. 1440-1448).
- Faster R-CNN(2015):
- 说明:更快的 R-CNN 是对快速 R-CNN 的修改,引入了一个区域提议网络(RPN),该网络与检测网络共享全图像卷积特征,从而实现几乎免费的区域提议。
- 参考:
Ren, S., He, K., Girshick, R., and Sun, J. (2015). Faster R-CNN – Towards real-time object detection with region proposal networks. In Advances in neural information processing systems (pp. 91-99).
- R-FCN(2016):
- 描述:基于区域的全卷积网络(R-FCN)是仅包含卷积层的框架,允许进行完整的反向传播以进行训练和推理,从而获得准确而有效的对象检测。
- 参考:
Dai, J., Li, Y., He, K., and Sun, J. (2016). R-FCN: Object Detection via Region-based Fully Convolutional Networks. In Advances in neural information processing systems (pp. 379-387).
- YOLO(2016):
- 描述:只看一次(YOLO)是一种深度学习架构,可在单个步骤中预测边界框和类概率。 与其他深度学习检测器相比,YOLO 会产生更多的定位错误,但是在背景中预测假正例的可能性较小。
- 参考:
Redmon, J., Divvala, S., Girshick, R., and Farhadi, A. (2016). You only look once: Unified, Real-Time Object Detection.
- SSD(2016):
- 描述:单发多盒检测器(SSD)是一个深层神经网络,旨在通过端到端卷积神经网络架构的方法,同时预测边界框和类概率。
- 参考:
Liu, W., Anguelov, D., Erhan, D., Szegedy, C., Reed, S., Fu, C. Y., and Berg, A. C. (2016, October). SSD: Single Shot Multibox Detector. In European conference on Computer Vision (pp. 21-37). Springer, Cham.
- YOLO V2(2016):
- 描述:作者在同一出版物中介绍了 YOLO9000 和 YOLO V2。 YOLO9000 是一种实时对象检测系统,可以检测 9,000 多个对象类别,而 YOLO V2 是 YOLO 的改进版本,致力于在提高准确率的同时仍是快速检测器。
- 参考:
Redmon, J., and Farhadi, A. (2017). YOLO9000: Better, Faster, Stronger. arXiv preprint.
- NASNet(2016):
- 描述:作者介绍了一种神经网络搜索,这是使用循环神经网络构成神经网络架构的想法。 神经架构搜索网络(NASNet)包括学习模型的架构,以优化层数,同时还提高准确率。
- 参考:
Zoph, B., and Le, Q. V. (2016). Neural Architecture Search with Reinforcement Learning. arXiv preprint arXiv:1611.01578.
- Mask R-CNN(2017):
- 描述:基于遮罩区域的卷积网络(遮罩 R-CNN)是 Faster R-CNN 模型的另一个扩展,它为边界框检测添加了并行分支,目的是预测对象遮罩。 对象遮罩是按图像中的像素进行分割,从而可以对对象实例进行分割。
- 参考:
He, K., Gkioxari, G., Dollár, P., and Girshick, R. (2017, October). Mask r-cnn. In Computer Vision (ICCV), 2017 IEEE International Conference on Computer Vision (pp. 2980-2988). IEEE.
OpenCV 中的深度学习
自 OpenCV 3.1 以来,库中已有深层神经网络(DNN)模块,可通过一些流行的深度学习框架进行预训练的深度网络实现前向传递(推理) ,例如 Caffe,TensorFlow,Torch/Pytorch,Darknet 和 ONNX 格式的模型。 在 OpenCV 3.3 中,该模块已从opencv_contrib
存储库升级到主存储库,并已进行了显着加速。 因此,从 OpenCV 3.3 开始,可以在我们的应用中使用经过预训练的网络进行预测,并且在上一节中介绍的许多流行的网络架构都与 OpenCV 3.3 兼容。
在本节中,我们将看到如何将这些架构中的一些应用于对象检测和图像分类,但是在涵盖这一方面之前,应先回顾一下 OpenCV 在 DNN 模块中提供的许多功能。
了解cv2.dnn.blobFromImage()
在第 11 章,“人脸检测,跟踪和识别”中,我们看到了一些涉及深度学习计算的示例。 例如,在face_detection_opencv_dnn.py
脚本中,使用了基于深度学习的面部检测器来检测图像中的人脸。 第一步是按以下方式加载预训练的模型:
net = cv2.dnn.readNetFromCaffe("deploy.prototxt", "res10_300x300_ssd_iter_140000_fp16.caffemodel")
提醒一下,deploy.prototxt
文件定义了模型架构,res10_300x300_ssd_iter_140000_fp16.caffemodel
文件包含了实际层的权重。 为了对整个网络执行前向传递以计算输出,网络的输入应为 BLOB。 BLOB 可以看作是经过充分预处理以馈送到网络的图像集合。
此预处理由几个操作组成-调整大小,裁剪,减去平均值,缩放以及交换蓝色和红色通道。
例如,在上述面部检测示例中,我们执行了以下命令:
# Load image:
image = cv2.imread("test_face_detection.jpg")
# Create 4-dimensional blob from image:
blob = cv2.dnn.blobFromImage(image, 1.0, (300, 300), [104., 117., 123.], False, False)
在这种情况下,这意味着我们要在调整为300 x 300
,的 BGR 图像上运行模型,分别对蓝色,绿色和红色通道应用(104, 117, 123)
值的平均减法。 下表中对此进行了总结:
| 模型 | 规模 | 尺寸WxH
| 均值减法 | 通道顺序 |
| --- | --- | --- | --- |
| OpenCV 人脸检测器 | 1.0 |300 x 300
| 104
,177
,123
| BGR |
此时,我们可以将 BLOB 设置为输入,并按以下方式获得检测结果:
# Set the blob as input and obtain the detections:
net.setInput(blob)
detections = net.forward()
有关更多详细信息,请参见face_detection_opencv_dnn.py
脚本。
现在,我们将详细了解cv2.dnn.blobFromImage()
和cv2.dnn.blobFromImages()
函数。 为此,我们首先要看到两个函数的签名,然后我们将看到blob_from_image.py
和blob_from_images.py
脚本。 这些脚本在理解这些函数时可能会有所帮助。 此外,在这些脚本中,我们还将使用 OpenCV cv2.dnn.imagesFromBlob()
函数。
cv2.dnn.blobFromImage()
的签名如下:
retval=cv2.dnn.blobFromImage(image[, scalefactor[, size[, mean[, swapRB[, crop[, ddepth]]]]]])
此函数从image
创建一个二维 BLOB。 此外,它还可以选择将图像调整为size
大小,并从中心裁剪输入图像,减去mean
值,按scalefactor
缩放值,并交换蓝色和红色通道:
image
:这是要预处理的输入图像。scalefactor
:这是image
值的乘数。 此值可用于缩放我们的图像。 默认值为1.0
,这表示不执行缩放。size
:这是输出图像的空间大小。mean
:这是从图像中减去平均值的标量。 如果执行均值减法,则在使用swapRB =True
时,这些值应为(mean-R
,mean-G
,mean-B
)。swapRB
:通过将该标志设置为True
,可以使用该标志交换图像中的R
和B
通道。crop
:这是一个标志,指示在调整大小后是否将裁切图像。ddepth
:输出 BLOB 的深度。 您可以在CV_32F
或CV_8U
之间选择。- 如果为
crop=False
,则在不裁剪的情况下执行图像的调整大小。 否则,如果(crop=True
),则首先应用调整大小,然后从中心裁剪图像。 - 默认值为
scalefactor=1.0
,size = Size()
,mean = Scalar()
,swapRB = false
,crop = false
和ddepth = CV_32F
。
cv.dnn.blobFromImages()
的签名如下:
retval=cv.dnn.blobFromImages(images[, scalefactor[, size[, mean[, swapRB[, crop[, ddepth]]]]]])
此函数从多个图像创建一个四维 BLOB。 这样,您可以对整个网络执行前向传递,以一次计算多个图像的输出。 以下代码显示了如何正确使用此函数:
# Create a list of images:
images = [image, image2]
# Call cv2.dnn.blobFromImages():
blob_images = cv2.dnn.blobFromImages(images, 1.0, (300, 300), [104., 117., 123.], False, False)
# Set the blob as input and obtain the detections:
net.setInput(blob_images)
detections = net.forward()
至此,我们介绍了cv2.dnn.blobFromImage()
和cv2.dnn.blobFromImages()
函数。 因此,我们准备看blob_from_image.py
和blob_from_images.py
脚本。
在blob_from_image.py
脚本中,我们首先加载 BGR 图像,然后使用cv2.dnn.blobFromImage()
函数创建一个二维 BLOB。 您可以检查创建的 BLOB 的形状是否为(1, 3, 300, 300)
。 然后,我们调用get_image_from_blob()
函数,该函数可用于执行逆预处理转换,以便再次获取输入图像。 这样,您将更好地了解此预处理。 get_image_from_blob
函数的代码如下:
def get_image_from_blob(blob_img, scalefactor, dim, mean, swap_rb, mean_added):
"""Returns image from blob assuming that the blob is from only one image""
images_from_blob = cv2.dnn.imagesFromBlob(blob_img)
image_from_blob = np.reshape(images_from_blob[0], dim) / scalefactor
image_from_blob_mean = np.uint8(image_from_blob)
image_from_blob = image_from_blob_mean + np.uint8(mean)
if mean_added is True:
if swap_rb:
image_from_blob = image_from_blob[:, :, ::-1]
return image_from_blob
else:
if swap_rb:
image_from_blob_mean = image_from_blob_mean[:, :, ::-1]
return image_from_blob_mean
在脚本中,我们利用此函数从 BLOB 获取不同的图像,如以下代码片段所示:
# Load image:
image = cv2.imread("face_test.jpg")
# Call cv2.dnn.blobFromImage():
blob_image = cv2.dnn.blobFromImage(image, 1.0, (300, 300), [104., 117., 123.], False, False)
# The shape of the blob_image will be (1, 3, 300, 300):
print(blob_image.shape)
# Get different images from the blob:
img_from_blob = get_image_from_blob(blob_image, 1.0, (300, 300, 3), [104., 117., 123.], False, True)
img_from_blob_swap = get_image_from_blob(blob_image, 1.0, (300, 300, 3), [104., 117., 123.], True, True)
img_from_blob_mean = get_image_from_blob(blob_image, 1.0, (300, 300, 3), [104., 117., 123.], False, False)
img_from_blob_mean_swap = get_image_from_blob(blob_image, 1.0, (300, 300, 3), [104., 117., 123.], True, False)
创建的图像说明如下:
img_from_blob
图像对应于调整为(300,300)
的原始 BGR 图像。img_from_blob_swap
图像对应于调整为(300,300)
尺寸的原始 BGR 图像,并且蓝色和红色通道已交换。img_from_blob_mean
图像对应于调整为(300,300)
尺寸的原始 BGR 图像,其中未将具有平均值的标量添加到图像中。img_from_blob_mean_swap
图像对应于调整为(300,300)
的原始 BGR 图像,其中未将具有平均值的标量添加到该图像,并且已交换了蓝色和红色通道。
在以下屏幕截图中可以看到此脚本的输出:
在上一个屏幕截图中,我们可以看到获得的四个图像(img_from_blob
,img_from_blob_swap
,img_from_blob_mean
和img_from_blob_mean_swap
)。
在blob_from_images.py
脚本中,我们首先加载两个 BGR 图像,并使用cv2.dnn.blobFromImages()
函数创建一个二维 BLOB。 您可以检查创建的 BLOB 的形状是否为(2, 3, 300, 300)
。 然后,我们调用get_images_from_blob()
函数,该函数可用于执行逆预处理转换,以便再次获取输入图像。
get_images_from_blob
函数的代码如下:
def get_images_from_blob(blob_imgs, scalefactor, dim, mean, swap_rb, mean_added):
"""Returns images from blob"""
images_from_blob = cv2.dnn.imagesFromBlob(blob_imgs)
imgs = []
for image_blob in images_from_blob:
image_from_blob = np.reshape(image_blob, dim) / scalefactor
image_from_blob_mean = np.uint8(image_from_blob)
image_from_blob = image_from_blob_mean + np.uint8(mean)
if mean_added is True:
if swap_rb:
image_from_blob = image_from_blob[:, :, ::-1]
imgs.append(image_from_blob)
else:
if swap_rb:
image_from_blob_mean = image_from_blob_mean[:, :, ::-1]
imgs.append(image_from_blob_mean)
return imgs
如前所示,get_images_from_blob()
函数使用 OpenCV cv2.dnn.imagesFromBlob()
函数从 BLOB 返回图像。 在脚本中,我们利用此函数从 BLOB 中获取不同的图像,如下所示:
# Load images and get the list of images:
image = cv2.imread("face_test.jpg")
image2 = cv2.imread("face_test_2.jpg")
images = [image, image2]
# Call cv2.dnn.blobFromImages():
blob_images = cv2.dnn.blobFromImages(images, 1.0, (300, 300), [104., 117., 123.], False, False)
# The shape of the blob_image will be (2, 3, 300, 300):
print(blob_images.shape)
# Get different images from the blob:
imgs_from_blob = get_images_from_blob(blob_images, 1.0, (300, 300, 3), [104., 117., 123.], False, True)
imgs_from_blob_swap = get_images_from_blob(blob_images, 1.0, (300, 300, 3), [104., 117., 123.], True, True)
imgs_from_blob_mean = get_images_from_blob(blob_images, 1.0, (300, 300, 3), [104., 117., 123.], False, False)
imgs_from_blob_mean_swap = get_images_from_blob(blob_images, 1.0, (300, 300, 3), [104., 117., 123.], True, False)
在前面的代码中,我们利用get_images_from_blob()
函数从 BLOB 获取不同的图像。 创建的图像说明如下:
imgs_from_blob
图像对应于调整为(300,300)
尺寸的原始 BGR 图像。imgs_from_blob_swap
图像对应于调整为(300,300)
尺寸的原始 BGR 图像,并且蓝色和红色通道已交换。imgs_from_blob_mean
图像对应于调整为(300,300)
尺寸的原始 BGR 图像,其中带有平均值的标量尚未添加到图像。imgs_from_blob_mean_swap
图像对应于调整为(300,300)
的原始 BGR 图像,其中未将具有平均值的标量添加到图像中,并且蓝色和红色通道已交换。
在以下屏幕截图中可以看到此脚本的输出:
cv2.dnn.blobFromImage()
和cv2.dnn.blobFromImages()
的最后一个考虑因素是crop
参数,该参数指示是否裁切图像。 在裁剪的情况下,图像将从中心裁剪,如以下屏幕截图所示:
如您所见,裁剪是从图像的中心进行的,由黄线表示。 为了复制 OpenCV 在cv2.dnn.blobFromImage()
和cv2.dnn.blobFromImages()
函数内部执行的裁剪,我们对get_cropped_img()
函数进行了如下编码:
def get_cropped_img(img):
"""Returns the cropped image"""
# calculate size of resulting image:
size = min(img.shape[1], img.shape[0])
# calculate x1, and y1
x1 = int(0.5 * (img.shape[1] - size))
y1 = int(0.5 * (img.shape[0] - size))
# crop and return the image
return img[y1:(y1 + size), x1:(x1 + size)]
如您所见,裁剪图像的大小将基于原始图像的最小尺寸。 因此,在前面的示例中,裁剪后的图像将具有(482, 482)
的大小。
在blob_from_images_cropping.py
脚本中,我们看到了裁剪的效果,并且还在get_cropped_img()
函数中复制了裁剪过程:
# Load images and get the list of images:
image = cv2.imread("face_test.jpg")
image2 = cv2.imread("face_test_2.jpg")
images = [image, image2]
# To see how cropping works, we are going to perform the cropping formulation that
# both blobFromImage() and blobFromImages() perform applying it to one of the input images:
cropped_img = get_cropped_img(image)
# cv2.imwrite("cropped_img.jpg", cropped_img)
# Call cv2.dnn.blobFromImages():
blob_images = cv2.dnn.blobFromImages(images, 1.0, (300, 300), [104., 117., 123.], False, False)
blob_blob_images_cropped = cv2.dnn.blobFromImages(images, 1.0, (300, 300), [104., 117., 123.], False, True)
# Get different images from the blob:
imgs_from_blob = get_images_from_blob(blob_images, 1.0, (300, 300, 3), [104., 117., 123.], False, True)
imgs_from_blob_cropped = get_images_from_blob(blob_blob_images_cropped, 1.0, (300, 300, 3), [104., 117., 123.], False, True)
在以下屏幕截图中可以看到blob_from_images_cropping.py
脚本的输出:
可以看到在两个加载的图像中裁剪的效果,并且我们还可以理解保持了宽高比。
OpenCV DNN 人脸检测器的完整示例
接下来,我们将看到如何修改face_detection_opencv_dnn.py
脚本(摘自第 11 章,“人脸检测,跟踪和识别”),以便执行以下操作:
- 当几张图像(可能具有不同的大小)馈送到网络时,计算输出–
face_detection_opencv_cnn_images.py
脚本 - 当
cv2.dnn.blobFromImages()
函数-face_detection_opencv_cnn_images_crop.py
脚本中的crop=True
参数馈入网络时,将几张图像(可能具有不同的尺寸)馈送到网络时,计算输出
以下屏幕快照显示了face_detection_opencv_cnn_images.py
脚本的输出:
以下屏幕快照显示了face_detection_opencv_cnn_images_crop.py
脚本的输出:
在上一个屏幕截图中,您可以清楚地看到从中心裁剪图像时的区别。
OpenCV 深度学习分类
本节将介绍如何使用不同的预训练模型进行图像分类的几个示例。 请注意,您可以通过使用net.getPerfProfile()
方法获得推断时间,如下所示:
# Feed the input blob to the network, perform inference and get the output:
net.setInput(blob)
preds = net.forward()
# Get inference time:
t, _ = net.getPerfProfile()
print('Inference time: %.2f ms' % (t * 1000.0 / cv2.getTickFrequency()))
如您所见,在执行推断后将调用net.getPerfProfile()
方法。
net.getPerfProfile()
方法返回推理的总时间和层的计时(以滴答为单位)。 这样,您可以使用不同的深度学习架构比较推理时间。
我们将从下一部分介绍的 AlexNet 架构开始,了解主要的深度学习分类架构。
用于图像分类的 AlexNet
在image_classification_opencv_alexnet_caffe.py
脚本中,通过使用 AlexNet 和 Caffe 预训练模型,使用 OpenCV DNN 模块进行图像分类。 第一步是加载类的名称。 第二步是从磁盘加载序列化的 Caffe 模型。 第三步是加载输入图像进行分类。 第四步是创建大小为(227, 2327)
和(104, 117, 123)
平均减法值的 BLOB。 第五步是将输入 BLOB 馈送到网络,执行推理并获得输出。 第六步是获得概率最高(降序排列)的 10 个索引。 这样,具有最高概率(最高预测)的索引将是第一个。 最后,我们将在图像上绘制与最高预测相关的类和概率。 在以下屏幕截图中可以看到此脚本的输出:
如前面的屏幕快照所示,最高的预测对应于教堂的概率为 0.8325679898。
十大预测如下:
1\. label: church, probability: 0.8325679898
2\. label: monastery, probability: 0.043678388
3\. label: mosque, probability: 0.03827961534
4\. label: bell cote, probability: 0.02479489893
5\. label: beacon, probability: 0.01249620412
6\. label: dome, probability: 0.01223050058
7\. label: missile, probability: 0.006323920097
8\. label: projectile, probability: 0.005275635514
9\. label: palace, probability: 0.004289720673
10\. label: castle, probability: 0.003241452388
还应注意,在绘制类别和概率时,我们执行以下操作:
text = "label: {} probability: {:.2f}%".format(classes[indexes[0]], preds[0][indexes[0]] * 100)
print(text)
y0, dy = 30, 30
for i, line in enumerate(text.split('\n')):
y = y0 + i * dy
cv2.putText(image, line, (5, y), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2)
这样,可以将文本拆分并在图像的不同行中绘制。 例如,如果我们执行以下代码,则文本将分两行绘制:
text = "label: {}\nprobability: {:.2f}%".format(classes[indexes[0]], preds[0][indexes[0]] * 100)
应当注意,bvlc_alexnet.caffemodel
文件未包含在本书的存储库中,因为它超过了 GitHub 的文件大小限制 100.00 MB。 您必须从这里下载。
因此,您必须在运行脚本之前下载bvlc_alexnet.caffemodel
文件。
用于图像分类的 GoogLeNet
以与先前脚本类似的方式,在image_classification_opencv_googlenet_caffe.py
脚本中使用 GoogLeNet 和 Caffe 预训练模型使用 OpenCV CNN 模块进行图像分类。
在以下屏幕截图中可以看到此脚本的输出:
如前面的屏幕快照所示,最高的预测对应于一座教堂,其概率为 0.9082632661。
十大预测如下:
1\. label: church, probability: 0.9082632661
2\. label: bell cote, probability: 0.06350905448
3\. label: monastery, probability: 0.02046923898
4\. label: dome, probability: 0.002624791814
5\. label: mosque, probability: 0.001077500987
6\. label: fountain, probability: 0.001011475339
7\. label: palace, probability: 0.0007750992081
8\. label: castle, probability: 0.0002349214483
9\. label: pedestal, probability: 0.0002306570677
10\. label: analog clock, probability: 0.0002107089822
用于图像分类的 ResNet
用于图像分类的 ResNet 脚本(image_classification_opencv_restnet_50_caffe.py
)将使用带有 Caffe 预训练模型的 ResNet-50 进行图像分类。
在以下屏幕截图中可以看到输出:
如前面的屏幕快照所示,最高的预测对应于一座教堂的概率为 0.9955400825。
十大预测如下:
1\. label: church, probability: 0.9955400825
2\. label: dome, probability: 0.002429900225
3\. label: bell cote, probability: 0.0007424423238
4\. label: monastery, probability: 0.0003768313909
5\. label: picket fence, probability: 0.0003282549733
6\. label: mosque, probability: 0.000258318265
7\. label: mobile home, probability: 0.0001083607058
8\. label: stupa, probability: 2.96174203e-05
9\. label: palace, probability: 2.621001659e-05
10\. label: beacon, probability: 2.02897063e-05
用于图像分类的 SqueezeNet
在image_classification_opencv_squeezenet_caffe.py
脚本中,我们使用 SqueezeNet 架构执行图像分类,该架构可将 AlexNet 级别的精度降低 50 倍。 在以下屏幕截图中可以看到此脚本的输出:
如前面的屏幕快照所示,最高的预测对应于一座教堂的概率为 0.9967952371。
在此脚本中,我们使用的是 SqueezeNet v1.1,其计算量比 v1.0 少 2.4 倍,但又不牺牲任何准确率。
十大预测如下:
1\. label: church, probability: 0.9967952371
2\. label: monastery, probability: 0.001899079769
3\. label: bell cote, probability: 0.0006924766349
4\. label: mosque, probability: 0.0002616141282
5\. label: dome, probability: 0.0001891527208
6\. label: palace, probability: 0.0001046952093
7\. label: stupa, probability: 8.239243471e-06
8\. label: vault, probability: 7.135886335e-06
9\. label: triumphal arch, probability: 6.732503152e-06
10\. label: cinema, probability: 4.201304819e-06
OpenCV 深度学习对象检测
本节将介绍如何使用不同的预训练模型执行对象检测的几个示例。 对象检测尝试检测图像或视频中预定义类(例如猫,汽车和人)的语义对象实例。
用于对象检测的 MobileNet-SSD
我们将结合使用 MobileNet 架构和 SSD 框架。 MobileNets 可以看作是用于移动视觉应用的高效卷积神经网络。
MobileNet-SSD 在 COCO 数据集上进行了训练,并在 PASCAL VOC 上进行了微调,达到了 72.27% 的 mAP(请参阅汇总 mAP 的表格以了解对象检测算法,以将该指标置于上下文中)。 在 PASCAL VOC 上进行微调时,可以检测到 20 个对象类,如下所示:
- 人:人
- 动物:鸟,猫,牛,狗,马和羊
- 车辆:飞机,自行车,轮船,公共汽车,汽车,摩托车和火车
- 室内:瓶子,椅子,餐桌,盆栽,沙发和电视/显示器
在object_detection_opencv_mobilenet_caffe.py
脚本中,我们使用 OpenCV DNN 模块通过使用 MobileNet-SSD 和 Caffe 预训练模型来执行对象检测。
在以下屏幕截图中可以看到此脚本的输出:
如上一个屏幕截图所示,所有对象都可以高精度正确检测。
用于对象检测的 YOLO
在object_detection_opencv_yolo_darknet.py
脚本中,使用 YOLO v3 进行对象检测。 YOLO v3 使用了一些技巧来改善训练并提高表现,其中包括多尺度预测和更好的主干分类器。
在以下屏幕截图中可以看到此脚本的输出:
如上一个屏幕截图所示,可以高度准确地检测到除一个(绵羊)以外的所有物体。 因此,您必须在运行脚本之前下载yolov3.weights
文件。
应当注意,yolov3.weights
文件未包含在本书的存储库中,因为它超过了 GitHub 的文件大小限制 100.00 MB。 您必须从这里下载。
TensorFlow 库
TensorFlow 是 Google Brain 团队为内部使用而开发的用于机器学习和深度学习的开源软件平台。 随后,TensorFlow 于 2015 年在 Apache 许可下发布。在本节中,我们将看到一些示例,以向您介绍 TensorFlow 库。
TensorFlow 的介绍示例
TensorFlow 库通过将操作链接到计算图中来表示要执行的计算。 创建此计算图后,您可以打开 TensorFlow 会话并执行该计算图以获取结果。 可以在tensorflow_basic_op.py
脚本中看到此过程,该脚本执行在计算图中定义的乘法运算,如下所示:
# path to the folder that we want to save the logs for Tensorboard
logs_path = "./logs"
# Define placeholders:
X_1 = tf.placeholder(tf.int16, name="X_1")
X_2 = tf.placeholder(tf.int16, name="X_2")
# Define a multiplication operation:
multiply = tf.multiply(X_1, X_2, name="my_multiplication")
在会话中运行图时,将提供占位符的值,如以下代码片段所示:
# Start the session and run the operation with different inputs:
with tf.Session() as sess:
summary_writer = tf.summary.FileWriter(logs_path, sess.graph)
print("2 x 3 = {}".format(sess.run(multiply, feed_dict={X_1: 2, X_2: 3})))
print("[2, 3] x [3, 4] = {}".format(sess.run(multiply, feed_dict={X_1: [2, 3], X_2: [3, 4]})))
如您所见,计算图已参数化以访问外部输入,称为占位符。 在同一会话中,我们将使用不同的输入执行两次乘法。 由于计算图是 TensorFlow 的关键点,因此图的可视化可以帮助您使用 TensorBoard 来理解和调试图,TensorBoard 是任何标准 TensorFlow 安装随附的可视化软件。 要使用 TensorBoard 可视化计算图,您需要使用tf.summary.FileWriter()
编写程序的日志文件,如前所示。 如果执行此脚本,则会在执行该脚本的相同位置创建logs
目录。 要运行 TensorBoard,您应该执行以下代码:
$ tensorboard --logdir="./logs"
这将生成一个链接(http://localhost:6006/
),供您在浏览器中输入,您将看到 TensorBoard 页面,该页面可以在以下屏幕截图中看到:
您可以看到先前脚本的计算图。 另外,由于 TensorFlow 图可以具有成千上万个节点,因此可以创建范围以简化可视化,并且 TensorBoard 使用此信息来定义图中节点的层次结构。 这个想法显示在tensorflow_basic_ops_scope.py
脚本中,其中我们在Operations
范围内定义了两个操作(加法和乘法),如下所示:
with tf.name_scope('Operations'):
addition = tf.add(X_1, X_2, name="my_addition")
multiply = tf.multiply(X_1, X_2, name="my_multiplication")
如果执行脚本并重复前面的步骤,则可以在以下屏幕截图中看到 TensorBoard 中显示的计算图:
请注意,您还可以在脚本中使用常量(tf.Constant
)和变量(tf.Variable
)。 tf.Variable
和tf.placeholder
之间的差异在于传递值的时间。 如您在前面的示例中所看到的,使用tf.placeholder
不必提供初始值,并且这些值在运行时使用会话内的feed_dict
参数指定。 另一方面,如果使用tf.Variable
变量,则在声明它时必须提供一个初始值。
占位符只是一个变量,之后将向其分配数据。 在训练/测试算法时,通常使用占位符将训练/测试数据输入到计算图中。
为了简化起见,我们不会在下一个脚本中显示已创建的计算图,但是推荐使用 TensorBoard 来可视化计算图的方法,因为这将有助于您理解(以及验证)执行哪些计算。
TensorFlow 中的线性回归
在接下来的示例中,我们将使用 TensorFlow 执行线性回归,以帮助您了解训练和测试深度学习算法时所需的其他概念。
更具体地说,我们将看到三个脚本。 在每个脚本中,我们将涵盖以下主题:
tensorflow_linear_regression_training.py
:此脚本生成线性回归模型。tensorflow_linear_regression_testing.py
:此脚本加载创建的模型并使用它进行新的预测。tensorflow_save_and_load_using_model_builder.py
:此脚本加载创建的模型,并使用SavedModelBuilder()
导出模型以进行推断。 此外,此脚本还加载最终模型以做出新的预测。
线性回归是一种非常普遍的统计方法,它使我们能够根据给定的二维样本点集对关系进行建模。 在这种情况下,模型函数如下:
这描述了具有W
斜率和y
-截距b
的线。 因此,目标是找到W
和b
参数的值,这些值将在某种意义上为二维采样点提供最佳拟合(例如,最小化均方误差)。
在训练线性回归模型(请参阅tensorflow_linear_regression_training.py
)时,第一步是生成一些数据,用于训练算法,如下所示:
x = np.linspace(0, N, N)
y = 3 * np.linspace(0, N, N) + np.random.uniform(-10, 10, N)
下一步是定义占位符,以便在训练过程中将我们的训练数据输入到优化器中,如下所示:
X = tf.placeholder("float", name='X')
Y = tf.placeholder("float", name='Y')
此时,我们为权重和偏差声明两个变量(随机初始化),如下所示:
W = tf.Variable(np.random.randn(), name="W")
b = tf.Variable(np.random.randn(), name="b")
下一步是构建线性模型,如下所示:
y_model = tf.add(tf.multiply(X, W), b, name="y_model")
我们还定义了成本函数。 在这种情况下,我们将使用均方误差cost
函数,如以下代码片段所示:
cost = tf.reduce_sum(tf.pow(y_model - Y, 2)) / (2 * N)
现在,我们创建梯度下降优化器,以最小化cost
函数,修改W
和b
变量的值。
传统的优化器称为梯度下降(旨在查找函数最小值的迭代优化算法),如下所示:
optimizer = tf.train.GradientDescentOptimizer(learning_rate).minimize(cost)
学习率参数控制每次梯度下降算法更新时系数的变化量。 如前所述,梯度下降是一种迭代优化算法,因此,在每次迭代中,根据学习率参数修改参数。
创建模型的最后一步是执行变量的初始化,如下所示:
init = tf.global_variables_initializer()
此时,我们可以在一个会话中开始训练过程,如以下代码片段所示:
# Start the training procedure inside a TensorFlow Session:
with tf.Session() as sess:
# Run the initializer:
sess.run(init)
# Uncomment if you want to see the created graph
# summary_writer = tf.summary.FileWriter(logs_path, sess.graph)
# Iterate over all defined epochs:
for epoch in range(training_epochs):
# Feed each training data point into the optimizer:
for (_x, _y) in zip(x, y):
sess.run(optimizer, feed_dict={X: _x, Y: _y})
# Display the results every 'display_step' epochs:
if (epoch + 1) % disp_step == 0:
# Calculate the actual cost, W and b:
c = sess.run(cost, feed_dict={X: x, Y: y})
w_est = sess.run(W)
b_est = sess.run(b)
print("Epoch", (epoch + 1), ": cost =", c, "W =", w_est, "b =", b_est)
# Save the final model
saver.save(sess, './linear_regression')
# Storing necessary values to be used outside the session
training_cost = sess.run(cost, feed_dict={X: x, Y: y})
weight = sess.run(W)
bias = sess.run(b)
print("Training finished!")
如前面的代码片段所示,一旦会话开始,我们将运行初始化器,然后对所有定义的时期进行迭代以训练线性回归模型。 此外,我们为每个display_step
周期打印结果。 最后,训练完成后,我们将保存最终模型。
至此,训练结束,我们可以显示结果了,可以在以下屏幕截图中看到:
在上图中,我们可以看到训练数据(左)和与线性回归模型相对应的拟合线(右)。
保存最终模型(saver.save(sess, './linear_regression')
)时,将创建四个文件:
.meta
文件:包含 TensorFlow 图.data
文件:包含权重,偏差,梯度和所有其他已保存变量的值.index
文件:标识检查点checkpoint
文件:记录保存的最新检查点文件
此时,我们可以加载预训练的模型并将其用于预测目的。 这在tensorflow_linear_regression_testing.py
脚本中执行。 加载模型时要做的第一件事是从.meta
文件中加载图,如下所示:
tf.reset_default_graph()
imported_meta = tf.train.import_meta_graph("linear_regression.meta")
第二步是加载变量的值(请注意,这些值仅在会话中存在)。 我们还运行模型以获取W
,b
的值和新的预测值,如下所示:
with tf.Session() as sess:
imported_meta.restore(sess, './linear_regression')
# Run the model to get the values of the variables W, b and new prediction values:
W_estimated = sess.run('W:0')
b_estimated = sess.run('b:0')
new_predictions = sess.run(['y_model:0'], {'X:0': new_x})
此时,我们可以显示训练数据,回归线和新获得的预测,可以在以下屏幕截图中看到:
如上一个屏幕截图所示,我们使用了预先训练的模型来进行新的预测(蓝点)。 但是,在生产中提供模型时,我们只希望将模型及其权重很好地打包在一个文件中,以方便存储,版本控制和更新不同模型。 结果将是一个扩展名为.pb
的二进制文件,其中包含受训网络的拓扑和权重。 在tensorflow_save_and_load_using_model_builder.py
脚本中执行创建此二进制文件的过程以及如何将其用于推理。
在此脚本中,我们对export_model()
函数进行了编码,以使用SaveModel
导出训练后的模型,如下所示:
def export_model():
"""Exports the model"""
trained_checkpoint_prefix = 'linear_regression'
loaded_graph = tf.Graph()
with tf.Session(graph=loaded_graph) as sess:
sess.run(tf.global_variables_initializer())
# Restore from checkpoint
loader = tf.train.import_meta_graph(trained_checkpoint_prefix + '.meta')
loader.restore(sess, trained_checkpoint_prefix)
# Add signature:
...
signature_map = {signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY: signature}
# Export model:
builder = tf.saved_model.builder.SavedModelBuilder('./my_model')
builder.add_meta_graph_and_variables(sess, signature_def_map=signature_map,
tags=[tf.saved_model.tag_constants.SERVING])
builder.save()
这将在my_model
文件夹内创建saved_model.pb
。 在这一点上,为了验证是否正确生成了导出的模型,我们可以同时导入和使用它,以便进行新的预测,如下所示:
with tf.Session(graph=tf.Graph()) as sess:
tf.saved_model.loader.load(sess, [tf.saved_model.tag_constants.SERVING], './my_model')
graph = tf.get_default_graph()
x = graph.get_tensor_by_name('X:0')
model = graph.get_tensor_by_name('y_model:0')
print(sess.run(model, {x: new_x}))
调用load
函数后,该图将作为默认图被加载。 此外,变量也已加载,因此您可以开始对任何新数据运行推理。 这将输出[153.04472 166.54755 180.05037]
数组,它对应于我们的模型生成的预测值。
使用 TensorFlow 的手写数字识别
在此示例中,我们将使用 TensorFlow 对图像进行分类。 更具体地说,我们将创建一个简单的模型(softmax 回归模型),用于使用 MNIST 数据集学习和预测图像中的手写数字。
Softmax 回归是可用于多类分类的逻辑回归的概括。 MNIST 数据集包含各种手写的数字图像:
mnist_tensorflow_save_model.py
脚本创建用于学习和预测图像中手写数字的模型。
主要步骤如下所示。 您可以使用以下代码自动导入此数据集:
from tensorflow.examples.tutorials.mnist import input_data
data = input_data.read_data_sets("MNIST/", one_hot=True)
下载的数据集由三部分组成-55,000 行mnist.train
训练数据,10,000 行mnist.test
测试数据和 5,000 行mnist.validation
验证数据。 此外,训练,测试和验证部分还为每个数字包含相应的标签。 例如,训练数据由mnist.train.images
(训练数据集图像)和mnist.train.labels
(训练数据集标签)组成。 每个图像由28 x 28
像素组成,从而形成784
元素数组。 one_hot=True
选项意味着标签将以这样的方式表示:特定数字的1
只有一位。 例如,对于9
,相应的标签将为[0 0 0 0 0 0 0 0 0 1]
。
这项技术称为单热编码,这意味着标签已从单个数字转换为向量,向量的长度等于可能的类数。 这样,除了i
元素(其值将是与i
类相对应的1
之外),向量的所有元素都将设置为零。
在定义占位符时,我们需要匹配其形状和类型,以便将数据输入以下变量:
x = tf.placeholder(tf.float32, shape=[None, 784], name='myInput')
y = tf.placeholder(tf.float32, shape=[None, 10], name='Y')
当我们将None
分配给占位符时,这意味着可以根据需要为该占位符提供尽可能多的示例。 在这种情况下,x
占位符可以输入任何 784 维向量。 因此,该张量的形状为[None, 784 ]
。 此外,我们还创建了y
占位符,以提供真实标签。 在这种情况下,该张量的形状将为[None, 10]
。
至此,我们可以开始构建计算图了。 第一步是如下创建W
和b
变量:
W = tf.Variable(tf.zeros([784, 10]))
b = tf.Variable(tf.zeros([10]))
创建W
和b
变量并将它们初始化为零,因为 TensorFlow 会在训练时优化这些值。 W
的尺寸为[784, 10]
,因为我们想将其乘以与某个图像表示形式相对应的 784 维数组,以获得 10 维输出向量。
现在,我们可以按以下方式实现我们的模型:
output_logits = tf.matmul(x, W) + b
y_pred = tf.nn.softmax(output_logits, name='myOutput')
tf.matmul()
用于矩阵乘法,tf.nn.softmax()
用于将softmax
函数应用于输入张量,这意味着输出已归一化并且可以解释为概率。 在这一点上,我们可以定义损失函数,即创建优化器(在本例中为AdamOptimizer
),模型的准确率如下:
# Define the loss function, optimizer, and accuracy
loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=y, logits=output_logits), name='loss')
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate, name='Adam-op').minimize(loss)
correct_prediction = tf.equal(tf.argmax(output_logits, 1), tf.argmax(y, 1), name='correct_pred')
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32), name='accuracy')
最后,我们可以训练模型,并使用mnist.validation
验证数据对其进行验证,并按如下方式保存模型:
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for i in range(num_steps):
# Get a batch of training examples and their corresponding labels.
x_batch, y_true_batch = data.train.next_batch(batch_size)
# Put the batch into a dict to be fed into the placeholders
feed_dict_train = {x: x_batch, y: y_true_batch}
sess.run(optimizer, feed_dict=feed_dict_train)
# Validation:
feed_dict_validation = {x: data.validation.images, y: data.validation.labels}
loss_test, acc_test = sess.run([loss, accuracy], feed_dict=feed_dict_validation)
print("Validation loss: {}, Validation accuracy: {}".format(loss_test, acc_test))
# Save model:
saved_path_model = saver.save(sess, './softmax_regression_model_mnist')
print('Model has been saved in {}'.format(saved_path_model))
保存模型后,我们可以使用它来识别图像中的手写数字。 在mnist_save_and_load_model_builder.py
脚本中,我们将在my_model
文件夹中创建saved_model.pb
,并使用该模型对使用 OpenCV 加载图像进行新的预测。 为了保存模型,我们使用了上一节介绍的export_model()
函数。 为了做出新的预测,我们使用以下代码:
# Load some test images:
test_digit_0 = load_digit("digit_0.png")
test_digit_1 = load_digit("digit_1.png")
test_digit_2 = load_digit("digit_2.png")
test_digit_3 = load_digit("digit_3.png")
with tf.Session(graph=tf.Graph()) as sess:
tf.saved_model.loader.load(sess, [tf.saved_model.tag_constants.SERVING], './my_model')
graph = tf.get_default_graph()
x = graph.get_tensor_by_name('myInput:0')
model = graph.get_tensor_by_name('myOutput:0')
output = sess.run(model, {x: [test_digit_0, test_digit_1, test_digit_2, test_digit_3]})
print("predicted labels: {}".format(np.argmax(output, axis=1)))
在此,test_digit_0
,test_digit_1
,test_digit_2
和test_digit_3
是四个加载的图像,每个图像包含一个数字。 要加载每个图像,我们使用load_digit()
函数,如下所示:
def load_digit(image_name):
"""Loads a digit and pre-process in order to have the proper format"""
gray = cv2.imread(image_name, cv2.IMREAD_GRAYSCALE)
gray = cv2.resize(gray, (28, 28))
flatten = gray.flatten() / 255.0
return flatten
如您所见,我们必须对每个图像进行预处理,以具有与 MNIST 数据库图像的格式相对应的正确格式。 如果执行此脚本,则将为每个图像获得以下预测类:
predicted labels: [0 1 2 3]
Keras 库
Keras 是用 Python 编写的开放源代码,高级神经网络 API(与 Python 2.7-3.6 兼容)。 它能够在 TensorFlow,Microsoft Cognitive Toolkit,Theano 或 PlaidML 之上运行,并且其开发重点是实现快速实验。 在本节中,我们将看到两个示例。 在第一个示例中,我们将看到如何使用与上一节中的 TensorFlow 示例相同的输入数据来解决线性回归问题。 在第二个示例中,我们将使用 MNIST 数据集对一些手写数字进行分类,就像在上一节中使用 TensorFlow 进行的操作一样。 这样,当解决相同类型的问题时,您可以清楚地看到两个库之间的差异。
Keras 中的线性回归
linear_regression_keras_training.py
数据集执行线性回归模型的训练。 第一步是创建用于训练/测试算法的数据,如下所示:
# Generate random data composed by 50 (N = 50) points:
x = np.linspace(0, N, N)
y = 3 * np.linspace(0, N, N) + np.random.uniform(-10, 10, N)
下一步是创建模型。 为此,我们创建了create_model()
函数,如以下代码片段所示:
def create_model():
"""Create the model using Sequencial model"""
# Create a sequential model:
model = Sequential()
# All we need is a single connection so we use a Dense layer with linear activation:
model.add(Dense(input_dim=1, units=1, activation="linear", kernel_initializer="uniform"))
# Compile the model defining mean squared error(mse) as the loss
model.compile(optimizer=Adam(lr=0.1), loss='mse')
# Return the created model
return model
使用 Keras 时,最简单的模型类型是Sequential
模型,该模型可以看作是线性的层栈,并且在此示例中用于创建模型。 此外,对于更复杂的架构,可以使用 Keras 函数式 API,该 API 允许构建任意的层图。 因此,使用Sequential
模型,我们通过使用model.add()
方法堆叠层来构建模型。 在此示例中,我们使用具有线性激活函数的单个密集或全连接层。 接下来,我们可以编译(或配置)将均方误差(MSE)定义为损失的模型。 在这种情况下,将使用Adam
优化器,并设置学习率0.1
。
此时,我们现在可以使用model.fit()
方法训练提供数据的模型,如下所示:
linear_reg_model.fit(x, y, epochs=100, validation_split=0.2, verbose=1)
训练后,我们可以获得w
和b
的值(学习的参数),这些值将用于计算预测,如下所示:
w_final, b_final = get_weights(linear_reg_model)
get_weights()
函数返回这些参数的值,如下所示:
def get_weights(model):
"""Get weights of w and b"""
w = model.get_weights()[0][0][0]
b = model.get_weights()[1][0]
return w, b
在这一点上,我们可以建立以下预测:
# Calculate the predictions:
predictions = w_final * x + b_final
我们还可以如下保存模型:
linear_reg_model.save_weights("my_model.h5")
在以下屏幕截图中可以看到此脚本的输出:
如前面的屏幕快照所示,我们既可以看到训练数据(在左侧),也可以看到与线性回归模型相对应的拟合线(在右侧)。
我们可以加载预训练的模型进行预测。 可以在linear_regression_keras_testing.py
脚本中看到此示例。 第一步是按以下方式加载权重:
linear_reg_model.load_weights('my_model.h5')
使用get_weights()
函数,我们可以获得如下学习的参数:
m_final, b_final = get_weights(linear_reg_model)
此时,我们获得了训练数据的以下预测,并且还获得了新的预测:
predictions = linear_reg_model.predict(x)
new_predictions = linear_reg_model.predict(new_x)
最后一步是显示获得的结果,可以在以下屏幕截图中看到:
如上一个屏幕截图所示,我们使用了预先训练的模型来进行新的预测(蓝点)。
Keras 中的手写数字识别
在此示例中,我们将看到如何使用 Keras 识别手写数字。 mnist_keras_training.py
脚本使用四层神经网络创建模型,如以下代码片段所示:
def create_model():
"""Create the model using Sequencial model"""
# Create a sequential model (a simple NN is created) adding a softmax activation at the end with 10 units:
model = Sequential()
model.add(Dense(units=128, activation="relu", input_shape=(784,)))
model.add(Dense(units=128, activation="relu"))
model.add(Dense(units=128, activation="relu"))
model.add(Dense(units=10, activation="softmax"))
# Compile the model using the loss function "categorical_crossentropy" and Stocastic Gradient Descent optimizer:
model.compile(optimizer=SGD(0.001), loss="categorical_crossentropy", metrics=["accuracy"])
# Return the created model
return model
在这种情况下,我们使用categorical_crossentropy
损失函数(该损失函数非常适合比较两个概率分布,并使用随机梯度下降(SGD)优化器。
要加载 MNIST 数据,我们必须使用以下代码:
(train_x, train_y), (test_x, test_y) = mnist.load_data()
此外,我们必须重新调整加载的数据的形状以使其具有适当的形状,如下所示:
train_x = train_x.reshape(60000, 784)
test_x = test_x.reshape(10000, 784)
train_y = keras.utils.to_categorical(train_y, 10)
test_y = keras.utils.to_categorical(test_y, 10)
此时,我们可以创建模型,训练模型,保存创建的模型,并获得评估测试数据时获得的准确率,如下所示:
# Create the model:
model = create_model()
# Use the created model for training:
model.fit(train_x, train_y, batch_size=32, epochs=10, verbose=1)
# Save the created model:
model.save("mnist-model.h5")
# Get the accuracy when testing:
accuracy = model.evaluate(x=test_x, y=test_y, batch_size=32)
# Show the accuracy:
print("Accuracy: ", accuracy[1])
此时,我们准备使用预先训练的模型来预测图像中的新手写数字。 这是在mnist_keras_predicting.py
脚本中执行的,如下所示:
# Note: Images should have black background:
def load_digit(image_name):
"""Loads a digit and pre-process in order to have the proper format"""
gray = cv2.imread(image_name, cv2.IMREAD_GRAYSCALE)
gray = cv2.resize(gray, (28, 28))
gray = gray.reshape((1, 784))
return gray
# Create the model:
model = create_model()
# Load parameters of the model from the saved mode file:
model.load_weights("mnist-model.h5")
# Load some test images:
test_digit_0 = load_digit("digit_0.png")
test_digit_1 = load_digit("digit_1.png")
test_digit_2 = load_digit("digit_2.png")
test_digit_3 = load_digit("digit_3.png")
imgs = np.array([test_digit_0, test_digit_1, test_digit_2, test_digit_3])
imgs = imgs.reshape(4, 784)
# Predict the class of the loaded images
prediction_class = model.predict_classes(imgs)
# Print the predicted classes:
print("Class: ", prediction_class)
如您所见,我们已经加载了四个图像,并且使用了经过训练的模型来预测这些图像的类别。 获得的输出如下:
Class: [0 1 2 3]
总结
在本章中,我们使用一些流行的库(包括 OpenCV,TensorFlow 和 Keras)对深度学习进行了介绍。 在本章的第一部分,我们概述了用于图像分类和对象检测的最新深度学习架构。 在第二部分中,我们研究了 OpenCV 中的深度学习模块,这些模块提供了 DNN 库,该库通过使用一些流行的深度学习框架进行了预训练的深度网络来实现前向传递(推理)。 因此,从 OpenCV 3.3 开始,可以在我们的应用中使用经过预训练的网络进行预测。 在本章的后面,我们对 TensorFlow 进行了介绍,最后,对 Keras 进行了介绍。
在下一章中,我们将对移动和网络计算机视觉进行介绍。 更具体地说,我们将看到如何使用 OpenCV,Keras 和 Flask 创建 Web 计算机视觉以及 Web 深度学习应用,并学习了如何与它们结合以提供 Web 应用机器学习和深度学习功能。
问题
- 本章开头所述的机器学习和深度学习之间的三个主要区别是什么?
- 哪一年被认为是深度学习的爆炸式增长?
- 以下函数执行什么功能?
blob = cv2.dnn.blobFromImage(image, 1.0, (300, 300), [104., 117., 123.], False, False)
- 以下几行执行什么操作?
net.setInput(blob)
preds = net.forward()
- TensorFlow 中的占位符是什么?
- 在 TensorFlow 中使用
saver.save()
保存模型时,将创建四个文件? - 单热编码是什么意思?
- Keras 中的顺序模型是什么?
- Keras 中
model.fit()
的作用是什么?
进一步阅读
以下参考资料将帮助您深入了解 Python 的深度学习:
十三、使用 Python 和 OpenCV 的移动和 Web 计算机视觉
Web 计算是一个有趣的话题,因为它允许我们利用云计算。 从这个意义上讲,有许多 Python Web 框架可用于部署应用。 这些框架提供了包的集合,使开发人员可以专注于应用的核心逻辑,而不必处理底层细节(例如,协议,套接字或进程和线程管理等)。
在本章中,我们将使用 Flask(这是一个小型且功能强大的 Python Web 框架,已获得 BSD 许可),以建立计算机视觉和深度学习 Web 应用。 此外,我们将了解如何利用云计算将应用部署到云中,而不是在计算机上运行它们。
本章的主要部分如下:
- Python Web 框架简介
- Flask 简介
- 使用 OpenCV 和 Flask 的 Web 计算机视觉应用
- 使用 Keras 和 Flask 的深度学习 API
- 将 Flask 应用部署到云上
技术要求
技术要求如下:
- Python 和 OpenCV
- 特定于 Python 的 IDE
- NumPy 和 Matplotlib 包
- Git 客户端
- Flask(请参阅下一节“安装包中”的“如何安装 Flask”)
- Keras(请参阅下一节“安装包”中的“如何安装 Keras”)
- TensorFlow(请参阅下一节“安装包”中的“如何安装 TensorFlow”)
requests
(请参阅下一节“安装包”中的“如何安装requests
”)- Pillow(请参阅下一小节“安装包”的安装方法)
有关如何安装这些要求的更多详细信息,请参见第 1 章,“设置 OpenCV”。 可以通过以下 URL 访问《精通 Python OpenCV 4》的 GitHub 存储库,其中包含从本书第一章到最后一章所需的所有支持项目文件。
在接下来的小节中,我们将介绍如何使用pip
命令安装必要的包(Flask,Keras,TensorFlow 和请求)。
安装包
让我们快速回顾一下如何安装所需的包:
- Flask:您可以使用以下命令安装 Flask:
$ pip install flask
要检查安装是否正确执行,只需打开 Python shell 并尝试导入 Flask 库:
python
import Flask
- TensorFlow:您可以使用以下命令安装 TensorFlow:
$ pip install tensorflow
要检查安装是否正确执行,只需打开一个 Python shell 并尝试导入 TensorFlow 库:
python
import tensorflow
- Keras:您可以使用以下命令安装 Keras:
$ pip install keras
要检查安装是否正确执行,只需打开一个 Python shell 并尝试导入 Keras 库:
python
import keras
requests
:您可以使用以下命令安装requests
:
$ pip install requests
要检查安装是否正确执行,只需打开 Python Shell 并尝试导入请求库:
python
import requests
- Pillow:为了安装 Pillow,请使用以下命令:
pip install Pillow
要检查安装是否正确执行,只需打开 Python shell 并尝试导入 Pillow 库:
python
import PIL
我想提一下,推荐的方法是在虚拟环境中安装包。 请参阅第 1 章,“设置 OpenCV”,以了解有关创建和管理虚拟环境的更多信息。
Python Web 框架简介
Python Web 框架提供了一组包,这些包使开发人员可以专注于应用的核心逻辑,而不必处理底层细节 (例如,协议,套接字或进程以及线程管理等)。 此外,可以将这些框架分为全栈和非全栈框架。 Django 和 Flask 是两个流行的 Python 网络框架,我们将在本章稍后讨论:
全栈框架的完美示例是 Django,这是一个免费的开源全栈 Python 框架, 尝试默认包含所有必需的功能,而不是将它们作为单独的库提供。 Django 使创建 Web 应用更加容易,并且比其他框架所需的时间更少。 它着重于通过遵循不要重复(DRY)的原理来实现尽可能的自动化。 如果您有兴趣学习 Django,建议您阅读本教程,该教程是关于编写第一个 Django 应用。
Flask(已获得 BSD 许可)可被视为非全栈框架的完美示例。 实际上,Flask 被认为是一个微框架,它是一个很少或完全不依赖外部库的框架。
Flask 具有以下依赖关系:
Django 和 Flask 均可用于开发计算机视觉和深度学习应用。 但是,公认的是,Django 的学习曲线比 Flask 略陡。 此外,Flask 专注于简约和简约。 例如,Flask 的Hello World
应用只有几行代码。 此外,还建议将 Flask 用于较小和较不复杂的应用,而 Django 通常用于较大和较复杂的应用。
在本章中,我们将了解如何将 Flask 用于创建计算机视觉和深度学习 Web 应用。
Flask 简介
正如我们提到的,这是 Flask 的Hello World
应用,仅包含几行代码。 可以在hello.py
脚本中看到,如下所示:
# Import required packages:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello World!"
if __name__ == "__main__":
app.run()
导入所需的包后,我们将创建Flask
类的实例,该实例将成为我们的 Web 服务器网关接口(WSGI)应用。 route()
装饰器用于指示哪个 URL 应该触发hello()
函数,该函数将打印消息Hello World!
。
在 Flask 中,您可以使用route()
装饰器将函数绑定到 URL。
使用以下命令执行此脚本:
$ python hello.py
您将在控制台中看到此消息,告诉您 Web 服务器已启动:
* Serving Flask app "hello" (lazy loading)
* Environment: production
WARNING: Do not use the development server in a production environment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
此时,您可以打开浏览器并输入http://127.0.0.1:5000/
。 这将对我们的服务器执行GET
请求,该请求将返回相应的消息,使我们可以在浏览器中看到它。 这些步骤可以总结在下一个屏幕截图中:
如您所见,浏览器显示Hello World!
消息。
在前面的示例中,脚本称为hello.py
。 确保不要调用您的应用flask.py
,因为这可能导致与 Flask 本身发生冲突。
在前面的示例中,只能从我们自己的计算机访问服务器,而不能从网络中的任何其他服务器访问服务器。 为了使服务器公开可用,在运行服务器应用时应添加参数host=0.0.0.0
。 可以在hello_external.py
脚本中看到:
# Import required packages:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello World!"
if __name__ == "__main__":
# Add parameter host='0.0.0.0' to run on your machines IP address:
app.run(host='0.0.0.0')
这样,我们可以从连接到该网络的任何其他设备执行请求,如下面的屏幕快照所示:
如前所述,可以使用route()
装饰器将函数绑定到 URL,如hello_routes_external.py
脚本所示:
# Import required packages:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello World!"
@app.route("/user")
def hello_user():
return "User: Hello World!"
if __name__ == "__main__":
# Add parameter host='0.0.0.0' to run on your machines IP address:
app.run(host='0.0.0.0')
在下一个屏幕截图中对此进行了说明,我们在其中请求http://192.168.1.101:5000/user
URL,并获得User: Hello World!
消息:
在本节中,我们介绍了一些使用 Flask 创建应用时必须考虑的基本概念。
在下一节中,我们将看到不同的示例,以了解如何使用 OpenCV 和 Flask 创建 Web 计算机视觉应用。
使用 OpenCV 和 Flask 的 Web 计算机视觉应用
在本节中,我们将看到如何使用 OpenCV 和 Flask 创建 Web 计算机视觉应用。 我们将从使用 OpenCV 和 Flask 的等效Hello world
应用开始。
OpenCV 和 Flask 的最小示例
对脚本hello_opencv.py
进行了编码,以显示如何使用 OpenCV 执行基本的 Web 计算机视觉应用。 该脚本的代码如下所示:
# Import required packages:
import cv2
from flask import Flask, request, make_response
import numpy as np
import urllib.request
app = Flask(__name__)
@app.route('/canny', methods=['GET'])
def canny_processing():
# Get the image:
with urllib.request.urlopen(request.args.get('url')) as url:
image_array = np.asarray(bytearray(url.read()), dtype=np.uint8)
# Convert the image to OpenCV format:
img_opencv = cv2.imdecode(image_array, -1)
# Convert image to grayscale:
gray = cv2.cvtColor(img_opencv, cv2.COLOR_BGR2GRAY)
# Perform canny edge detection:
edges = cv2.Canny(gray, 100, 200)
# Compress the image and store it in the memory buffer:
retval, buffer = cv2.imencode('.jpg', edges)
# Build the response:
response = make_response(buffer.tobytes())
response.headers['Content-Type'] = 'image/jpeg'
# Return the response:
return response
if __name__ == "__main__":
# Add parameter host='0.0.0.0' to run on your machines IP address:
app.run(host='0.0.0.0')
可以通过以下步骤来解释先前的代码:
- 第一步是导入所需的包。 在此示例中,我们使用
route()
装饰器将canny_processing()
函数绑定到/canny
URL。 此外,还需要url
参数才能正确执行GET
请求。 为了获得该参数,使用了request.args.get()
函数。 - 下一步是读取该 URL 持有的图像,如下所示:
with urllib.request.urlopen(request.args.get('url')) as url:
image_array = np.asarray(bytearray(url.read()), dtype=np.uint8)
这样,图像将作为数组读取。
- 下一步是将图像转换为 OpenCV 格式并执行 Canny 边缘处理,该处理应在相应的灰度图像上执行:
# Convert the image to OpenCV format:
img_opencv = cv2.imdecode(image_array, -1)
# Convet image to grayscale:
gray = cv2.cvtColor(img_opencv, cv2.COLOR_BGR2GRAY)
# Perform canny edge detection:
edges = cv2.Canny(gray, 100, 200)
- 下一步是压缩图像并将其存储在内存缓冲区中,如下所示:
# Compress the image and store it in the memory buffer:
retval, buffer = cv2.imencode('.jpg', edges)
- 最后一步是构建响应并将其返回给客户端,如下所示:
# Build and return the response:
response = make_response(buffer.tobytes())
response.headers['Content-Type'] = 'image/jpeg'
# Return the response:
return response
如果我们运行脚本($ python hello_opencv.py
),服务器将运行,然后,如果我们从客户端(例如我们的手机)执行GET
请求,我们将获得处理后的图像,该图像可以在下一个屏幕截图中可以看到。 此外,请考虑到您可能需要禁用防火墙(在 Windows 上)才能执行以下请求:
如图所示,我们执行了以下GET
请求:
http://192.168.1.101:5000/canny?url=https://raw.githubusercontent.com/opencv/opencv/master/samples/data/lena.jpg
在这里,https://raw.githubusercontent.com/opencv/opencv/master/samples/data/lena.jpg
是我们要使用 Web 计算视觉应用处理的图像。
使用 OpenCV 的最小人脸 API
在此示例中,我们将看到如何使用 OpenCV 和 Flask 创建 Web Face API。 minimal_face_api
项目对 Web 服务器应用进行编码。 main.py
脚本负责解析请求并建立对客户端的响应。 该脚本的代码如下:
# Import required packages:
from flask import Flask, request, jsonify
import urllib.request
from face_processing import FaceProcessing
# Initialize application and FaceProcessing():
app = Flask(__name__)
fc = FaceProcessing()
@app.errorhandler(400)
def bad_request(e):
# return also the code error
return jsonify({"status": "not ok", "message": "this server could not understand your request"}), 400
@app.errorhandler(404)
def not_found(e):
# return also the code error
return jsonify({"status": "not found", "message": "route not found"}), 404
@app.errorhandler(500)
def not_found(e):
# return also the code error
return jsonify({"status": "internal error", "message": "internal error occurred in server"}), 500
@app.route('/detect', methods=['GET', 'POST', 'PUT'])
def detect_human_faces():
if request.method == 'GET':
if request.args.get('url'):
with urllib.request.urlopen(request.args.get('url')) as url:
return jsonify({"status": "ok", "result": fc.face_detection(url.read())}), 200
else:
return jsonify({"status": "bad request", "message": "Parameter url is not present"}), 400
elif request.method == 'POST':
if request.files.get("image"):
return jsonify({"status": "ok", "result": fc.face_detection(request.files["image"].read())}), 200
else:
return jsonify({"status": "bad request", "message": "Parameter image is not present"}), 400
else:
return jsonify({"status": "failure", "message": "PUT method not supported for API"}), 405
if __name__ == "__main__":
# Add parameter host='0.0.0.0' to run on your machines IP address:
app.run(host='0.0.0.0')
如您所见,我们利用jsonify()
函数创建具有application/json
MIME 类型的给定参数的 JSON 表示形式。 可以将 JSON 视为信息交换的事实标准,在此示例中,我们将返回 JSON 响应,而不是像在上一个示例中执行的那样返回图像。 您还可以看到,此 API 支持GET
和POST
请求。 另外,在main.py
脚本中,我们还通过使用errorhandler()
装饰函数来注册错误处理器。 还记得在将响应返回给客户端时还要设置错误代码。
图像处理是在face_processing.py
脚本中执行的,其中FaceProcessing()
类被编码为:
# Import required packages:
import cv2
import numpy as np
import os
class FaceProcessing(object):
def __init__(self):
self.file = os.path.join(os.path.join(os.path.dirname(__file__), "data"), "haarcascade_frontalface_alt.xml")
self.face_cascade = cv2.CascadeClassifier(self.file)
def face_detection(self, image):
# Convert image to OpenCV format:
image_array = np.asarray(bytearray(image), dtype=np.uint8)
img_opencv = cv2.imdecode(image_array, -1)
output = []
# Detect faces and build output:
gray = cv2.cvtColor(img_opencv, cv2.COLOR_BGR2GRAY)
faces = self.face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(25, 25))
for face in faces:
# face.tolist(): returns a copy of the array data as a Python list
x, y, w, h = face.tolist()
face = {"box": [x, y, x + w, y + h]}
output.append(face)
# Return output:
return output
face_detection()
方法通过使用 OpenCV detectMultiScale()
函数执行面部检测。 对于每个检测到的人脸,我们将获取其坐标(x, y, w, h)
,并通过以适当格式对检测进行编码来构建box
:
face = {"box": [x, y, x + w, y + h]}
最后,我们将编码的人脸检测添加到output
中:
output.append(face)
将所有检测到的面部添加到输出后,我们将返回它。
要使用此 API,我们可以按照与前面示例相同的方式从浏览器执行GET
请求。 此外,由于我们的 API 也支持POST
请求,因此我们包含了两个脚本来测试此 API 的功能。 这些脚本同时执行GET
和POST
请求,以了解如何与上述 Face API 进行交互。 更具体地说,demo_request.py
对面部 API 执行几个请求,以获得不同的响应,并查看错误处理的工作方式。
在此脚本中,我们首先使用错误的 URL 执行GET
请求:
# Import required packages:
import requests
FACE_DETECTION_REST_API_URL = "http://localhost:5000/detect"
FACE_DETECTION_REST_API_URL_WRONG = "http://localhost:5000/process"
IMAGE_PATH = "test_face_processing.jpg"
URL_IMAGE = "https://raw.githubusercontent.com/opencv/opencv/master/samples/data/lena.jpg"
# Submit the GET request:
r = requests.get(FACE_DETECTION_REST_API_URL_WRONG)
# See the response:
print("status code: {}".format(r.status_code))
print("headers: {}".format(r.headers))
print("content: {}".format(r.json()))
在这种情况下,我们得到以下信息:
status code: 404
headers: {'Content-Type': 'application/json', 'Content-Length': '51', 'Server': 'Werkzeug/0.14.1 Python/3.6.6', 'Date': 'Sat, 16 Feb 2019 19:20:25 GMT'}
content: {'message': 'route not found', 'status': 'not found'}
获得的状态码(404
)意味着客户端可以与服务器通信,但是服务器找不到请求的内容。 这是因为请求的 URL(http://localhost:5000/process
)不正确。
我们执行的第二个请求是正确的GET
请求:
# Submit the GET request:
payload = {'url': URL_IMAGE}
r = requests.get(FACE_DETECTION_REST_API_URL, params=payload)
# See the response:
print("status code: {}".format(r.status_code))
print("headers: {}".format(r.headers))
print("content: {}".format(r.json()))
在这种情况下,我们得到以下信息:
status code: 200
headers: {'Content-Type': 'application/json', 'Content-Length': '53', 'Server': 'Werkzeug/0.14.1 Python/3.6.6', 'Date': 'Sat, 16 Feb 2019 19:20:31 GMT'}
content: {'result': [{'box': [213, 200, 391, 378]}], 'status': 'ok'}
状态码(200
)指示请求已成功执行。 此外,您还可以看到已检测到与 Lenna 的脸相对应的一张脸。
我们执行的第三个请求也是GET
请求,但有效负载丢失:
# Submit the GET request:
r = requests.get(FACE_DETECTION_REST_API_URL)
# See the response:
print("status code: {}".format(r.status_code))
print("headers: {}".format(r.headers))
print("content: {}".format(r.json()))
在这种情况下,我们得到的响应如下:
status code: 400
headers: {'Content-Type': 'application/json', 'Content-Length': '66', 'Server': 'Werkzeug/0.14.1 Python/3.6.6', 'Date': 'Sat, 16 Feb 2019 19:20:32 GMT'}
content: {'message': 'Parameter url is not present', 'status': 'bad request'}
状态码(400
)表示请求错误。 如您所见,url
参数丢失。
我们执行的第四个请求是带有正确有效负载的POST
请求:
# Load the image and construct the payload:
image = open(IMAGE_PATH, "rb").read()
payload = {"image": image}
# Submit the POST request:
r = requests.post(FACE_DETECTION_REST_API_URL, files=payload)
# See the response:
print("status code: {}".format(r.status_code))
print("headers: {}".format(r.headers))
print("content: {}".format(r.json()))
我们得到以下回应:
status code: 200
headers: {'Content-Type': 'application/json', 'Content-Length': '449', 'Server': 'Werkzeug/0.14.1 Python/3.6.6', 'Date': 'Sat, 16 Feb 2019 19:20:34 GMT'}
content: {'result': [{'box': [151, 29, 193, 71]}, {'box': [77, 38, 115, 76]}, {'box': [448, 37, 490, 79]}, {'box': [81, 172, 127, 218]}, {'box': [536, 47, 574, 85]}, {'box': [288, 173, 331, 216]}, {'box': [509, 170, 553, 214]}, {'box': [357, 48, 399, 90]}, {'box': [182, 179, 219, 216]}, {'box': [251, 38, 293, 80]}, {'box': [400, 174, 444, 218]}, {'box': [390, 87, 430, 127]}, {'box': [54, 89, 97, 132]}, {'box': [499, 91, 542, 134]}, {'box': [159, 95, 198, 134]}, {'box': [310, 115, 344, 149]}, {'box': [225, 116, 265, 156]}], 'status': 'ok'}
如您所见,检测到许多面部。 这是因为test_face_processing.jpg
包含很多人脸。
最终请求是PUT
请求:
# Submit the PUT request:
r = requests.put(FACE_DETECTION_REST_API_URL, files=payload)
# See the response:
print("status code: {}".format(r.status_code))
print("headers: {}".format(r.headers))
print("content: {}".format(r.json()))
我们得到以下输出:
status code: 405
headers: {'Content-Type': 'application/json', 'Content-Length': '66', 'Server': 'Werkzeug/0.14.1 Python/3.6.6', 'Date': 'Sat, 16 Feb 2019 19:20:35 GMT'}
content: {'message': 'PUT method not supported for API', 'status': 'failure'}
如您所见,不支持PUT
方法。 此面部 API 仅支持GET
和POST
方法。
如您在前面的响应中看到的那样,当请求成功执行时,我们将检测到的面部作为 JSON 数据。 为了了解如何解析响应并使用它绘制检测到的面部,我们可以对脚本demo_request_drawing.py
进行如下编码:
# Import required packages:
import cv2
import numpy as np
import requests
from matplotlib import pyplot as plt
def show_img_with_matplotlib(color_img, title, pos):
"""Shows an image using matplotlib capabilities"""
img_RGB = color_img[:, :, ::-1]
ax = plt.subplot(1, 1, pos)
plt.imshow(img_RGB)
plt.title(title)
plt.axis('off')
FACE_DETECTION_REST_API_URL = "http://localhost:5000/detect"
IMAGE_PATH = "test_face_processing.jpg"
# Load the image and construct the payload:
image = open(IMAGE_PATH, "rb").read()
payload = {"image": image}
# Submit the POST request:
r = requests.post(FACE_DETECTION_REST_API_URL, files=payload)
# See the response:
print("status code: {}".format(r.status_code))
print("headers: {}".format(r.headers))
print("content: {}".format(r.json()))
# Get JSON data from the response and get 'result':
json_data = r.json()
result = json_data['result']
# Convert the loaded image to the OpenCV format:
image_array = np.asarray(bytearray(image), dtype=np.uint8)
img_opencv = cv2.imdecode(image_array, -1)
# Draw faces in the OpenCV image:
for face in result:
left, top, right, bottom = face['box']
# To draw a rectangle, you need top-left corner and bottom-right corner of rectangle:
cv2.rectangle(img_opencv, (left, top), (right, bottom), (0, 255, 255), 2)
# Draw top-left corner and bottom-right corner (checking):
cv2.circle(img_opencv, (left, top), 5, (0, 0, 255), -1)
cv2.circle(img_opencv, (right, bottom), 5, (255, 0, 0), -1)
# Create the dimensions of the figure and set title:
fig = plt.figure(figsize=(8, 8))
plt.suptitle("Using face detection API", fontsize=14, fontweight='bold')
fig.patch.set_facecolor('silver')
# Show the output image
show_img_with_matplotlib(img_opencv, "face detection", 1)
# Show the Figure:
plt.show()
从上面可以看出,我们首先加载图像并构造有效负载。 然后,我们执行POST
请求。 然后,我们从响应中获取 JSON 数据并获得result
:
# Get JSON data from the response and get 'result':
json_data = r.json()
result = json_data['result']
在这一点上,我们可以绘制检测到的人脸,对所有检测到的人脸进行迭代,如下所示:
# Draw faces in the OpenCV image:
for face in result:
left, top, right, bottom = face['box']
# To draw a rectangle, you need top-left corner and bottom-right corner of rectangle:
cv2.rectangle(img_opencv, (left, top), (right, bottom), (0, 255, 255), 2)
# Draw top-left corner and bottom-right corner (checking):
cv2.circle(img_opencv, (left, top), 5, (0, 0, 255), -1)
cv2.circle(img_opencv, (right, bottom), 5, (255, 0, 0), -1)
对于每个检测到的脸部,我们绘制一个矩形以及左上角和右下角点。 下一个屏幕截图中可以看到此脚本的输出:
使用 OpenCV 使用 API检测到的人脸
如上一个屏幕截图所示,已检测到所有面部。
使用 OpenCV 的深度学习猫检测 API
遵循在上一个示例中使用的相同方法-使用 OpenCV 的最小面部 API,我们将使用 OpenCV 创建深度学习 API。 更具体地说,我们将看到如何创建深度学习猫检测 API。 cat_detection_api
项目对 Web 服务器应用进行编码。 main.py
脚本负责解析请求并建立对客户端的响应。 该脚本的代码如下:
# Import required packages:
from flask import Flask, request, jsonify
import urllib.request
from image_processing import ImageProcessing
app = Flask(__name__)
ip = ImageProcessing()
@app.errorhandler(400)
def bad_request(e):
# return also the code error
return jsonify({"status": "not ok", "message": "this server could not understand your request"}), 400
@app.errorhandler(404)
def not_found(e):
# return also the code error
return jsonify({"status": "not found", "message": "route not found"}), 404
@app.errorhandler(500)
def not_found(e):
# return also the code error
return jsonify({"status": "internal error", "message": "internal error occurred in server"}), 500
@app.route('/catfacedetection', methods=['GET', 'POST', 'PUT'])
def detect_cat_faces():
if request.method == 'GET':
if request.args.get('url'):
with urllib.request.urlopen(request.args.get('url')) as url:
return jsonify({"status": "ok", "result": ip.cat_face_detection(url.read())}), 200
else:
return jsonify({"status": "bad request", "message": "Parameter url is not present"}), 400
elif request.method == 'POST':
if request.files.get("image"):
return jsonify({"status": "ok", "result": ip.cat_face_detection(request.files["image"].read())}), 200
else:
return jsonify({"status": "bad request", "message": "Parameter image is not present"}), 400
else:
return jsonify({"status": "failure", "message": "PUT method not supported for API"}), 405
@app.route('/catdetection', methods=['GET', 'POST', 'PUT'])
def detect_cats():
if request.method == 'GET':
if request.args.get('url'):
with urllib.request.urlopen(request.args.get('url')) as url:
return jsonify({"status": "ok", "result": ip.cat_detection(url.read())}), 200
else:
return jsonify({"status": "bad request", "message": "Parameter url is not present"}), 400
elif request.method == 'POST':
if request.files.get("image"):
return jsonify({"status": "ok", "result": ip.cat_detection(request.files["image"].read())}), 200
else:
return jsonify({"status": "bad request", "message": "Parameter image is not present"}), 400
else:
return jsonify({"status": "failure", "message": "PUT method not supported for API"}), 405
if __name__ == "__main__":
# Add parameter host='0.0.0.0' to run on your machines IP address:
app.run(host='0.0.0.0')
如您所见,我们利用route()
装饰器将detect_cat_faces()
函数绑定到/catfacedetection
URL,还将detect_cats()
函数绑定到/catdetection
URL。 另外,我们利用jsonify()
函数创建具有application/json
MIME 类型的给定参数的 JSON 表示形式。 您还可以看到,此 API 支持GET
和POST
请求。 此外,在main.py
脚本中,我们还通过使用errorhandler()
装饰函数来注册错误处理器。 将响应返回给客户端时,请记住还要设置错误代码。
图像处理在image_processing.py
脚本中进行,其中ImageProcessing()
类被编码。 从这个意义上讲,仅显示cat_face_detection()
和cat_detection()
方法:
class ImageProcessing(object):
def __init__(self):
...
...
def cat_face_detection(self, image):
image_array = np.asarray(bytearray(image), dtype=np.uint8)
img_opencv = cv2.imdecode(image_array, -1)
output = []
gray = cv2.cvtColor(img_opencv, cv2.COLOR_BGR2GRAY)
cats = self.cat_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(25, 25))
for cat in cats:
# face.tolist(): returns a copy of the array data as a Python list
x, y, w, h = cat.tolist()
face = {"box": [x, y, x + w, y + h]}
output.append(face)
return output
def cat_detection(self, image):
image_array = np.asarray(bytearray(image), dtype=np.uint8)
img_opencv = cv2.imdecode(image_array, -1)
# Create the blob with a size of (300,300), mean subtraction values (127.5, 127.5, 127.5):
# and also a scalefactor of 0.007843:
blob = cv2.dnn.blobFromImage(img_opencv, 0.007843, (300, 300), (127.5, 127.5, 127.5))
# Feed the input blob to the network, perform inference and ghe the output:
self.net.setInput(blob)
detections = self.net.forward()
# Size of frame resize (300x300)
dim = 300
output = []
# Process all detections:
for i in range(detections.shape[2]):
# Get the confidence of the prediction:
confidence = detections[0, 0, i, 2]
# Filter predictions by confidence:
if confidence > 0.1:
# Get the class label:
class_id = int(detections[0, 0, i, 1])
# Get the coordinates of the object location:
left = int(detections[0, 0, i, 3] * dim)
top = int(detections[0, 0, i, 4] * dim)
right = int(detections[0, 0, i, 5] * dim)
bottom = int(detections[0, 0, i, 6] * dim)
# Factor for scale to original size of frame
heightFactor = img_opencv.shape[0] / dim
widthFactor = img_opencv.shape[1] / dim
# Scale object detection to frame
left = int(widthFactor * left)
top = int(heightFactor * top)
right = int(widthFactor * right)
bottom = int(heightFactor * bottom)
# Check if we have detected a cat:
if self.classes[class_id] == 'cat':
cat = {"box": [left, top, right, bottom]}
output.append(cat)
return output
如此处所示,实现了两种方法。 cat_face_detection()
方法使用 OpenCV detectMultiScale()
函数执行猫脸检测。
cat_detection()
方法使用在 Cafe-SSD 框架中训练的 MobileNet SSD 对象检测执行猫检测,并且可以检测20
类。 在此示例中,我们将检测猫。 因此,如果class_id
是一只猫,我们将把检测结果添加到输出中。 有关如何处理检测和使用预训练的深度学习模型的更多信息,我们建议第 12 章,“深度学习简介”,该课程侧重于深度学习。 完整代码可在这个页面中找到。
为了测试此 API,可以使用demo_request_drawing.py
脚本,如下所示:
# Import required packages:
import cv2
import numpy as np
import requests
from matplotlib import pyplot as plt
def show_img_with_matplotlib(color_img, title, pos):
"""Shows an image using matplotlib capabilities"""
img_RGB = color_img[:, :, ::-1]
ax = plt.subplot(1, 1, pos)
plt.imshow(img_RGB)
plt.title(title)
plt.axis('off')
CAT_FACE_DETECTION_REST_API_URL = "http://localhost:5000/catfacedetection"
CAT_DETECTION_REST_API_URL = "http://localhost:5000/catdetection"
IMAGE_PATH = "cat.jpg"
# Load the image and construct the payload:
image = open(IMAGE_PATH, "rb").read()
payload = {"image": image}
# Convert the loaded image to the OpenCV format:
image_array = np.asarray(bytearray(image), dtype=np.uint8)
img_opencv = cv2.imdecode(image_array, -1)
# Submit the POST request:
r = requests.post(CAT_DETECTION_REST_API_URL, files=payload)
# See the response:
print("status code: {}".format(r.status_code))
print("headers: {}".format(r.headers))
print("content: {}".format(r.json()))
# Get JSON data from the response and get 'result':
json_data = r.json()
result = json_data['result']
# Draw cats in the OpenCV image:
for cat in result:
left, top, right, bottom = cat['box']
# To draw a rectangle, you need top-left corner and bottom-right corner of rectangle:
cv2.rectangle(img_opencv, (left, top), (right, bottom), (0, 255, 0), 2)
# Draw top-left corner and bottom-right corner (checking):
cv2.circle(img_opencv, (left, top), 10, (0, 0, 255), -1)
cv2.circle(img_opencv, (right, bottom), 10, (255, 0, 0), -1)
# Submit the POST request:
r = requests.post(CAT_FACE_DETECTION_REST_API_URL, files=payload)
# See the response:
print("status code: {}".format(r.status_code))
print("headers: {}".format(r.headers))
print("content: {}".format(r.json()))
# Get JSON data from the response and get 'result':
json_data = r.json()
result = json_data['result']
# Draw cat faces in the OpenCV image:
for face in result:
left, top, right, bottom = face['box']
# To draw a rectangle, you need top-left corner and bottom-right corner of rectangle:
cv2.rectangle(img_opencv, (left, top), (right, bottom), (0, 255, 255), 2)
# Draw top-left corner and bottom-right corner (checking):
cv2.circle(img_opencv, (left, top), 10, (0, 0, 255), -1)
cv2.circle(img_opencv, (right, bottom), 10, (255, 0, 0), -1)
# Create the dimensions of the figure and set title:
fig = plt.figure(figsize=(6, 7))
plt.suptitle("Using cat detection API", fontsize=14, fontweight='bold')
fig.patch.set_facecolor('silver')
# Show the output image
show_img_with_matplotlib(img_opencv, "cat detection", 1)
# Show the Figure:
plt.show()
在先前的脚本中,我们执行两个POST
请求,以便同时检测猫的脸部以及cat.jpg
图像中的猫。 此外,我们还解析了两个请求的响应并绘制了结果,这可以在此脚本的输出中看到,如以下屏幕快照所示:
如前面的屏幕快照所示,绘制了猫脸检测和全身猫检测。
使用 Keras 和 Flask 的深度学习 API
在第 12 章,“深度学习简介”中,我们看到了如何同时使用 TensorFlow 和 Keras 创建深度学习应用。 在本节中,我们将看到如何使用 Keras 和 Flask 创建深度学习 API。
更具体地说,我们将看到如何与 Keras 中包含的经过预先训练的深度学习架构一起使用,然后,我们将看到如何使用这些经过预先训练的深度学习架构来创建深度学习 API。
Keras 应用
Keras 应用 ,与 python 2.7-3.6 兼容,并以 MIT 许可分发)是 Keras 深度学习库的应用模块, 为许多流行的架构(例如 VGG16,ResNet50,Xception 和 MobileNet 等)提供深度学习模型定义和预训练权重,这些架构可用于预测,特征提取和微调。
在 Keras 安装期间会下载模型架构,但是在实例化模型时会自动下载预先训练的权重。 此外,所有这些深度学习架构都与所有后端(TensorFlow,Theano 和 CNTK)兼容。
这些深度学习架构在 ImageNet 数据集上进行了训练和验证,用于将图像分类为1,000
类别或类别之一:
在上一个屏幕截图中,您可以在 Keras 应用模块中查看各个模型的文档:
- Xception:Xception V1 模型,权重在 ImageNet 上进行了预训练
- VGG16:VGG16 模型,权重在 ImageNet 上进行了预训练
- VGG19:VGG19 模型,权重在 ImageNet 上进行了预训练
- ResNet50:ResNet50 模型,权重在 ImageNet 上进行了预训练
- InceptionV3:InceptionV3 模型,权重在 ImageNet 上进行了预训练
- InceptionResNetV2:InceptionResNetV2 模型,权重在 ImageNet 上进行了预训练
- MobileNet:MobileNet 模型,权重在 ImageNet 上进行了预训练
- MobileNetV2:MobileNetV2 模型,权重在 ImageNet 上进行了预训练
- DenseNet121:DenseNet121 模型,权重在 ImageNet 上进行了预训练
- DenseNet169:DenseNet169 模型,权重在 ImageNet 上进行了预训练
- DenseNet201:DenseNet201 模型,权重在 ImageNet 上进行了预训练
- NASNetMobile:NASNetMobile 模型,权重在 ImageNet 上进行了预训练
- NASNetLarge:NASNetLarge 模型,权重在 ImageNet 上进行了预训练
在classification_keras_pretrained_imagenet_models.py
脚本中,我们展示了如何将这些经过预训练的模型用于预测。
这些预训练的模型还可以用于特征提取(例如,从任意中间层进行特征提取)和微调(例如,在一组新的类上微调预训练的模型)。
接下来可以看到classification_keras_pretrained_imagenet_models.py
脚本的关键代码。 应该注意的是,该脚本需要很长时间才能执行。 完整代码可以在这个页面:
# Import required packages
...
def preprocessing_image(img_path, target_size, architecture):
"""Image preprocessing to be used for each Deep Learning architecture"""
# Load image in PIL format
img = image.load_img(img_path, target_size=target_size)
# Convert PIL format to numpy array:
x = image.img_to_array(img)
# Convert the image/images into batch format:
x = np.expand_dims(x, axis=0)
# Pre-process (prepare) the image using the specific architecture:
x = architecture.preprocess_input(x)
return x
def put_text(img, model_name, decoded_preds, y_pos):
"""Show the predicted results in the image"""
cv2.putText(img, "{}: {}, {:.2f}".format(model_name, decoded_preds[0][0][1], decoded_preds[0][0][2]),
(20, y_pos), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 0, 255), 2)
# Path of the input image to be classified:
img_path = 'car.jpg'
# Load some available models:
model_inception_v3 = inception_v3.InceptionV3(weights='imagenet')
model_vgg_16 = vgg16.VGG16(weights='imagenet')
model_vgg_19 = vgg19.VGG19(weights='imagenet')
model_resnet_50 = resnet50.ResNet50(weights='imagenet')
model_mobilenet = mobilenet.MobileNet(weights='imagenet')
model_xception = xception.Xception(weights='imagenet')
model_nasnet_mobile = nasnet.NASNetMobile(weights='imagenet')
model_densenet_121 = densenet.DenseNet121(weights='imagenet')
# Prepare the image for the corresponding architecture:
x_inception_v3 = preprocessing_image(img_path, (299, 299), inception_v3)
x_vgg_16 = preprocessing_image(img_path, (224, 224), vgg16)
x_vgg_19 = preprocessing_image(img_path, (224, 224), vgg19)
x_resnet_50 = preprocessing_image(img_path, (224, 224), resnet50)
x_mobilenet = preprocessing_image(img_path, (224, 224), mobilenet)
x_xception = preprocessing_image(img_path, (299, 299), xception)
x_nasnet_mobile = preprocessing_image(img_path, (224, 224), nasnet)
x_densenet_121 = preprocessing_image(img_path, (224, 224), densenet)
# Get the predicted probabilities:
preds_inception_v3 = model_inception_v3.predict(x_inception_v3)
preds_vgg_16 = model_vgg_16.predict(x_vgg_16)
preds_vgg_19 = model_vgg_19.predict(x_vgg_19)
preds_resnet_50 = model_resnet_50.predict(x_resnet_50)
preds_mobilenet = model_mobilenet.predict(x_mobilenet)
preds_xception = model_xception.predict(x_xception)
preds_nasnet_mobile = model_nasnet_mobile.predict(x_nasnet_mobile)
preds_densenet_121 = model_nasnet_mobile.predict(x_densenet_121)
# Print the results (class, description, probability):
print('Predicted InceptionV3:', decode_predictions(preds_inception_v3, top=5)[0])
print('Predicted VGG16:', decode_predictions(preds_vgg_16, top=5)[0])
print('Predicted VGG19:', decode_predictions(preds_vgg_19, top=5)[0])
print('Predicted ResNet50:', decode_predictions(preds_resnet_50, top=5)[0])
print('Predicted MobileNet:', decode_predictions(preds_mobilenet, top=5)[0])
print('Predicted Xception:', decode_predictions(preds_xception, top=5)[0])
print('Predicted NASNetMobile:', decode_predictions(preds_nasnet_mobile, top=5)[0])
print('Predicted DenseNet121:', decode_predictions(preds_densenet_121, top=5)[0])
# Show results:
numpy_image = np.uint8(image.img_to_array(image.load_img(img_path))).copy()
numpy_image = cv2.resize(numpy_image, (500, 500))
numpy_image_res = numpy_image.copy()
put_text(numpy_image_res, "InceptionV3", decode_predictions(preds_inception_v3), 40)
put_text(numpy_image_res, "VGG16", decode_predictions(preds_vgg_16), 65)
put_text(numpy_image_res, "VGG19", decode_predictions(preds_vgg_19), 90)
put_text(numpy_image_res, "ResNet50", decode_predictions(preds_resnet_50), 115)
put_text(numpy_image_res, "MobileNet", decode_predictions(preds_mobilenet), 140)
put_text(numpy_image_res, "Xception", decode_predictions(preds_xception), 165)
put_text(numpy_image_res, "NASNetMobile", decode_predictions(preds_nasnet_mobile), 190)
put_text(numpy_image_res, "DenseNet121", decode_predictions(preds_densenet_121), 215)
第一步是导入所需的包,如下所示:
from keras.preprocessing import image
from keras.applications import inception_v3, vgg16, vgg19, resnet50, mobilenet, xception, nasnet, densenet
from keras.applications.imagenet_utils import decode_predictions
第二步是实例化不同的模型架构,如下所示:
# Load some available models:
model_inception_v3 = inception_v3.InceptionV3(weights='imagenet')
model_vgg_16 = vgg16.VGG16(weights='imagenet')
model_vgg_19 = vgg19.VGG19(weights='imagenet')
model_resnet_50 = resnet50.ResNet50(weights='imagenet')
model_mobilenet = mobilenet.MobileNet(weights='imagenet')
model_xception = xception.Xception(weights='imagenet')
model_nasnet_mobile = nasnet.NASNetMobile(weights='imagenet')
model_densenet_121 = densenet.DenseNet121(weights='imagenet')
第三步是加载和预处理图像以进行分类。 为此,我们具有preprocessing_image()
函数:
def preprocessing_image(img_path, target_size, architecture):
"""Image preprocessing to be used for each Deep Learning architecture"""
# Load image in PIL format
img = image.load_img(img_path, target_size=target_size)
# Convert PIL format to numpy array:
x = image.img_to_array(img)
# Convert the image/images into batch format:
x = np.expand_dims(x, axis=0)
# Pre-process (prepare) the image using the specific architecture:
x = architecture.preprocess_input(x)
return x
preprocessing_image()
函数的第一步是使用image.load_img()
函数加载图像,并指定目标尺寸。 应当注意,Keras 以 PIL 格式(width, height)
加载图像,应使用image.img_to_array()
函数将其转换为 NumPy 格式(height, width, channels)
。 然后,应使用 NumPy 的expand_dims()
函数将输入图像转换为三维张量(batchsize, height, width, channels)
。 预处理图像的最后一步是对图像进行规范化,这是每种架构所特有的。 这可以通过调用preprocess_input()
函数来实现。
我们使用上述preprocessing_image()
,如下所示:
# Prepare the image for the corresponding architecture:
x_inception_v3 = preprocessing_image(img_path, (299, 299), inception_v3)
x_vgg_16 = preprocessing_image(img_path, (224, 224), vgg16)
x_vgg_19 = preprocessing_image(img_path, (224, 224), vgg19)
x_resnet_50 = preprocessing_image(img_path, (224, 224), resnet50)
x_mobilenet = preprocessing_image(img_path, (224, 224), mobilenet)
x_xception = preprocessing_image(img_path, (299, 299), xception)
x_nasnet_mobile = preprocessing_image(img_path, (224, 224), nasnet)
x_densenet_121 = preprocessing_image(img_path, (224, 224), densenet)
一旦对图像进行了预处理,我们可以使用model.predict()
获得分类结果(每个类的预测概率):
# Get the predicted probabilities:
preds_inception_v3 = model_inception_v3.predict(x_inception_v3)
preds_vgg_16 = model_vgg_16.predict(x_vgg_16)
preds_vgg_19 = model_vgg_19.predict(x_vgg_19)
preds_resnet_50 = model_resnet_50.predict(x_resnet_50)
preds_mobilenet = model_mobilenet.predict(x_mobilenet)
preds_xception = model_xception.predict(x_xception)
preds_nasnet_mobile = model_nasnet_mobile.predict(x_nasnet_mobile)
preds_densenet_121 = model_nasnet_mobile.predict(x_densenet_121)
可以将预测值解码为元组列表(class ID
,description
和confidence of prediction
):
# Print the results (class, description, probability):
print('Predicted InceptionV3:', decode_predictions(preds_inception_v3, top=5)[0])
print('Predicted VGG16:', decode_predictions(preds_vgg_16, top=5)[0])
print('Predicted VGG19:', decode_predictions(preds_vgg_19, top=5)[0])
print('Predicted ResNet50:', decode_predictions(preds_resnet_50, top=5)[0])
print('Predicted MobileNet:', decode_predictions(preds_mobilenet, top=5)[0])
print('Predicted Xception:', decode_predictions(preds_xception, top=5)[0])
print('Predicted NASNetMobile:', decode_predictions(preds_nasnet_mobile, top=5)[0])
print('Predicted DenseNet121:', decode_predictions(preds_densenet_121, top=5)[0])
应该注意的是,我们为批量中的每个图像获得一个元组列表(class ID
,description
和confidence of prediction
)。
在这种情况下,仅一个图像用作输入。 获得的输出如下:
Predicted InceptionV3: [('n04285008', 'sports_car', 0.5347126), ('n03459775', 'grille', 0.26265427), ('n03100240', 'convertible', 0.04198084), ('n03770679', 'minivan', 0.030852199), ('n02814533', 'beach_wagon', 0.01985116)]
Predicted VGG16: [('n03770679', 'minivan', 0.38101497), ('n04285008', 'sports_car', 0.11982699), ('n04037443', 'racer', 0.079280525), ('n02930766', 'cab', 0.063257575), ('n02974003', 'car_wheel', 0.058513235)]
Predicted VGG19: [('n03770679', 'minivan', 0.23455109), ('n04285008', 'sports_car', 0.22764407), ('n04037443', 'racer', 0.091262065), ('n02930766', 'cab', 0.082842484), ('n02974003', 'car_wheel', 0.07619765)]
Predicted ResNet50: [('n04285008', 'sports_car', 0.2878513), ('n03770679', 'minivan', 0.27558535), ('n03459775', 'grille', 0.14996652), ('n02974003', 'car_wheel', 0.07796249), ('n04037443', 'racer', 0.050856136)]
Predicted MobileNet: [('n04285008', 'sports_car', 0.2911019), ('n03770679', 'minivan', 0.24308795), ('n04037443', 'racer', 0.17548184), ('n02814533', 'beach_wagon', 0.12273211), ('n02974003', 'car_wheel', 0.065000646)]
Predicted Xception: [('n04285008', 'sports_car', 0.3404192), ('n03770679', 'minivan', 0.12870753), ('n03459775', 'grille', 0.11251074), ('n03100240', 'convertible', 0.068289846), ('n03670208', 'limousine', 0.056636304)]
Predicted NASNetMobile: [('n04285008', 'sports_car', 0.54606944), ('n03100240', 'convertible', 0.2797665), ('n03459775', 'grille', 0.037253976), ('n02974003', 'car_wheel', 0.02682667), ('n02814533', 'beach_wagon', 0.014193514)]
Predicted DenseNet121: [('n04285008', 'sports_car', 0.65400195), ('n02974003', 'car_wheel', 0.076283), ('n03459775', 'grille', 0.06899618), ('n03100240', 'convertible', 0.058678553), ('n04037443', 'racer', 0.051732656)]
最后,我们使用put_text()
函数显示图像中每种架构的获得的结果(最佳预测):
put_text(numpy_image_res, "InceptionV3", decode_predictions(preds_inception_v3), 40)
put_text(numpy_image_res, "VGG16", decode_predictions(preds_vgg_16), 65)
put_text(numpy_image_res, "VGG19", decode_predictions(preds_vgg_19), 90)
put_text(numpy_image_res, "ResNet50", decode_predictions(preds_resnet_50), 115)
put_text(numpy_image_res, "MobileNet", decode_predictions(preds_mobilenet), 140)
put_text(numpy_image_res, "Xception", decode_predictions(preds_xception), 165)
put_text(numpy_image_res, "NASNetMobile", decode_predictions(preds_nasnet_mobile), 190)
put_text(numpy_image_res, "DenseNet121", decode_predictions(preds_densenet_121), 215)
put_text()
函数代码如下:
def put_text(img, model_name, decoded_preds, y_pos):
"""Show the predicted results in the image"""
cv2.putText(img, "{}: {}, {:.2f}".format(model_name, decoded_preds[0][0][1], decoded_preds[0][0][2]),
(20, y_pos), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 0, 255), 2)
在put_text()
函数中,我们调用cv2.putText()
函数在图像中渲染相应的字符串。
下一个屏幕截图中可以看到classification_keras_pretrained_imagenet_models.py
脚本的输出:
如前面的屏幕快照所示,大多数模型将此图像分类为sport_car
; 但是,VGG 模型(VGG16 和 VGG19)将此图像归类为minivan
,这可能是由于汽车的高度所致。
如果我们使用另一个输入图像(在这种情况下为cat.jpg
)运行脚本,则输出显示在下一个屏幕截图中:
在这种情况下,大多数模型将此图像分类为tabby
,这是家猫(虎斑猫)。 Xception 模型将此图像分类为Egyptian_cat
。
使用 Keras 应用的深度学习 REST API
在上一小节中,我们已经了解了如何使用 Keras 深度学习库的应用模块,为许多流行的架构提供了深度学习模型定义和预训练权重。
在本小节中,我们将了解如何基于这些预先训练的架构之一创建深度学习 REST API。
Keras 深度学习 REST API 是一个名为keras_server.py
的文件。 接下来可以看到此脚本的代码:
# Import required packages:
from keras.applications import nasnet, NASNetMobile
from keras.preprocessing.image import img_to_array
from keras.applications import imagenet_utils
from PIL import Image
import numpy as np
import flask
import io
import tensorflow as tf
# Initialize Flask app, Keras model and graph:
app = flask.Flask(__name__)
graph = None
model = None
def load_model():
# Get default graph:
global graph
graph = tf.get_default_graph()
# Load the pre-trained Keras model(pre-trained on ImageNet):
global model
model = NASNetMobile(weights="imagenet")
def preprocessing_image(image, target):
# Make sure the image mode is RGB:
if image.mode != "RGB":
image = image.convert("RGB")
# Resize the input image:
image = image.resize(target)
# Convert PIL format to numpy array:
image = img_to_array(image)
# Convert the image/images into batch format:
image = np.expand_dims(image, axis=0)
# Pre-process (prepare) the image using the specific architecture:
image = nasnet.preprocess_input(image)
# Return the image:
return image
@app.route("/predict", methods=["POST"])
def predict():
# Initialize result:
result = {"success": False}
if flask.request.method == "POST":
if flask.request.files.get("image"):
# Read input image in PIL format:
image = flask.request.files["image"].read()
image = Image.open(io.BytesIO(image))
# Pre-process the image to be classified:
image = preprocessing_image(image, target=(224, 224))
# Classify the input image:
with graph.as_default():
predictions = model.predict(image)
results = imagenet_utils.decode_predictions(predictions)
result["predictions"] = []
# Add the predictions to the result:
for (imagenet_id, label, prob) in results[0]:
r = {"label": label, "probability": float(prob)}
result["predictions"].append(r)
# At this point we can say that the request was dispatched successfully:
result["success"] = True
# Return result as a JSON response:
return flask.jsonify(result)
@app.route("/")
def home():
# Initialize result:
result = {"success": True}
# Return result as a JSON response:
return flask.jsonify(result)
if __name__ == "__main__":
print("Loading Keras pre-trained model")
load_model()
print("Starting")
app.run()
第一步是导入所需的包,如下所示:
# Import required packages:
from keras.applications import nasnet, NASNetMobile
from keras.preprocessing.image import img_to_array
from keras.applications import imagenet_utils
from PIL import Image
import numpy as np
import flask
import io
import tensorflow as tf
下一步是初始化 Flask 应用,我们的模型和计算图,如下所示:
# Initialize Flask app, Keras model and graph:
app = flask.Flask(__name__)
graph = None
model = None
我们定义load_model()
函数,该函数负责创建架构并加载所需的权重:
def load_model():
# Get default graph:
global graph
graph = tf.get_default_graph()
# Load the pre-trained Keras model(pre-trained on ImageNet):
global model
model = NASNetMobile(weights="imagenet")
可以看出,NASNetMobile 权重已加载。
我们还定义了preprocessing_image()
函数,如下所示:
def preprocessing_image(image, target):
# Make sure the image mode is RGB:
if image.mode != "RGB":
image = image.convert("RGB")
# Resize the input image:
image = image.resize(target)
# Convert PIL format to numpy array:
image = img_to_array(image)
# Convert the image/images into batch format:
image = np.expand_dims(image, axis=0)
# Pre-process (prepare) the image using the specific architecture:
image = nasnet.preprocess_input(image)
# Return the image:
return image
此函数准备输入图像-将图像转换为 RGB,调整其大小,将一个或多个图像转换为批量格式,最后使用特定的架构对其进行预处理。
最后,我们使用route()
装饰器将predict()
函数绑定到/predict
URL。 predict()
函数处理请求并将预测返回给客户端,如下所示:
@app.route("/predict", methods=["POST"])
def predict():
# Initialize result:
result = {"success": False}
if flask.request.method == "POST":
if flask.request.files.get("image"):
# Read input image in PIL format:
image = flask.request.files["image"].read()
image = Image.open(io.BytesIO(image))
# Pre-process the image to be classified:
image = preprocessing_image(image, target=(224, 224))
# Classify the input image:
with graph.as_default():
predictions = model.predict(image)
results = imagenet_utils.decode_predictions(predictions)
result["predictions"] = []
# Add the predictions to the result:
for (imagenet_id, label, prob) in results[0]:
r = {"label": label, "probability": float(prob)}
result["predictions"].append(r)
# At this point we can say that the request was dispatched successfully:
result["success"] = True
# Return result as a JSON response:
return flask.jsonify(result)
在predict()
函数中处理图像的第一步是读取 PIL 格式的输入图像。 接下来,我们对图像进行预处理,并将其通过网络以获得预测。 最后,我们将预测添加到结果中,并将结果作为 JSON 响应返回。
以与前面各节相同的方式,我们已经编码了两个脚本,以便对 Keras 深度学习 REST API 执行POST
请求。 request_keras_rest_api.py
脚本执行POST
请求并打印结果。 request_keras_rest_api_drawing.py
脚本执行POST
请求,打印结果,并创建图像以渲染获得的结果。 为了简化起见,仅显示request_keras_rest_api_drawing.py
脚本,如下所示:
# Import required packages:
import cv2
import numpy as np
import requests
from matplotlib import pyplot as plt
def show_img_with_matplotlib(color_img, title, pos):
"""Shows an image using matplotlib capabilities"""
img_RGB = color_img[:, :, ::-1]
ax = plt.subplot(1, 1, pos)
plt.imshow(img_RGB)
plt.title(title)
plt.axis('off')
KERAS_REST_API_URL = "http://localhost:5000/predict"
IMAGE_PATH = "car.jpg"
# Load the image and construct the payload:
image = open(IMAGE_PATH, "rb").read()
payload = {"image": image}
# Submit the POST request:
r = requests.post(KERAS_REST_API_URL, files=payload).json()
# Convert the loaded image to the OpenCV format:
image_array = np.asarray(bytearray(image), dtype=np.uint8)
img_opencv = cv2.imdecode(image_array, -1)
img_opencv = cv2.resize(img_opencv, (500, 500))
y_pos = 40
# Show the results:
if r["success"]:
# Iterate over the predictions
for (i, result) in enumerate(r["predictions"]):
# Print the results:
print("{}. {}: {:.4f}".format(i + 1, result["label"], result["probability"]))
# Render the results in the image:
cv2.putText(img_opencv, "{}. {}: {:.4f}".format(i + 1, result["label"], result["probability"]),
(20, y_pos), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 0, 255), 2)
y_pos += 30
else:
print("Request failed")
# Create the dimensions of the figure and set title:
fig = plt.figure(figsize=(8, 6))
plt.suptitle("Using Keras Deep Learning REST API", fontsize=14, fontweight='bold')
fig.patch.set_facecolor('silver')
# Show the output image
show_img_with_matplotlib(img_opencv, "Classification results (NASNetMobile)", 1)
# Show the Figure:
plt.show()
如您所见,我们向 Keras 深度学习 REST API 执行POST
请求。 为了显示结果,我们迭代获得的预测。 对于每个预测,我们都将打印结果并将结果呈现在图像中。
下一个屏幕截图中可以看到此脚本的输出:
在上一个屏幕截图中,您可以看到与输入car.jpg
图像相对应的顶部5
预测。
将 Flask 应用部署到云上
如果您开发了 Flask 应用,则可以在计算机上运行,可以通过将其部署到云中轻松地将其公开。 如果要将应用部署到云中,有很多选择(例如,Google App Engine,Microsoft Azure,Heroku 和 Amazon Web Services 等)。 另外,您还可以使用 PythonAnywhere,它是 Python 在线集成开发环境(IDE)。 和网络托管环境,可轻松在云中创建和运行 Python 程序。
PythonAnywhere 非常简单,也是托管基于机器学习的 Web 应用的推荐方法。 PythonAnywhere 提供了一些有趣的功能,例如基于 WSGI 的网络托管(例如 Django,Flask 和 Web2py)。
在本节中,我们将看到如何创建 Flask 应用以及如何在 PythonAnywhere 上部署它。
为了向您展示如何使用 PythonAnywhere 将 Flask 应用部署到云中,我们将使用mysite
项目的代码。 该代码与本章先前所见的最小面部 API 非常相似(进行了少量修改)。 创建网站后,将对这些修改进行说明:
- 第一步是创建一个 PythonAnywhere 帐户。 对于此示例,一个初学者帐户就足够了:
- 注册后,您将可以访问仪表板。 在下一个屏幕截图中可以看到:
如您所见,我已经创建了用户opencv
。
- 下一步是单击“Web”菜单,然后单击“添加新的 Web 应用”按钮,如以下屏幕截图所示:
- 此时,您可以创建新的 Web 应用,如下面的屏幕快照所示:
- 单击下一步,然后单击 Flask,然后单击最新版本的 Python。 最后,单击“下一步”接受项目路径:
这将创建一个Hello world
Flask 应用,如果您访问这里,则可以看到该应用。 在我的情况下,URL 为https://opencv.pythonanywhere.com
。
- 至此,我们准备上传自己的项目。 第一步是单击 Web 菜单的“代码”部分中的“转到目录”,如下面的屏幕快照所示:
- 我们可以使用“上传文件”按钮将文件上传到我们的网站。 我们上传了三个文件,如下所示:
flask_app.py
face_processing.py
haarcascade_frontalface_alt.xml
在下一个屏幕截图中可以看到:
您可以通过单击下载图标查看这些文件的上载内容。 在这种情况下,您可以在以下 URL 中查看这些文件的内容:
- 下一步是设置虚拟环境。 为此,应通过单击此处的“打开 Bash 控制台”来打开 bash 控制台(请参阅上一个屏幕截图)。 打开后,运行以下命令:
$ mkvirtualenv --python=/usr/bin/python3.6 my-virtualenv
您将看到提示从$
更改为(my-virtualenv)$
。 这意味着虚拟环境已被激活。 此时,我们将安装所有必需的包(flask
和opencv-contrib-python
):
(my-virtualenv)$ pip install flask
(my-virtualenv)$ pip install opencv-contrib-python
您会看到还安装了numpy
。 所有这些步骤可以在下一个屏幕截图中看到:
如果要安装其他包,请不要忘记激活已创建的虚拟环境。 您可以使用以下命令重新激活它:
$ workon my-virtualenv
(my-virtualenv)$
- 至此,我们几乎完成了。 最后一步是通过单击菜单中的 Web 选项并重新加载站点来重新加载上载的项目,这可以在下一个屏幕截图中看到:
因此,我们准备好测试上传到 PythonAnywhere 的 face API,可以使用这个页面访问。 您将看到类似以下屏幕截图的内容:
您可以看到一个 JSON 响应。 之所以获得此 JSON 响应,是因为我们已使用route()
装饰器将info_view()
函数绑定到 URL /
。 与在本章中看到的最小人脸 API 相比,这是我们在本示例中执行的修改之一。 因此,我们修改了flask_app.py
脚本,使其包括:
@app.route('/', methods=["GET"])
def info_view():
# List of routes for this API:
output = {
'info': 'GET /',
'detect faces via POST': 'POST /detect',
'detect faces via GET': 'GET /detect',
}
return jsonify(output), 200
这样,当访问这个页面时,我们将获得此 API 的路由列表。 将项目上传到 PythonAnywhere 以便查看一切正常时,这很有用。 第二次(也是最后一次)修改是在face_processing.py
脚本中执行的。 在此脚本中,我们更改了haarcascade_frontalface_alt.xml
文件的路径,该文件由人脸检测器使用:
class FaceProcessing(object):
def __init__(self):
self.file = "/home/opencv/mysite/haarcascade_frontalface_alt.xml"
self.face_cascade = cv2.CascadeClassifier(self.file)
查看文件的路径,该路径与将haarcascade_frontalface_alt.xml
文件上传到 PythonAnywhere 时分配的新路径匹配。
该路径应根据用户名(在这种情况下为opencv
)进行更改。
与前面的示例相同,我们可以对上传到 PythonAnywhere 的 Face API 执行POST
请求。 这是在demo_request.py
脚本中执行的:
# Import required packages:
import cv2
import numpy as np
import requests
from matplotlib import pyplot as plt
def show_img_with_matplotlib(color_img, title, pos):
"""Shows an image using matplotlib capabilities"""
img_RGB = color_img[:, :, ::-1]
ax = plt.subplot(1, 1, pos)
plt.imshow(img_RGB)
plt.title(title)
plt.axis('off')
FACE_DETECTION_REST_API_URL = "http://opencv.pythonanywhere.com/detect"
IMAGE_PATH = "test_face_processing.jpg"
# Load the image and construct the payload:
image = open(IMAGE_PATH, "rb").read()
payload = {"image": image}
# Submit the POST request:
r = requests.post(FACE_DETECTION_REST_API_URL, files=payload)
# See the response:
print("status code: {}".format(r.status_code))
print("headers: {}".format(r.headers))
print("content: {}".format(r.json()))
# Get JSON data from the response and get 'result':
json_data = r.json()
result = json_data['result']
# Convert the loaded image to the OpenCV format:
image_array = np.asarray(bytearray(image), dtype=np.uint8)
img_opencv = cv2.imdecode(image_array, -1)
# Draw faces in the OpenCV image:
for face in result:
left, top, right, bottom = face['box']
# To draw a rectangle, you need top-left corner and bottom-right corner of rectangle:
cv2.rectangle(img_opencv, (left, top), (right, bottom), (0, 255, 255), 2)
# Draw top-left corner and bottom-right corner (checking):
cv2.circle(img_opencv, (left, top), 5, (0, 0, 255), -1)
cv2.circle(img_opencv, (right, bottom), 5, (255, 0, 0), -1)
# Create the dimensions of the figure and set title:
fig = plt.figure(figsize=(8, 6))
plt.suptitle("Using face API", fontsize=14, fontweight='bold')
fig.patch.set_facecolor('silver')
# Show the output image
show_img_with_matplotlib(img_opencv, "face detection", 1)
# Show the Figure:
plt.show()
除了以下几行之外,此脚本中没有任何新内容:
FACE_DETECTION_REST_API_URL = "http://opencv.pythonanywhere.com/detect"
请注意,我们正在请求我们的云 API。 下一个屏幕截图中可以看到此脚本的输出:
这样,我们可以确认我们的云 API 已启动并正在运行。
总结
在本书的最后一章中,我们了解了如何使用 Python Web 框架创建 Web 应用,并发现了 Flask 等 Web 框架的潜力。 更具体地说,我们已经使用 OpenCV,Keras 和 Flask 开发了多个 Web 计算机视觉和 Web 深度学习应用,并学习了如何与它们结合以提供 Web 应用机器学习和深度学习功能。 此外,我们还介绍了如何使用提供网络托管功能的 PythonAnywhere 将 Flask 应用部署到云中。 最后,我们还了解了如何执行来自浏览器的请求(例如 GET 和 POST)(GET 请求),以及如何以编程方式(GET 和 POST 请求)使用 OpenCV 和 Flask 创建 Web Face API,以及如何使用 OpenCV 创建深度学习 API。
问题
- Web 框架存在两种主要类别?
- Flask 中
route()
装饰器的目的是什么? - 如何运行 Flask 服务器应用以从网络上的任何其他计算机进行访问?
jsonify()
函数的作用是什么?- Flask 中
errorhandler()
装饰器的目的是什么? - 什么是 Keras 应用?
- 什么是 PythonAnywhere?
进一步阅读
以下参考资料将帮助您更深入地研究 Flask(以及 Django):
- 《精通 Flask Web 开发:第二版》,作者 Daniel Gaspar 和 Jack Stouffer
- 《Flask – 构建 Web 应用》
- 《Flask 示例》
- 《Python Web 开发:2018 年的 Django VS Flask》,作者 Aaron Lazar
十四、答案
第一章
- Python 虚拟环境的主要目的是为 Python 项目创建一个隔离的环境。 这意味着每个项目都可以具有自己的依赖关系,而不管每个其他项目都具有什么依赖关系。 换句话说,它是 Python 的一个隔离工作副本,使您可以在特定项目上工作而不必担心影响其他项目。
pip
,virtualenv
,pipenv
,Anaconda 和conda
之间的连接如下:
pip
:Python 包管理器:- PyPA 推荐的用于安装 Python 包的工具
- 您可以使用 PyPI 查找和发布 Python 包:Python 包
索引
pyenv
:Python 版本管理器:- pyenv 让您轻松在多个版本的 Python 之间切换
- 如果您需要使用其他版本的 Python,
pyenv
可让您轻松管理
virtualenv
:Python 环境管理器:virtualenv
是用于创建隔离的 Python 环境的工具- 要创建
virtualenv
,只需调用virtualenv ENV
,其中ENV
是用于放置新虚拟环境的目录 - 要初始化
virtualenv
,您需要获取ENV/bin/activate
- 要停止使用
virtualenv
,只需致电deactivate
- 激活
virtualenv
后,您可以通过运行pip install -r requirements.txt
安装工作区的所有包要求。
anaconda
:包管理器,环境管理器和其他科学库:- Anaconda 包括易于安装的 Python,并更新了 100 多个经过预先构建和测试的科学和分析 Python 包,其中包括 NumPy,Pandas,SciPy,Matplotlib 和 IPython,还可以通过简单的
conda install <packagename>
提供 620 多个包。 conda
是 Anaconda 发行版中包含的开源包管理系统和环境
管理系统(提供虚拟环境功能)。 因此,您可以使用conda
创建虚拟环境。- 尽管
conda
允许您安装包,但是这些包与 PyPI 包是分开的,因此根据您需要安装的包的类型,您可能仍需要额外使用pip
。
- Anaconda 包括易于安装的 Python,并更新了 100 多个经过预先构建和测试的科学和分析 Python 包,其中包括 NumPy,Pandas,SciPy,Matplotlib 和 IPython,还可以通过简单的
-
笔记本文档是 Jupyter 笔记本应用生成的文档,其中包含计算机代码和富文本元素。 由于代码和文本元素的这种混合,笔记本是将分析描述及其结果结合在一起的理想场所。 此外,可以执行它们以实时执行数据分析。 Jupyter 笔记本 App 是一个服务器客户端应用,它允许通过 Web 浏览器编辑和运行笔记本文档。 Jupyter 是一个缩写,代表它设计的三种语言(Julia,Python 和 R)。它属于 Anaconda 发行版。
-
要使用图像,需要的主要包如下:Numpy,opencv,scikit-image,PIL,Pillow,SimpleCV,Mahotas 和 ilastik。 此外,要解决机器学习问题,您还可以使用 Pandas,Scikit-learn,Orange,PyBrain 或 Milk。 最后,如果您的计算机视觉项目涉及深度学习技术,则还可以使用 TensorFlow,Pytorch,Theano 或 Keras。
-
要根据本地目录中的
requirements.txt
文件使用pip
安装包,我们应执行pip install -r requirements.txt
安装此文件中包含的所有包。 您还可以先创建一个虚拟环境,然后安装所有必需的包:
cd
到requirements.txt
所在的目录- 激活您的
virtualenv
- 运行
pip install -r requirements.txt
- 集成开发环境(IDE)是一种软件应用,为计算机程序员提供用于软件开发的全面功能。 IDE 通常包括源代码编辑器,构建自动化工具和调试器。 大多数现代 IDE 具有智能的代码完成功能。 Python IDE 是开始使用 Python 编程的第一件事。 您可以在基本的文本编辑器(如记事本)中开始使用 Python 编程,但是最好使用完整且功能丰富的 Python IDE。
PyCharm 是专业的 Python IDE,有两种形式:
- 专业:用于 Python 和 Web 开发的全功能 IDE(免费试用)
- 社区:用于 Python 和科学开发的轻量级 IDE(免费,开源)
它的大多数功能都以社区形式提供,包括智能代码完成,直观的项目导航,即时错误检查和修复,带有 PEP8 检查和智能重构的代码质量,图调试器和测试运行程序。 它还与 IPython 笔记本集成,并支持 Anaconda 以及其他科学包,例如 Matplotlib 和 NumPy。
- OpenCV 是在 BSD 许可下发布的。 因此,它对于商业和学术用途都是免费的。 BSD 许可证可以分为三种类型:
- 两条款 BSD 许可证
- 三条款 BSD 许可证
- 四条款 BSD 许可证
OpenCV 使用三节 BSD 许可证。 所有这些子句列出如下:
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
(1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
(2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials
provided with the distribution.
(3) Neither the name of the [organization] nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
(4) All advertising materials mentioning features or use of this software must display the following acknowledgement: This product includes software developed by the [organization].
第二章
-
共有三个图像处理步骤:
- 获取必要的信息以供使用(例如,图像或视频文件等)
- 通过应用图像处理技术处理图像
- 以所需的方式显示结果(例如,将图像保存到磁盘,显示图像等等)
-
处理步骤可以分为三个处理级别:
- 低级处理
- 中级处理
- 高级处理
-
灰度图像包含图像的每个像素的值,该值与图像的亮度或灰度级成正比。 此值也称为强度或灰度级。 该值是属于
[0, L-1]
,其中L = 256
(对于 8 位图像)。
另一方面,黑白图像为图像的每个像素包含一个只能取两个值的值。 通常,这些值为 0(黑色)和 255(白色)。 在许多情况下,黑白图像是某些图像处理步骤和技术的结果(例如,阈值运算的结果)。
- 数字图像是 2D 图像表示形式的有限数字值集,称为像素。 像素是数字图像中可编程颜色的基本单位。
图像分辨率可以看作是图像的细节。 分辨率为800×1200
的图像是具有 800 列和 1200 行的网格,包含800×1200 = 960,000
像素。
- OpenCV 执行以下操作:
- 加载(读取)图像:
cv2.imread()
:img = cv2.imread('logo.png')
gray_img = cv2.imread('logo.png', cv2.IMREAD_GRAYSCALE)
- 显示图像:
cv2.imshow()
:cv2.imshow('bgr image', img )
- 等待按键:
cv2.waitKey()
:cv2.waitKey(0)
- 拆分通道:
cv2.split()
:b, g, r = cv2.split(img)
- 合并通道:
cv2.merge()
:img = cv2.merge([r, g, b])
-
$ jupyter notebook
。 -
将获得以下颜色:
- (
B = 0, G = 255, R = 255
):黄色 - (
B = 255, G = 255, R = 0
):青色 - (
B = 255, G = 0, R = 255
):洋红色 - (
B = 255, G = 255, R = 255
):白色
您可以在这个页面上进一步使用 RGB 颜色图表。
- 图像是彩色还是灰度均可通过其尺寸(
img.shape
)确定。 如本章所述,如果加载了彩色图像,则img.shape
的长度将为3
。 另一方面,如果加载的图像是灰度图像,则img.shape
的长度将为2
。 该代码如下所示:
# load OpenCV logo image:
img = cv2.imread('logo.png')
# Get the shape of the image:
dimensions = img.shape
# Check the length of dimensions
if len(dimensions) < 3:
print("grayscale image!")
if len(dimensions) == 3:
print("color image!")
# Load the same image but in grayscale:
gray_img = cv2.imread('logo.png', cv2.IMREAD_GRAYSCALE)
# Get again the img.shape properties:
dimensions = gray_img.shape
# Check the length of dimensions
if len(dimensions) < 3:
print("grayscale image!")
if len(dimensions) == 3:
print("color image!")
第三章
- 列表的第二个元素是脚本的第一个参数,即
sys.argv[1]
。 - 代码如下:
parser = argparse.ArgumentParser()
parser.add_argument("first_number", help="first number to be added", type=int)
- 保存图像的代码如下:
cv2.imwrite("image.png", img)
capture
对象的创建如下:
capture = cv2.VideoCapture(0)
capture
对象的创建如下:
capture = cv2.VideoCapture(0)
print("CV_CAP_PROP_FRAME_WIDTH: '{}'".format(capture.get(cv2.CAP_PROP_FRAME_WIDTH)))
- 读取图像的代码如下:
image = cv2.imread("logo.png")
cv2.imwrite("logo_copy.png", gray_image)
- 脚本编写如下:
"""
Example to introduce how to read a video file backwards and save it
"""
# Import the required packages
import cv2
import argparse
def decode_fourcc(fourcc):
"""Decodes the fourcc value to get the four chars identifying it
"""
fourcc_int = int(fourcc)
# We print the int value of fourcc
print("int value of fourcc: '{}'".format(fourcc_int))
# We can also perform this in one line:
# return "".join([chr((fourcc_int >> 8 * i) & 0xFF) for i in range(4)])
fourcc_decode = ""
for i in range(4):
int_value = fourcc_int >> 8 * i & 0xFF
print("int_value: '{}'".format(int_value))
fourcc_decode += chr(int_value)
return fourcc_decode
# We first create the ArgumentParser object
# The created object 'parser' will have the necessary information
# to parse the command-line arguments into data types.
parser = argparse.ArgumentParser()
# We add 'video_path' argument using add_argument() including a help.
parser.add_argument("video_path", help="path to the video file")
# We add 'output_video_path' argument using add_argument() including a help.
parser.add_argument("output_video_path", help="path to the video file to write")
args = parser.parse_args()
# Create a VideoCapture object and read from input file
# If the input is the camera, pass 0 instead of the video file name
capture = cv2.VideoCapture(args.video_path)
# Get some properties of VideoCapture (frame width, frame height and frames per second (fps)):
frame_width = capture.get(cv2.CAP_PROP_FRAME_WIDTH)
frame_height = capture.get(cv2.CAP_PROP_FRAME_HEIGHT)
fps = capture.get(cv2.CAP_PROP_FPS)
codec = decode_fourcc(capture.get(cv2.CAP_PROP_FOURCC))
print("codec: '{}'".format(codec))
# FourCC is a 4-byte code used to specify the video codec and it is platform dependent!
fourcc = cv2.VideoWriter_fourcc(*codec)
# Create VideoWriter object. We use the same properties as the input camera.
# Last argument is False to write the video in grayscale. True otherwise (write the video in color)
out = cv2.VideoWriter(args.output_video_path, fourcc, int(fps), (int(frame_width), int(frame_height)), True)
# Check if camera opened successfully
if capture.isOpened()is False:
print("Error opening video stream or file")
# We get the index of the last frame of the video file
frame_index = capture.get(cv2.CAP_PROP_FRAME_COUNT) - 1
# print("starting in frame: '{}'".format(frame_index))
# Read until video is completed
while capture.isOpened() and frame_index >= 0:
# We set the current frame position
capture.set(cv2.CAP_PROP_POS_FRAMES, frame_index)
# Capture frame-by-frame from the video file:
ret, frame = capture.read()
if ret is True:
# Print current frame number per iteration
# print("CAP_PROP_POS_FRAMES : '{}'".format(capture.get(cv2.CAP_PROP_POS_FRAMES)))
# Get the timestamp of the current frame in milliseconds
# print("CAP_PROP_POS_MSEC : '{}'".format(capture.get(cv2.CAP_PROP_POS_MSEC)))
# Display the resulting frame
cv2.imshow('Original frame', frame)
# Write the frame to the video
out.write(frame)
# Convert the frame to grayscale:
gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# Display the grayscale frame
cv2.imshow('Grayscale frame', gray_frame)
frame_index = frame_index - 1
# print("next index to read: '{}'".format(frame_index))
# Press q on keyboard to exit the program:
if cv2.waitKey(25) & 0xFF == ord('q'):
break
# Break the loop
else:
break
# Release everything:
capture.release()
out.release()
cv2.destroyAllWindows()
第四章
- 参数厚度可以取正值和负值。 如果该值为正,则表示轮廓的粗细。 负值(例如
-1
)表示将绘制填充形状。 例如,要绘制填充的椭圆,请注意以下几点:
cv2.ellipse(image, (80, 80), (60, 40), 0, 0, 360, colors['red'], -1)
您也可以使用cv2.FILLED
:
cv2.ellipse(image, (80, 80), (60, 40), 0, 0, 360, colors['red'], cv2.FILLED)
lineType
参数可以采用三个值(cv2.LINE_4 == 4
,cv2.LINE_AA == 16
和cv2.LINE_8 == 8
)。 要绘制抗锯齿线,必须使用cv2.LINE_AA
:
cv2.line(image, (0, 0), (20, 20), colors['red'], 1, cv2.LINE_AA)
- 对角线是在以下代码的帮助下创建的:
cv2.line(image, (0, 0), (512, 512), colors['green'], 3)
- 文本展示如下:
cv2.putText(image, 'Hello OpenCV', (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.9, colors['red'], 2, cv2.LINE_4)
- 此练习的代码对应于
circle_polygon.py
脚本。 要获取坐标,可以使用圆的参数方程式(请参见analog_clock_values.py
)。 下图显示了此多边形:
circle_polygon.py
文件的代码如下:
"""
Example to show how to draw a circle polygon
"""
# Import required packages:
import cv2
import numpy as np
import matplotlib.pyplot as plt
def show_with_matplotlib(img, title):
"""Shows an image using matplotlib capabilities
"""
# Convert BGR image to RGB
img_RGB = img[:, :, ::-1]
# Show the image using matplotlib:
plt.imshow(img_RGB)
plt.title(title)
plt.show()
# Dictionary containing some colors
colors = {'blue': (255, 0, 0), 'green': (0, 255, 0), 'red': (0, 0, 255), 'yellow': (0, 255, 255),
'magenta': (255, 0, 255), 'cyan': (255, 255, 0), 'white': (255, 255, 255), 'black': (0, 0, 0),
'gray': (125, 125, 125), 'rand': np.random.randint(0, high=256, size=(3,)).tolist(),
'dark_gray': (50, 50, 50), 'light_gray': (220, 220, 220)}
# We create the canvas to draw: 640 x 640 pixels, 3 channels, uint8 (8-bit unsigned integers)
# We set background to black using np.zeros()
image = np.zeros((640, 640, 3), dtype="uint8")
# If you want another background color you can do the following:
# image[:] = colors['light_gray']
image.fill(255)
pts = np.array(
[(600, 320), (563, 460), (460, 562), (320, 600), (180, 563), (78, 460), (40, 320), (77, 180), (179, 78), (319, 40),
(459, 77), (562, 179)])
# Reshape to shape (number_vertex, 1, 2)
pts = pts.reshape((-1, 1, 2))
# Call cv2.polylines() to build the polygon:
cv2.polylines(image, [pts], True, colors['green'], 5)
# Show image:
show_with_matplotlib(image, 'polygon with the shape of a circle using 12 points')
- 该代码对应于
matplotlib_mouse_events_rect.py
脚本。
关键是如何捕获双击鼠标左键:
- 双击:
event.dblclick
- 左键点击:
event.button == 1
matplotlib_mouse_events_rect.py
文件的代码如下:
"""
Example to show how to capture a double left click with matplotlib events to draw a rectangle
"""
# Import required packages:
import cv2
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
# Dictionary containing some colors
colors = {'blue': (255, 0, 0), 'green': (0, 255, 0), 'red': (0, 0, 255), 'yellow': (0, 255, 255),
'magenta': (255, 0, 255), 'cyan': (255, 255, 0), 'white': (255, 255, 255), 'black': (0, 0, 0),
'gray': (125, 125, 125), 'rand': np.random.randint(0, high=256, size=(3,)).tolist(),
'dark_gray': (50, 50, 50), 'light_gray': (220, 220, 220)}
# We create the canvas to draw: 400 x 400 pixels, 3 channels, uint8 (8-bit unsigned integers)
# We set the background to black using np.zeros()
image = np.zeros((400, 400, 3), dtype="uint8")
# If you want another background color you can do the following:
image[:] = colors['light_gray']
def update_img_with_matplotlib():
"""Updates an image using matplotlib capabilities
"""
# Convert BGR to RGB image format
img_RGB = image[:, :, ::-1]
# Display the image:
plt.imshow(img_RGB)
# Redraw the Figure because the image has been updated:
figure.canvas.draw()
# We define the event listener for the 'button_press_event':
def click_mouse_event(event):
# Check if a double left click is performed:
if event.dblclick and event.button == 1:
# (event.xdata, event.ydata) contains the float coordinates of the mouse click event:
cv2.rectangle(image, (int(round(event.xdata)), int(round(event.ydata))),
(int(round(event.xdata)) + 100, int(round(event.ydata)) + 50), colors['blue'], cv2.FILLED)
# Call 'update_image()' method to update the Figure:
update_img_with_matplotlib()
# We create the Figure:
figure = plt.figure()
figure.add_subplot(111)
# To show the image until a click is performed:
update_img_with_matplotlib()
# 'button_press_event' is a MouseEvent where a mouse botton is click (pressed)
# When this event happens the function 'click_mouse_event' is called:
figure.canvas.mpl_connect('button_press_event', click_mouse_event)
# Display the figure:
plt.show()
- 该代码对应于
meme_generator_opencv_python.py
脚本。 这是一个简单的脚本,在其中加载图像,然后渲染一些文本:
"""
Example to show how to draw basic memes with OpenCV
"""
# Import required packages:
import cv2
import numpy as np
import matplotlib.pyplot as plt
def show_with_matplotlib(img, title):
"""Shows an image using matplotlib capabilities
"""
# Convert BGR image to RGB
img_RGB = img[:, :, ::-1]
# Show the image using matplotlib:
plt.imshow(img_RGB)
plt.title(title)
plt.show()
# Dictionary containing some colors
colors = {'blue': (255, 0, 0), 'green': (0, 255, 0), 'red': (0, 0, 255), 'yellow': (0, 255, 255),
'magenta': (255, 0, 255), 'cyan': (255, 255, 0), 'white': (255, 255, 255), 'black': (0, 0, 0),
'gray': (125, 125, 125), 'rand': np.random.randint(0, high=256, size=(3,)).tolist(),
'dark_gray': (50, 50, 50), 'light_gray': (220, 220, 220)}
# We load the image 'lenna.png':
image = cv2.imread("lenna.png")
# Write some text (up)
cv2.putText(image, 'Hello World', (10, 30), cv2.FONT_HERSHEY_TRIPLEX, 0.8, colors['green'], 1, cv2.LINE_AA)
# Write some text (down)
cv2.putText(image, 'Goodbye World', (10, 200), cv2.FONT_HERSHEY_TRIPLEX, 0.8, colors['red'], 1, cv2.LINE_AA)
# Show image:
show_with_matplotlib(image, 'very basic meme generator')
第五章
cv2.split()
函数将源多通道图像分割为几个单通道图像
(b, g, r) = cv2.split(image)
。cv2.merge()
函数将几个单通道图像合并为一个多通道图像image = cv2.merge((b, g, r))
。- 图像可以平移如下:
height, width = image.shape[:2]
M = np.float32([[1, 0, 150], [0, 1, 300]])
dst_image = cv2.warpAffine(image, M, (width, height))
- 可以按以下方式旋转图像:
height, width = image.shape[:2]
M = cv2.getRotationMatrix2D((width / 2.0, height / 2.0), 30, 1)
dst_image = cv2.warpAffine(image, M, (width, height))
- 该图像可以如下构建:
kernel = np.ones((5, 5), np.float32) / 25
smooth_image = cv2.filter2D(image, -1, kernel)
- 灰度图像如下:
M = np.ones(image.shape, dtype="uint8") * 40
added_image = cv2.add(image, M)
COLORMAP_JET
可以如下应用:img_COLORMAP_JET = cv2.applyColorMap(gray_img, cv2.COLORMAP_JET)
第六章
- 图像直方图是一种反映图像色调分布的直方图。 它绘制每个色调值的频率(像素数)(通常在[
0-255
]范围内)。 - 在 OpenCV 中,我们使用
cv2.calcHist()
函数来计算图像的直方图。 要使用64
位计算灰度图像的直方图,代码如下:
hist = cv2.calcHist([gray_image], [0], None, [64], [0, 256])
- 我们首先构建具有与灰度图像
gray_image
相同形状的图像M
,然后为该图像的每个像素设置50
值。 然后,我们使用cv2.add()
添加两个图像。 最后,使用cv2.calcHist()
计算直方图:
M = np.ones(gray_image.shape, dtype="uint8") * 50
added_image = cv2.add(gray_image, M)
hist_added_image = cv2.calcHist([added_image], [0], None, [256], [0, 256])
- 在 BGR 图像中,红色通道是第三通道(
index 2
):
cv2.calcHist([img], [2], None, [256], [0, 256])
- OpenCV 提供
cv2.calcHist()
,numpy 提供np.histogram()
,而 matplotlib 提供plt.hist()
。 如本章所述,cv2.calcHist()
比np.histogram()
和plt.hist()
都快。 - 我们定义了一个函数
get_brightness()
,该函数计算给定灰度图像的亮度。 此函数使用 numpy 函数np.mean()
,该函数返回数组元素的平均值。 因此,此函数的代码如下:
def get_brightness(img):
"""Calculates the brightness of the image"""
brightness = np.mean(img)
return brightness
我们已经计算了三个图像的亮度:
brightness_1 = get_brightness(gray_image)
brightness_2 = get_brightness(added_image)
brightness_3 = get_brightness(subtracted_image)
该示例的完整代码可以在grayscale_histogram_brightness.py
脚本中看到。
- 首先,我们必须导入
default_timer
:
from timeit import default_timer as timer
然后,我们必须测量两个函数的执行时间:
start = timer()
gray_image_eq = cv2.equalizeHist(gray_image)
end = timer()
exec_time_equalizeHist = (end - start) * 1000
start = timer()
gray_image_clahe = clahe.apply(gray_image)
end = timer()
exec_time_CLAHE = (end - start) * 1000
该示例的完整代码可以在comparing_hist_equalization_clahe_time.py.
脚本中看到。
第七章
ret, thresh = cv2.threshold(gray_image, 100, 255, cv2.THRESH_BINARY)
thresh = cv2.adaptiveThreshold(gray_image, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 9, 2)
ret, th = cv2.threshold(gray_image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
ret, th = cv2.threshold(gray_image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_TRIANGLE)
- 使用 scikit-image 的大津阈值可应用如下:
thresh = threshold_otsu(gray_image)
binary = gray_image > thresh
binary = img_as_ubyte(binary)
请记住,threshold_otsu(gray_image)
函数基于大津的二值化算法返回阈值。 然后,使用此值构造二进制图像(dtype= bool
),应将其转换为 8 位无符号整数格式(dtype= uint8
)以进行适当的可视化。 img_as_ubyte()
函数用于此目的。
- 可以使用 scikit-image 进行三角形阈值处理:
thresh_triangle = threshold_triangle(gray_image)
binary_triangle = gray_image > thresh_triangle
binary_triangle = img_as_ubyte(binary_triangle)
- Niblack 使用 scikit-image 的阈值可按以下方式应用:
thresh_niblack = threshold_niblack(gray_image, window_size=25, k=0.8)
binary_niblack = gray_image > thresh_niblack
binary_niblack = img_as_ubyte(binary_niblack)
该算法最初是为文本识别而设计的。 有关更多详细信息,请参见出版物《数字图像处理入门》(1986)。
- 使用 Scikit-image 和
25
窗口大小的 Sauvola 阈值可按以下方式应用:
thresh_sauvola = threshold_sauvola(gray_image, window_size=25)
binary_sauvola = gray_image > thresh_sauvola
binary_sauvola = img_as_ubyte(binary_sauvola)
值得注意两个要点:
- Sauvola 是 Niblack 技术的改良版
- 该算法最初是为文本识别而设计的
有关更多详细信息,请参见出版物《自适应文档图像二值化》(2000)。
- 为了获得带有阈值图像的值的数组,我们使用
np.arange()
。 由于我们需要一个带有10
步骤的[60-130]
范围内的值的数组,因此以下行对此函数进行了编码:
threshold_values = np.arange(start=60, stop=140, step=10)
之后,我们反复应用cv2.threshold()
函数和threshold_values
中定义的相应阈值:
thresholded_images = []
for threshold in threshold_values:
ret, thresh = cv2.threshold(gray_image, threshold, 255, cv2.THRESH_BINARY)
thresholded_images.append(thresh)
最后,我们显示thresholded_images
数组中包含的阈值图像。 完整的代码可以在thresholding_example_arange.py
脚本中看到。
第八章
cv2.findContours()
函数在二进制图像(例如,阈值运算后得到的图像)中找到轮廓。- OpenCV 提供的用于压缩轮廓的四个标志如下:
cv2.CHAIN_APPROX_NONE
cv2.CHAIN_APPROX_SIMPLE
cv2.CHAIN_APPROX_TC89_KCOS
cv2.CHAIN_APPROX_TC89_L1
cv2.moments()
函数计算直到多边形或栅格化形状的三阶的所有矩。- 矩
m00
给出轮廓的面积。 - OpenCV 提供
cv2.HuMoments()
函数来计算七个胡矩不变量。 cv2.approxPolyDP()
函数根据给定的精度返回给定轮廓的轮廓近似值。 此函数使用 Douglas-Peucker 算法。epsilon
参数指定用于在原始曲线与其近似之间建立最大距离的精度。- 可以以更紧凑的方式覆盖
contour_functionality.py
脚本中定义的extreme_points()
函数,如下所示:
def extreme_points_2(contour):
"""Returns extreme points of the contour"""
extreme_left = tuple(contour[contour[:, :, 0].argmin()][0])
extreme_right = tuple(contour[contour[:, :, 0].argmax()][0])
extreme_top = tuple(contour[contour[:, :, 1].argmin()][0])
extreme_bottom = tuple(contour[contour[:, :, 1].argmax()][0])
return extreme_left, extreme_right, extreme_top, extreme_bottom
- OpenCV 提供
cv2.matchShapes()
函数,可使用三种比较方法来比较两个轮廓。 所有这些方法都使用胡矩不变量。 三种实现的方法是cv2.CONTOURS_MATCH_I1
,cv2.CONTOURS_MATCH_I2
和cv.CONTOURS_MATCH_I3
。
第九章
- 带有 ORB 的已加载图像
image
中的关键点和计算描述符如下:
orb = cv2.ORB()
keypoints = orb.detect(image, None)
keypoints, descriptors = orb.compute(image, keypoints)
- 先前检测到的关键点
keypoints
如下:
image_keypoints = cv2.drawKeypoints(image, keypoints, None, color=(255, 0, 255), flags=0)
要绘制检测到的关键点,请使用cv2.drawKeypoints()
函数。
BFMatcher
对象和先前已计算的描述符,描述符_1
和描述符_2
的匹配如下创建:
bf_matcher = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
bf_matches = bf_matcher.match(descriptors_1, descriptors_2)
- 之前已进行匹配的前 20 个匹配如下:
bf_matches = sorted(bf_matches, key=lambda x: x.distance)
result = cv2.drawMatches(image_query, keypoints_1, image_scene, keypoints_2, bf_matches[:20], None, matchColor=(255, 255, 0), singlePointColor=(255, 0, 255), flags=0)
要绘制计算出的匹配项,请使用cv2.drawMatches()
。
- 在图像
gray_frame
中使用 ArUco 进行标记的检测如下:
corners, ids, rejected_corners = cv2.aruco.detectMarkers(gray_frame, aruco_dictionary, parameters=parameters)
要检测标记,请使用cv2.aruco.detectMarkers()
函数。
- 使用 ArUco 时检测到的标记如下:
frame = cv2.aruco.drawDetectedMarkers(image=frame, corners=corners, ids=ids, borderColor=(0, 255, 0))
要绘制标记,请使用cv2.aruco.drawDetectedMarkers()
函数。
- 使用 Aruco 时被拒绝的标记如下:
frame = cv2.aruco.drawDetectedMarkers(image=frame, corners=rejected_corners, borderColor=(0, 0, 255))
要绘制标记,还可以使用cv2.aruco.drawDetectedMarkers()
函数。
- 使用以下代码检测并解码图像中包含的 QR 代码:
data, bbox, rectified_qr_code = qr_code_detector.detectAndDecode(image)
第十章
- 在机器学习的上下文中,主要有三种方法和技术:有监督,无监督和半监督机器学习。
- 监督学习问题可以进一步分为回归和分类问题。 当输出变量为类别时,将发生分类问题;而当输出变量为实值时,将出现回归问题。 例如,如果我们预测某些地区会下雨的可能性,并分配两个标签(降雨/不下雨),这就是分类问题。 另一方面,如果我们模型的输出是与降雨相关的概率,则这是一个回归问题。
- OpenCV 提供
cv2.kmeans()
函数,实现了 K 均值聚类算法,该算法查找聚类的中心并对聚类周围的输入样本进行分组。 K 均值是可用于无监督学习的最重要的聚类算法之一。 cv2.ml.KNearest_create()
方法创建一个空的 KNN 分类器,应使用train()
方法对其进行训练,同时提供数据和标签。cv2.findNearest()
方法用于查找邻居。- 要创建空模型,请使用
cv2.ml.SVM_create()
函数。 - 通常,RBF 核是合理的首选。 RBF 核将样本非线性地映射到更高维度的空间中,因此,与线性核不同,RBF 核可以处理类标签和属性之间的关系为非线性的情况。 有关更多详细信息,请参见《支持向量分类的实用指南》(2003)。
第十一章
- 我们已经看到了四个库和包:OpenCV 库,还有
dlib
主页和 PyPIdlib
主页,Githubdlib
主页,PyPIface_recognition
主页,Githubface_recognition
主页和PyPIcvlib
主页,Githubcvlib
主页,cvlib
主页 Python 包。 - 人脸识别是对象识别的一种特殊情况,其中可以使用从人脸提取的信息从图像或视频中识别或验证人的身份,可以分解为人脸识别和人脸验证:
- 人脸验证是一个 1:1 匹配的问题,试图回答以下问题:“这是所要求的人吗?” 例如,使用面部解锁手机就是面部验证的一个示例。
- 人脸识别是一个 1:N 匹配问题,试图回答以下问题:“这个人是谁?” 例如,可以在办公大楼中安装面部识别系统,以识别所有雇员进入办公室时的身份。
cv2.face.getFacesHAAR()
函数可用于检测图像中的人脸:
retval, faces = cv2.face.getFacesHAAR(img, "haarcascade_frontalface_alt2.xml")
此处,img
是 BGR 图像,"haarcascade_frontalface_alt2.xml"
是 Haar 级联文件的字符串变量。
cv2.dnn.blobFromImage()
函数用于根据输入图像创建一个四维 BLOB。 可选地,此函数执行预处理,这是将 BLOB 输入网络以获得正确结果所必需的。 下一章将更深入地介绍此函数。cv.detect_face()
函数可用于使用cvlib
检测面部:
import cvlib as cv
faces, confidences = cv.detect_face(image)
在后台,此函数将 OpenCV DNN 面部检测器与经过预训练的 Caffe 模型一起使用。 有关更多详细信息,请参见face_detection_cvlib_dnn.py
脚本。
- 要检测地标,应调用
face_recognition.face_landmarks()
函数:
face_landmarks_list_68 = face_recognition.face_landmarks(rgb)
此函数为图像中的每个脸部返回人脸标志(例如,眼睛,鼻子等)的字典。 应当注意,face_recognition
包适用于 RGB 图像。
- 要初始化相关跟踪器,应调用
dlib.correlation_tracker()
函数:
tracker = dlib.correlation_tracker()
这将使用默认值初始化跟踪器。
- 要开始跟踪,请使用
tracker.start_track()
方法,并且需要包含要跟踪的对象的边界框:
tracker.start_track(image, rect)
在此,rect
是要跟踪的对象的边界框。
- 要获取被跟踪对象的位置,请调用
tracker.get_position()
方法:
pos = tracker.get_position()
此方法返回被跟踪对象的位置。
- 使用 Dlib 执行 BGR 图像
image
的人脸识别的 128D 描述符的计算如下:
# Convert image from BGR (OpenCV format) to RGB (dlib format):
rgb = image[:, :, ::-1]
# Calculate the encodings for every face of the image:
encodings = face_encodings(rgb)
# Show the first encoding:
print(encodings[0])
第十二章
- 机器学习和深度学习之间的三个主要区别如下:
- 深度学习算法需要具有高端基础架构才能正确训练。 深度学习技术严重依赖高端机器,这与可以在低端机器上运行的传统机器学习技术相反。
- 当对特征自省和工程都缺乏领域的了解时,深度学习技术会胜过其他技术,因为您不必担心特征工程。
- 机器学习和深度学习都能够处理海量数据集,但是在处理小型数据集时,机器学习方法更有意义。 经验法则是,如果数据量很大,则深度学习要胜过其他技术,而当数据集较小时,传统的机器学习算法更可取。
-
关于计算机视觉,Alex Krizhevsky,Ilya Sutskever 和 Geoff Hinton 发表了《使用深度卷积神经网络的 ImageNet 分类》(2012)。 该出版物也称为 AlexNet ,这是作者设计的卷积神经网络的名称,被认为是计算机视觉中最具影响力的论文之一。 因此,2012 年被认为是深度学习的爆炸式增长。
-
此函数从图像创建一个四维 BLOB,这意味着我们要在调整大小为
300x300
的BGR
图像上运行模型,并对每个蓝色,绿色和红色通道应用均值(104, 117, 123)
的均值减法 , 分别。 -
第一行将输入的 BLOB 馈送到网络,而第二行执行推理,当推理完成时,我们得到了预测。
-
占位符只是一个变量,稍后我们将为其分配数据。 在训练/测试算法时,通常使用占位符将训练/测试数据输入到计算图中。
-
保存最终模型(例如
saver.save(sess, './linear_regression')
)时,将创建四个文件:
.meta
文件:包含 TensorFlow 图.data
文件:包含权重,偏差,梯度以及所有其他已保存变量的值.index
文件:确定检查点checkpoint
文件:记录保存的最新检查点文件
- 单热编码意味着标签已从单个数字转换为长度等于可能的类数的向量。 这样,除
i
元素(其值将为 1)之外,向量的所有元素都将设置为零,对应于类别i
。 - 使用 Keras 时,最简单的模型类型是顺序模型,可以将其视为线性的层堆叠,并在本示例中用于创建模型。 此外,对于更复杂的架构,可以使用 Keras 函数式 API,该 API 允许您构建任意的层图。
- 此方法可用于训练模型以获取固定数量的周期(数据集上的迭代)。
第十三章
- Web 框架可以分为全栈和非全栈框架。 Django 是用于 Python 的全栈 Web 框架,而 Flask 是 Python 的非全栈框架。
- 在 Flask 中,您可以使用
route()
装饰器将函数绑定到 URL。 换句话说,route()
装饰器用于向 Flask 指示应触发特定函数的 URL。 - 为了使服务器可以公开,在运行服务器应用时应添加
host=0.0.0.0
参数:
if __name__ == "__main__":
# Add parameter host='0.0.0.0' to run on your machines IP address:
app.run(host='0.0.0.0')
jsonify()
函数用于创建具有application/json
mimetype 的给定参数的 JSON 表示形式。 JSON 被认为是信息交换的事实上的标准,因此,将 JSON 数据返回给客户端是一个好主意。- 我们可以通过用
errorhandler()
装饰函数来注册错误处理器。 例如,请注意以下几点:
@app.errorhandler(500)
def not_found(e):
# return also the code error
return jsonify({"status": "internal error", "message": "internal error occurred in server"}), 500
如果服务器中发生内部错误,将为客户端提供500
错误代码。
- Keras 应用 是 Keras 深度学习库的应用模块,它为许多流行的架构(例如 VGG16,ResNet50,Xception 和 MobileNet 等),可用于预测,特征提取和微调。 这些深度学习架构在 ImageNet 数据集上进行了训练和验证,用于将图像分类为
1000
类别或类别之一。 - PythonAnywhere 是 Python 在线集成开发环境(IDE)和 Web 托管环境,可轻松在云中创建和运行 Python 程序。
第 1 部分:OpenCV 4 和 Python 简介
在本书的第一部分中,将向您介绍 OpenCV 库。 您将学习如何安装开始使用 Python 和 OpenCV 进行编程所需的一切。 另外,您还将熟悉通用的术语和概念,以根据您所学的内容进行语境化,并为掌握本书的主要概念奠定基础。 此外,您将开始编写第一个脚本以掌握 OpenCV 库,并且还将学习如何处理文件和图像,这是构建计算机视觉应用所必需的。 最后,您将看到如何使用 OpenCV 库绘制基本和高级形状。
本节将介绍以下章节:
- 第 1 章,“设置 OpenCV”
- 第 2 章,“OpenCV 中的图像基础”
- 第 3 章,“处理文件和图像”
- 第 4 章,“在 OpenCV 中构建基本形状”
第 2 部分:OpenCV 中的图像处理
在本书的第二部分中,您将更深入地了解 OpenCV 库。 更具体地说,您将看到计算机视觉项目中所需的大多数常见图像处理技术。 此外,您还将看到如何创建和理解直方图,直方图是用于更好地理解图像内容的强大工具。 此外,您将在计算机视觉应用中看到所需的主要阈值处理技术,这是图像分割的关键部分。 此外,您还将看到如何处理轮廓,轮廓用于形状分析以及对象检测和识别。 最后,您将学习如何构建第一个增强现实应用。
本节将介绍以下章节:
- 第 5 章,“图像处理技术”
- 第 6 章,“直方图的构建”
- 第 7 章,“阈值处理技术”
- 第 8 章,“轮廓检测,过滤和绘制”
- 第 9 章,“增强现实”
第 3 部分:OpenCV 中的机器学习和深度学习
在本书的第三部分中,您将体验一下机器学习和深度学习。 我们将探索和利用 OpenCV 的机器学习模块。 此外,您还将学习如何使用与人脸检测,跟踪和识别相关的最新算法来创建人脸处理项目。 最后,将向您介绍 OpenCV 和一些深度学习 Python 库(TensorFlow 和 Keras)的深度学习领域。
本节将介绍以下章节:
-
第 10 章,“使用 OpenCV 的机器学习”
-
第 11 章,“人脸检测,跟踪和识别”
-
第 12 章,“深度学习简介”
第 4 部分:移动和 Web 计算机视觉
在本书的最后一部分中,您将学习如何使用 Flask(这是 BSD 许可下的一个功能强大的小型 Python Web 框架)来创建计算机视觉和深度学习 Web 应用,以构建计算机视觉和深度学习 Web 应用 。 此外,您还将学习如何将 Flask 应用部署到云中。
本章将介绍以下章节:
- 第 13 章,“使用 Python 和 OpenCV 的移动和 Web 计算机视觉”