ANN-计算机视觉应用构建指南-一-

105 阅读1小时+

ANN 计算机视觉应用构建指南(一)

原文:Building Computer Vision Applications Using Artificial Neural Networks

协议:CC BY-NC-SA 4.0

一、先决条件和软件安装

这是一本描述如何用 Python 编程语言开发计算机视觉应用的实践书籍。在本书中,您将学习如何使用 OpenCV 操作图像,并使用 TensorFlow 构建机器学习模型。

OpenCV 最初由英特尔开发,用 C++编写,是一个开源的计算机视觉和机器学习库,由 2500 多种用于处理图像和视频的优化算法组成。TensorFlow 是一个用于高性能数值计算和大规模机器学习的开源框架。它是用 C++编写的,提供了对 GPU 的原生支持。Python 是开发机器学习应用最广泛的编程语言。它被设计成与 C++一起工作。TensorFlow 和 OpenCV 都提供 Python 接口来访问它们的底层功能。虽然 TensorFlow 和 OpenCV 提供了其他编程语言的接口,如 Java、C++和 MATLAB,但我们将使用 Python 作为主要语言,因为它很简单,而且有很大的支持社区。

这本书的先决条件是 Python 的实用知识和对 NumPy 和 Pandas 的熟悉。本书假设您熟悉 Python 中的内置数据容器,如字典、列表、集合和元组。以下是一些可能有助于满足先决条件的资源:

在我们继续之前,让我们准备好我们的工作环境,并为我们将要进行的练习做好准备。在这里,我们将从下载和安装所需的软件库和软件包开始。

Python 和 PIP

Python 是我们主要的编程语言。PIP 是 Python 的包安装程序,也是安装和管理 Python 包的事实上的标准。为了设置我们的工作环境,我们将从在我们的工作计算机上安装 Python 和 PIP 开始。安装步骤取决于您使用的操作系统(OS)。确保您按照操作系统的说明进行操作。如果您已经安装了 Python 和 PIP,请确保您使用的是 Python 版本 3.6 或更高版本以及 PIP 版本 19 或更高版本。要检查 Python 的版本号,请在您的终端上执行以下命令:

$ python3 --version

这个命令的输出应该是这样的:Python 3.6.5。

要检查 PIP 的版本号,请在您的终端上执行以下命令:

$ pip3 --version

该命令应该显示 PIP 3 的版本号,例如 PIP 19.1。

在 Ubuntu 上安装 Python 和 PIP

在 Ubuntu 终端中运行以下命令:

sudo apt update
sudo apt install python3-dev python3-pip

在 macOS 上安装 Python 和 PIP

在 macOS 上运行以下命令:

brew update
brew install python

这将同时安装 Python 和 PIP。

在 CentOS 7 上安装 Python 和 PIP

在 CentOS 7 上运行以下命令:

sudo yum install rh-python36
sudo yum groupinstall 'Development Tools'

在 Windows 上安装 Python 和 PIP

安装 Microsoft Visual C++ 2015 可再发行更新 3。这是 Visual Studio 2015 附带的,但可以通过以下步骤单独安装:

  1. 前往 https://visualstudio.microsoft.com/vs/older-downloads/ 的 Visual Studio 下载。

  2. 选择可再发行软件和构建工具。

  3. 下载并安装 Microsoft Visual C++ 2015 可再发行更新 3。

确保在 Windows 上启用了长路径。下面是这样做的说明: https://superuser.com/questions/1119883/windows-10-enable-ntfs-long-paths-policy-option-missing

https://www.python.org/downloads/windows/ 安装用于 Windows 的 64 位 Python 3 版本(选择 PIP 作为可选功能)。

如果这些安装说明在您的环境中不起作用,请参考位于 https://www.python.org/ 的 Python 官方文档。

virtualenv(虚拟环境)

virtualenv 是一个创建独立 Python 环境的工具。virtualenv 创建一个目录,其中包含使用 Python 项目所需的包所需的所有可执行文件。virtualenv 提供了以下优势:

  • virtualenv 允许您拥有同一个库的两个版本,这样您的两个程序都可以继续运行。假设你有一个程序需要 Python 库的版本 1,而另一个程序需要同一个库的版本 2;virtualenv 将允许您同时运行这两种功能。

  • virtualenv 为您的开发工作创建了一个有用的独立和自包含的环境,可以在生产环境中使用,而无需安装依赖项。

接下来,我们将安装 virtualenv,并使用所有必需的软件配置环境。对于本书的其余部分,我们将假设我们的引用程序依赖项将包含在这个 virtualenv 中。

使用以下 PIP 命令安装 virtualenv(该命令在所有操作系统上都是相同的):

$ sudo pip3 install -U virtualenv

这将在系统范围内安装 virtualenv。

安装和激活 virtualenv

首先,创建一个要安装 virtualenv 的目录。我已经把这个目录命名为cv(“计算机视觉”的简称)。

$ mkdir cv
Then create the virtualenv in this directory, cv
$ virtualenv --system-site-packages -p python3 ./cv

以下是运行此命令的输出示例(在我的 MacBook 上):

Running virtualenv with interpreter /anaconda3/bin/python3
Already using interpreter /anaconda3/bin/python3
Using base prefix '/anaconda3'
New python executable in /Users/sansari/cv/bin/python3
Also creating executable in /Users/sansari/cv/bin/python
Installing setuptools, pip, wheel...
done.

使用特定于 shell 的命令激活虚拟环境。

$ source ./cv/bin/activate  # for sh, bash, ksh, or zsh

当 virtualenv 处于活动状态时,您的 shell 提示符会带有前缀(cv)。这里有一个例子:

(cv) Shamshads-MacBook-Air:~ sansari$

在虚拟环境中安装软件包,而不影响主机系统设置。从升级 PIP 开始(确保在 virtualenv 中不要以 root 或 sudo 身份运行任何命令)。

$ pip install --upgrade pip

$ pip list  # show packages installed within the virtual environment

完成后,如果您想退出 virtualenv,请执行以下操作:

$ deactivate  # don't exit until you're done with your programming

TensorFlow

TensorFlow 是一个用于数值计算和大规模机器学习的开源库。您将在后续章节中了解更多关于 TensorFlow 的内容。让我们首先安装它,并为我们的深度学习练习做好准备。

安装 TensorFlow

我们将安装 PyPI ( https://pypi.org/project/tensorflow/ )最新版本的 TensorFlow。我们将为 CPU 安装 TensorFlow。确保您处于 virtualenv 中,并运行以下命令:

(cv) $ pip install --upgrade tensorflow

通过运行以下命令测试 TensorFlow 安装:

(cv) $ python -c "import tensorflow as tf"

如果 TensorFlow 安装成功,输出应该不会显示任何错误。

我喜欢这里

您可以使用您最喜欢的 IDE 来编写和管理 Python 代码,但是出于本书的目的,我们将使用 PyCharm 的社区版本,这是一个 Python IDE。

安装 PyCharm

进入 PyCharm 官网 https://www.jetbrains.com/pycharm/download/#section=linux ,选择合适的操作系统,点击下载(社区版下)。下载完成后,单击下载的软件包,并按照屏幕上的说明进行操作。以下是不同操作系统的直接链接:

配置 PyCharm 以使用 virtualenv

按照以下步骤使用我们之前创建的 virtualenv,cv:

img/493065_1_En_1_Fig1_HTML.jpg

图 1-1

选择口译员

  1. 启动 PyCharm IDE,为 Windows 和 Linux 选择文件➤设置,或者为 macOS 选择 PyCharm ➤首选项。

  2. 在设置/首选项对话框中,选择项目 ➤项目解释器。

  3. 点击img/493065_1_En_1_Figa_HTML.gif图标,然后点击添加。

  4. 在添加 Python 解释器对话框的左侧窗格中,选择现有环境。

  5. 展开解释器列表并选择任何现有的解释器。或者,点击img/493065_1_En_1_Figb_HTML.gif并在您的文件系统中指定 Python 可执行文件的路径,例如/Users/sansari/cv/bin/python3.6(参见图 1-1 )。

  6. 如果您愿意,请选择“对所有项目可用”复选框。

开放计算机视觉

OpenCV 是最流行和广泛使用的图像处理库之一。本书中的所有代码示例都基于 OpenCV 4。因此,我们的安装步骤是针对 OpenCV 版本 4 的。

使用 OpenCV

OpenCV 是用 C/C++编写的,因为它依赖于平台,所以不同的操作系统有不同的安装说明。换句话说,OpenCV 需要为您的特定平台/操作系统构建,以便平稳运行。我们将使用 Python 绑定来调用 OpenCV 以满足任何图像处理需求。

像任何其他库一样,OpenCV 也在发展;因此,如果以下安装说明在您的情况下不起作用,请查看官方网站了解确切的安装过程。

我们将采用一种简单的方法,使用 PIP 安装 OpenCV 4 和 Python 3 绑定。我们将在之前创建的虚拟环境中安装来自 PyPI 的opencv-python-contrib包。

所以我们开始吧!

使用 Python 绑定安装 OpenCV4

确保你在你的虚拟环境中。只需将目录切换到您的 virtualenv 目录(我们之前创建的cv目录)并键入以下命令:

$ source cv/bin/activate

使用以下命令快速安装 OpenCV:

$ pip install opencv-contrib-python

附加库

在我们研究一些例子时,还需要一些额外的库。让我们安装并保存它们。

安装 SciPy

使用以下内容安装 SciPy:

$ pip install scipy

安装 Matplotlib

使用以下内容安装 Matplotlib:

$ pip install matplotlib

请注意,本章中安装的库经常更新。强烈建议查看官方网站的更新、这些库的新版本以及最新的安装说明。

二、图像和视频处理的核心概念

本章介绍了图像的构造块,并描述了操作它们的各种方法。本章中我们的学习目标如下:

  • 了解图像的最小单位(像素)以及颜色是如何表现的

  • 了解图像中的像素是如何组织的,以及如何访问和操作它们

  • 在图像上绘制不同的形状,如线条、矩形和圆形

  • 使用 Python 编写代码并使用 OpenCV 处理示例来访问和操作图像

图像处理

图像处理是一种处理数字图像以获得增强图像或从中提取有用信息的技术。在图像处理中,输入是图像,输出可以是图像或与该图像相关联的一些特性或特征。视频是一系列图像或帧。因此,图像处理技术也适用于视频处理。在这一章中,我将解释数字图像处理的核心概念。我还将向您展示如何处理图像并编写代码来操作它们。

图像基础

数字图像是对象/场景或扫描文档的电子表示。图像的数字化意味着将其转换成一系列数字,并将这些数字存储在计算机存储系统中。理解这些数字是如何排列的以及如何操作它们是本章的主要目标。在这一章中,我将解释图像是由什么组成的,以及如何使用 OpenCV 和 Python 来操作它。

像素

想象一系列按行和列排列的点,这些点有不同的颜色。这差不多就是图像的形成过程。形成图像的点称为像素。这些像素用数字表示,数字的值决定了像素的颜色。将图像想象成一个由正方形单元组成的网格,每个单元由特定颜色的一个像素组成。例如,300×400 像素的图像意味着图像被组织成 300 行 400 列的网格。这意味着我们的图像有 300×400 = 120,000 个像素。

像素颜色

像素以两种方式表示:灰度和颜色。

灰度等级

在灰度图像中,每个像素取 0 到 255 之间的值。值 0 代表黑色,255 代表白色。介于两者之间的值是不同的灰度。接近 0 的值是较暗的灰色阴影,接近 255 的值是较亮的灰色阴影。

颜色

RGB(代表红色、蓝色和绿色)颜色模型是最流行的像素颜色表示之一。还有其他颜色模型,但在本书中我们将坚持使用 RGB。

在 RGB 模型中,每个像素被表示为三个值的元组,一般表示如下:(红色分量的值,绿色分量的值,蓝色分量的值)。这三种颜色中的每一种都用从 0 到 255 的整数来表示。以下是一些例子:

  • (0,0,0)是黑色。

  • (255,0,0)是纯红色。

  • (0,255,0)是纯绿色。

(0,0,255)代表什么颜色?

(255,255,255)代表什么颜色?

这个 w3school 网站( https://www.w3schools.com/colors/colors_rgb.asp )是一个玩 RGB 元组不同组合探索更多模式的好地方。

探究以下每个元组代表什么颜色:

  • (0,0,128)

  • (128,0,128)

  • (128,128,0)

让我们试着做黄色。这里有一个线索:红色和绿色组成黄色。这意味着一个纯红色(255),一个纯绿色(255),没有蓝色(0)将使黄色。因此,黄色的 RGB 元组是(255,255,0)。

现在我们对像素和它们的颜色有了很好的了解,让我们来了解像素在图像中是如何排列的,以及如何访问它们。下一节将讨论图像处理中坐标系的概念。

坐标系统

图像中的像素以网格的形式排列,网格由行和列组成。想象一个八行八列的正方形网格。这将形成一个 8×8 或 64 像素的图像。这可以想象成一个 2D 坐标系,其中(0,0)是左上角。图 2-1 显示了我们的示例 8×8 像素图像。

img/493065_1_En_2_Fig1_HTML.png

图 2-1

像素坐标系

左上角是图像坐标系的起点或原点。右上角的像素用(7,0)表示,左下角的像素用(7,0)表示,右下角的像素用(7,7)表示。这可以概括为(x,y),其中 x 是单元格距图像左边缘的位置,y 是距图像上边缘的垂直位置。在图 2-1 中,红色像素位于左起第五个位置和上起第四个位置。由于坐标系从 0 开始,所以图 2-1 中红色像素的坐标为(4,3)。

为了更清楚一点,让我们想象一个 8×8 像素的图像,上面写着字母 H (如图 2-3 )。此外,为了简单起见,假设这是一个灰度图像,字母 H 用黑色书写,图像的其余区域为白色。

img/493065_1_En_2_Fig2_HTML.png

图 2-2

像素坐标系示例

记住,在灰度模型中,黑色像素用 0 表示,白色像素用 255 表示。图 2-3 显示了 8×8 网格内每个像素的值。

img/493065_1_En_2_Fig3_HTML.jpg

图 2-3

像素矩阵和值

那么,位置(1,4)的像素值是多少呢?而在位置(2,2)?

我希望你现在已经清楚图像是如何用排列在网格中的数字来表示的了。这些数字被序列化并存储在计算机的存储系统中,并在屏幕上显示为图像。至此,您已经知道了如何使用坐标系访问像素,以及如何为这些像素分配颜色。

我们已经建立了坚实的基础,并学习了图像表示的基本概念。让我们自己动手练习一些 Python 和 OpenCV 编码。在下一节中,我将一步一步地向您展示如何编写代码来从计算机磁盘加载图像、访问像素、操作它们,以及将它们写回磁盘。事不宜迟,我们开始吧!

Python 和 OpenCV 代码来操作图像

OpenCV 将图像的像素值表示为 NumPy 数组。(不熟悉 NumPy?可以在 https://numpy.org/devdocs/user/quickstart.html 找到“入门”教程)。换句话说,当您加载一个图像时,OpenCV 会创建一个 NumPy 数组。通过简单地提供(x,y)坐标,可以从 NumPy 中获得像素值。

当您给出(x,y)坐标时,NumPy 将返回这些坐标处像素的颜色值,如下所示:

  • 对于灰度图像,NumPy 返回的值将是 0 到 255 之间的单个值。

  • 对于彩色图像,NumPy 返回的值将是一个红色、绿色和蓝色的元组。请注意,OpenCV 以相反的顺序维护 RGB 序列。请记住 OpenCV 的这个重要特性,以避免在使用 OpenCV 时出现任何混淆。

换句话说,OpenCV 在 BGR 序列中存储颜色,在 RGB 序列中存储而不是

在我们写任何代码之前,让我们确保我们总是使用我们的 virtualenv,在~/cv目录中,我们已经用 PyCharm 设置了它。

启动你的 PyCharm IDE,做一个项目(我把我的项目命名为 cviz,是“计算机视觉”的简称)。参考图 2-4 并确保您已经选择了现有的解释器并选择了我们的 virtualenv Python 3.6(cv)。

img/493065_1_En_2_Fig4_HTML.jpg

图 2-4

PyCharm IDE,显示了使用 virtualenv 的项目设置

程序:加载、浏览和显示图像

清单 2-1 展示了加载、浏览和显示图像的 Python 代码。

Filename: Listing_2_1.py
1    from __future__ import print_function
2    import cv2
3
4    # image path
5    image_path = "images/marsrover.png"
6    # Read or load image from its path
7    image = cv2.imread(image_path)
8    # image is a NumPy array
9    print("Dimensions of the image: ", image.ndim)
10   print("Image height: ", format(image.shape[0]))
11   print("Image width: ", format(image.shape[1]))
12   print("Image channels: ", format(image.shape[2]))
13   print("Size of the image array: ", image.size)
14   # Display the image and wait until a key is pressed
15   cv2.imshow("My Image", image)
16   cv2.waitKey(0)

Listing 2-1Python Code to Load, Explore, and Display an Image

这里解释清单 2-1 中的代码。

在第 1 行和第 2 行,我们从 OpenCV 的__future__包和cv2中导入 Python 的print_function

第 5 行只是我们要从一个目录中加载的图像的路径。如果您的输入路径在不同的目录中,您应该给出图像文件的完整或相对路径。

在第 7 行,使用 OpenCV 的cv2.imread()函数,我们将图像读入一个 NumPy 数组,并赋给一个名为image的变量(这个变量可以是您喜欢的任何东西)。

在第 9 行到第 13 行,使用 NumPy 特性,我们显示了图像数组的维度、高度、宽度、通道数和数组的大小(即像素数)。

第 15 行使用 OpenCV 的imshow()函数显示图像。

在第 16 行中,waitKey()函数允许程序不立即终止并等待用户按下任何键。当您看到将在第 15 行显示的图像窗口时,按任意键终止程序,否则程序将阻塞。

图 2-5 显示了清单 2-1 的输出。

img/493065_1_En_2_Fig5_HTML.jpg

图 2-5

输出和图像显示

image NumPy 数组由三个维度组成:高×宽×通道。数组的第一个元素是高度,它告诉我们像素网格有多少行。类似地,第二个元素是宽度,它表示网格的列数。这三个通道代表 BGR(不是 RBG)颜色分量。数组的大小为 400×640×3 = 768,000。这实际上意味着我们的图像有 400×640 = 256000 个像素,每个像素有三个颜色值。

程序:访问和操作像素的 OpenCV 代码

在下一个程序中,我们将看到如何使用我们之前学过的坐标系来访问和修改像素值。清单 2-2 显示了代码示例,后面有逐行解释。

Filename: Listing_2_2.py
1    from __future__ import print_function
2    import cv2
3
4    # image path
5    image_path = "images/marsrover.png"
6    # Read or load image from its path
7    image = cv2.imread(image_path)
8
9    # Access pixel at (0,0) location
10   (b, g, r) = image[0, 0]
11   print("Blue, Green and Red values at (0,0): ", format((b, g, r)))
12
13   # Manipulate pixels and show modified image
14   image[0:100, 0:100] = (255, 255, 0)
15   cv2.imshow("Modified Image", image)
16   cv2.waitKey(0)

Listing 2-2Code Example to Access and Manipulate Image Pixels

列表 2-2 在此说明。

第 1 行到第 7 行从一个目录路径导入和读取图像(如讨论清单 2-1 时所解释的)。

在第 10 行,我们获得了坐标(0,0)处像素的 BGR(而不是 RBG)值,并使用 NumPy 语法将它们分配给(b,g,r)元组。

第 11 行显示了 BGR 值。

在第 14 行中,我们沿着 y 轴从 0 到 100 以及沿着 x 轴从 0 到 100 的像素范围来形成一个 100×100 的正方形,并且将值(255,255,0)或者纯蓝色、纯绿色和无红色分配给该正方形内的所有像素。

第 16 行显示修改后的图像。

第 17 行等待用户按任意键退出程序。

图 2-6 显示了清单 2-2 的一些示例输出。

img/493065_1_En_2_Fig6_HTML.png

图 2-6

输出和修改的图像显示

如图 2-6 所示,修改后的图像在左上角有一个 100×100 像素的正方形,颜色为浅绿色,用 BGR 方案的(255,255,0)表示。

图画

OpenCV 提供了在图像上绘制形状的便捷方法。我们将学习如何使用以下方法在图像上绘制直线、矩形和圆形:

  • 线 : cv2.line()

  • 长方形 : cv2.rectangle()

  • : cv2.circle()

在图像上画线

我们将使用一种简单的方法在图像上画线,如下所示:

  1. 将图像加载到 NumPy 数组中。

  2. 确定直线起始位置的坐标。

  3. 确定直线终点的坐标。

  4. 设置线条的颜色。

  5. 或者,设置线条的粗细。

清单 2-3 演示了如何在图像上画线。

Filename: Listing_2_3.py
1    from __future__ import print_function
2    import cv2
3
4    # image path
5    image_path = "images/marsrover.png"
6    # Read or load image from its path
7    image = cv2.imread(image_path)
8
9    # set start and end coordinates
10   start = (0, 0)
11   end = (image.shape[1], image.shape[0])
12   # set the color in BGR
13   color = (255,0,0)
14   # set thickness in pixel
15   thickness = 4
16   cv2.line(image, start, end, color, thickness)
17
18   #display the modified image
19   cv2.imshow("Modified Image", image)
20   cv2.waitKey(0)

Listing 2-3Drawing a Line on an Image

下面是对代码的逐行解释。

第 1 行和第 2 行是通常的导入。从现在开始,我不会重复进口,除非我们有一个新的提及。

第 5 行是图像路径。

第 7 行实际上将图像加载到一个名为 image 的 NumPy 数组中。

第 10 行定义了绘制直线的起点坐标。回想一下,位置(0,0)是图像的左上角。

第 11 行指定了图像端点的坐标。您会注意到表达式(image.shape[1], image.shape[0])代表图像右下角的坐标。

你现在可能已经猜到我们在画一条对角线。

第 13 行设置我们要画的线的颜色,第 15 行设置它的粗细。

实际的线画在第 16 行。cv2.line()函数采用以下参数:

  • 图像编号。这就是我们正在划线的图像。

  • 开始坐标。

  • 结束坐标。

  • 颜色。

  • 厚度。(这是可选的。如果您不传递这个参数,我们的线条将有一个默认的厚度 1。)

最后,修改后的图像显示在第 19 行。第 20 行等待用户按任意键来终止程序。图 2-7 显示了我们刚刚画了一条线的图像的样本输出。

img/493065_1_En_2_Fig7_HTML.jpg

图 2-7

带有蓝色对角线的图像

在图像上绘制矩形

用 OpenCV 画矩形很容易。让我们直接深入代码(清单 2-4 )。我们将首先加载一个图像,并为其绘制一个矩形。我们会将修改后的图像保存到磁盘。

Filename: Listing_2_4.py
1    from __future__ import print_function
2    import cv2
3
4    # image path
5    image_path = "images/marsrover.png"
6    # Read or load image from its path
7    image = cv2.imread(image_path)
8    # set the start and end coordinates
9    # of the top-left and bottom-right corners of the rectangle
10   start = (100,70)
11   end = (350,380)
12   # Set the color and thickness of the outline
13   color = (0,255,0)
14   thickness = 5
15   # Draw the rectangle
16   cv2.rectangle(image, start, end, color, thickness)
17   # Save the modified image with the rectangle drawn to it.
18   cv2.imwrite("rectangle.jpg", image)
19   # Display the modified image
20   cv2.imshow("Rectangle", image)
21   cv2.waitKey(0)

Listing 2-4Loading an Image, Drawing a Rectangle to It, Saving It, and Displaying the Modified Image

下面是清单 2-4 的逐行解释。

1 号线和 2 号线是我们通常的进口货。

第 5 行指定了图像路径。

第 6 行从它的路径中读取图像。

第 10 行设置了我们想要在图像上绘制的矩形的起点。起点由矩形左上角的坐标组成。

第 11 行设置矩形的端点。这表示矩形右下角的坐标。

第 13 行设置颜色,第 14 行设置矩形轮廓的粗细。

第 16 行实际上绘制了矩形。我们使用 OpenCV 的rectangle()函数,它接受以下参数:

  • 保存图像像素值的 NumPy 数组

  • 起始坐标(矩形的左上角)

  • 结束坐标(矩形的右下角)

  • 轮廓的颜色

  • 轮廓的粗细

注意,第 16 行没有任何赋值操作符。换句话说,我们没有将来自cv2.rectangle()函数的返回值赋给任何变量。作为参数传递给cv2.rectangle()函数的 NumPy 数组image被修改。

第 18 行将修改后的画有矩形的图像保存到磁盘上的一个文件中。

第 20 行显示修改后的图像。

第 21 行调用waitKey()函数,允许图像保持显示在屏幕上,直到按下一个键。函数waitKey()无限期等待一个按键事件,或者等待一定的毫秒级延迟。由于操作系统在切换线程之间有一个最小时间间隔,在按键后,waitKey()函数不会等待作为参数传递给waitKey()函数的延迟时间。实际等待时间取决于按下按键和调用waitKey()功能时您的电脑可能正在运行的其他程序。

图 2-8 显示了画有矩形的图像的输出。

img/493065_1_En_2_Fig8_HTML.jpg

图 2-8

绘制了矩形的图像

在前面的例子中,我们首先从磁盘中读取一个图像,并在上面画了一个矩形。我们现在将稍微修改这个例子,并在空白画布上绘制矩形。我们将首先创建一个画布(而不是加载一个现有的图像),并在其上绘制一个矩形。然后,我们将保存并显示结果图像。参见清单 2-5 。

Filename: Listing 2_5.py
1    from __future__ import print_function
2    import cv2
3    import numpy as np
4
5    # create a new canvas
6    canvas = np.zeros((200, 200, 3), dtype = "uint8")
7    start = (10,10)
8    end = (100,100)
9    color = (0,0,255)
10   thickness = 5
11   cv2.rectangle(canvas, start, end, color, thickness)
12   cv2.imwrite("rectangle.jpg", canvas)
13   cv2.imshow("Rectangle", canvas)
14   cv2.waitKey(0)

Listing 2-5Drawing a Rectangle on a New Canvas and Saving the Image

在清单 2-5 中,除了第 3 行和第 6 行之外的所有行与清单 2-4 中的相同。

第 3 行导入了我们将用来创建画布的 NumPy 库。

第 6 行是我们创建图像的地方(称为画布)。我们的画布是 200×200 像素,每个像素保存三个通道(保存 BGR 值)。变量名canvas是一个 NumPy 数组,在本例中,它为每个像素保存一个零值。请注意,画布的每个像素值的数据类型是一个 8 位无符号整数(如第一章所述)。

你会怎么画一个实心矩形(意思是,用特定颜色填充的矩形)?

线索:厚度设为-1。

图 2-9 显示了清单 2-5 的输出。图 2-10 显示了一个画有实心矩形的画布。

img/493065_1_En_2_Fig10_HTML.jpg

图 2-10

厚度为-1 的实心矩形

img/493065_1_En_2_Fig9_HTML.jpg

图 2-9

边框粗细为 5 的矩形

在图像上画一个圆

在图像上画一个圆也同样容易。您可以创建自己的画布或加载现有图像,然后设置圆心坐标、半径、颜色和圆轮廓的粗细。

清单 2-6 显示了一段在空白画布上画圆的工作代码。图 2-11 显示了该代码清单的输出。

Filename: Listing_2_6.py
1    from __future__ import print_function
2    import cv2
3    import numpy as np
4
5    # create a new canvas
6    canvas = np.zeros((200, 200, 3), dtype = "uint8")
7    center = (100,100)
8    radius = 50
9    color = (0,0,255)
10   thickness = 5
11   cv2.circle(canvas, center, radius, color, thickness)
12   cv2.imwrite("circle.jpg", canvas)
13   cv2.imshow("My Circle", canvas)
14   cv2.waitKey(0)

Listing 2-6Drawing a Circle on a Canvas

清单 2-6 中的代码与清单 2-5 中的代码差别不大,除了第 7 行定义了圆心。

此外,第 8 行设置半径,第 9 行定义颜色,第 10 行设置圆的厚度。最后,第 11 行画圆并接受以下参数:

  • 要在其上绘制圆的图像。这是包含图像像素的 NumPy 数组。

  • 圆心的坐标。

  • 圆的半径。

  • 圆轮廓的颜色。

  • 轮廓的粗细。

img/493065_1_En_2_Fig11_HTML.jpg

图 2-11

画在黑色画布中心的圆

给你做个练习:

  1. 在画布的中心画一个实心圆。

  2. 画两个同心圆,最外圆的半径是内圆半径的 1.5 倍。

摘要

在这一章中,我们学习了图像的基础知识,从像素开始,以及它们如何在不同的配色方案中表示,即灰色和彩色。坐标系有助于定位特定的像素并处理它们的值。我们学习了如何在图像上绘制一些基本的形状,如直线、矩形和圆形。虽然这些都是非常基本和容易的,但它们是在图像处理中做任何事情的重要概念。

在下一章,我们将探索图像处理中使用的不同技术和算法。

三、图像处理技术

在计算机视觉应用中,图像通常从其来源获取,如相机、存储在计算机磁盘上的文件或来自另一个应用的流。在大多数情况下,这些输入图像从一种形式转换成另一种形式。例如,我们可能需要调整大小、旋转或改变它们的颜色。在某些情况下,我们可能需要删除背景像素或合并两幅图像。在其他情况下,我们可能需要找到图像中特定对象周围的边界。

本章通过 Python 和 OpenCV 中的例子探讨了图像转换的各种技术。我们本章的学习目标如下:

  • 探索最常用的转换技术

  • 学习图像处理中使用的算法

  • 学习清洁图像的技术,如降噪

  • 学习合并两个或多个图像或分割通道的技巧

  • 学习如何检测和绘制图像中对象周围的轮廓(边界)

转换

在处理任何计算机视觉问题时,您经常需要将图像转换成不同的形式。本章通过一组 Python 示例探索了不同的图像转换技术。

调整大小

让我们从第一个转换开始,调整大小。为了调整图像的大小,我们增加或减少图像的高度和宽度。纵横比是调整图像大小时要记住的一个重要概念。纵横比是宽度与高度的比例,通过宽度除以高度来计算。计算纵横比的公式如下:

  • 长宽比=宽度/高度

正方形图像的长宽比为 1:1,长宽比为 3:1 意味着宽度是高度的三倍。如果图像的高度为 300 像素,宽度为 600 像素,则其纵横比为 2:1。

调整大小时,保持原始纵横比可以确保调整后的图像看起来不会被拉伸或压缩。

清单 3-1 显示了以下两种不同的图像大小调整技术:

  • 在保持纵横比的同时,将图像调整到所需的像素大小。换句话说,如果您知道图像的期望高度,您可以使用纵横比计算相应的宽度。

  • 按因子调整图像大小。例如,将图像宽度放大 1.5 倍或高度放大 2.5 倍。

OpenCV 提供了一个函数cv2.resize()来执行这两种调整大小的技术。

Filename: Listing_3_1.py
1    from __future__ import print_function
2    import cv2
3    import numpy as np
4
5    # Load image
6    imagePath = "images/zebra.png"
7    image = cv2.imread(imagePath)
8
9    # Get image shape which returns height, width, and channels as a tuple. Calculate the aspect ratio
10   (h, w) = image.shape[:2]
11   aspect = w / h
12
13   # lets resize the image to  decrease height by half of the original image.
14   # Remember, pixel values must be integers.
15   height = int(0.5 * h)
16   width =  int(height * aspect)
17
18   # New image dimension as a tuple
19   dimension = (height, width)
20   resizedImage = cv2.resize(image, dimension, interpolation=cv2.INTER_AREA)
21   cv2.imshow("Resized Image", resizedImage)
22
23   # Resize using x and y factors
24   resizedWithFactors = cv2.resize(image, None, fx=1.2, fy=1.2, interpolation=cv2.INTER_LANCZOS4)
25   cv2.imshow("Resized with factors", resizedWithFactors)
26   cv2.waitKey(0)

Listing 3-1Code to Calculate Aspect Ratio and Resize the Image

清单 3-1 展示了如何使用 OpenCV 的cv2.resize()函数来调整图像的大小。resize()函数将以下参数作为参数:

  • 第一个参数是由 NumPy 数组表示的原始图像。

  • 第二个参数是要调整的尺寸。这是一个整数元组,表示调整后的图像的高度和宽度。如果您想要使用水平或垂直因子来调整大小,请将此参数作为None传递,稍后将对此进行解释。

  • 第三和第四个参数fxfy是水平(横向)和垂直(高度)方向上的调整大小因子。这两个参数是可选的。

  • 最后一个参数是插值。这是 OpenCV 内部用来调整图像大小的算法名称。可用的插补算法有INTER_AREAINTER_LINEARINTER_CUBICINTER_NEAREST。一会儿将简要描述这些算法。

    插值是在调整图像大小时计算像素值的过程。OpenCV 支持以下五种插值算法:

    INTER_LINEAR:这实际上是一个双线性插值,其中确定四个最近的邻居(2×2 = 4)并计算它们的加权平均值,以确定下一个像素的值。

    INTER_NEAREST:这使用最近邻插值方法,当给定某个空间中某个非给定点周围(相邻)点的函数值时,近似该点的函数值。换句话说,为了计算一个像素的值,它的最近邻被认为是插值函数的近似值。

    INTER_CUBIC:这使用双三次插值算法来计算像素值。与双线性插值类似,它使用 4×4 = 16 个最近邻来确定下一个像素的值。当速度不是一个问题时,双三次插值比双线性插值提供了更好的调整图像大小。

    INTER_LANCZOS4:这使用 8×8 最近邻插值。

    INTER_AREA:像素值的计算通过使用像素面积关系来执行(如 OpenCV 官方文档所述)。我们使用这种算法来创建无莫尔条纹的调整大小的图像。当图像尺寸被放大时,INTER_AREA类似于INTER_NEAREST方法。

让我们检查清单 3-1 中的代码。

第 1 行到第 3 行是库导入。

第 6 行分配图像路径,第 7 行将图像作为 NumPy 数组读取,并分配给一个名为image的变量。

NumPy 的 shape 函数返回数组中对象的尺寸。调用图像的 shape 函数以元组的形式返回通道的高度、宽度和数量。第 10 行通过指定索引长度 2 ( image.shape[,:2])仅检索高度和宽度。高度和宽度存储在变量hw中。

如果我们不指定索引长度,它将返回具有高度、宽度和通道的元组,如下所示:

(h, w, c) = image.shape[:]

在本例中,我们希望将图像尺寸缩小 50 %,同时保持原始的纵横比。我们可以简单地将原来的高度和宽度乘以 0.5,得到想要的高度和宽度。如果我们只知道期望的高度,我们可以通过将原始的新高度乘以长宽比来计算期望的宽度。第 15 行和第 16 行演示了这一点。

第 19 行将所需的高度和宽度设置为一个元组。

第 20 行调用 OpenCV 的cv2.resize()函数,并将原始图像 NumPy、期望的尺寸和插值算法(本例中为INTER_AREA)作为参数传递给resize()函数。

第 24 行演示了当我们知道图像的高度和/或宽度需要增加或减少的因素时,使用第二种方法的 resize 操作。在本例中,高度和宽度都放大了 1.2 倍。

图 3-1 和图 3-2 显示了我们的调整大小程序的示例输出。

img/493065_1_En_3_Fig2_HTML.jpg

图 3-2

调整图像大小

img/493065_1_En_3_Fig1_HTML.jpg

图 3-1

原象

翻译

图像平移是指沿着 x-y- 轴向左、向右、向上或向下移动图像。

移动图像时有两个主要步骤:定义一个平移矩阵和调用cv2.warpAffine函数。平移矩阵定义了移动的方向和量。warpAffine函数是执行实际移动的 OpenCV 函数。cv2.warpAffine函数有三个参数:图像数量、平移矩阵和图像尺寸。

让我们通过一个代码示例来理解这一点(参见清单 3-2 )。

Filename: Listing_3_2.py
1    from __future__ import print_function
2    import cv2
3    import numpy as np
4
5    #Load image
6    imagePath = "images/soccer-in-green.jpg"
7    image = cv2.imread(imagePath)
8
9    #Define translation matrix
10   translationMatrix = np.float32([[1,0,50],[0,1,20]])
11
12   #Move the image
13   movedImage = cv2.warpAffine(image, translationMatrix, (image.shape[1], image.shape[0]))
14
15   cv2.imshow("Moved image", movedImage)
16   cv2.waitKey(0)

Listing 3-2Image Translation Along the x- and y-Axes

清单 3-2 演示了翻译操作。平移矩阵在第 10 行定义,这里我们定义了移动方向,并定义了图像应该移动多少像素。下面是对第 10 行的解释。

在这个例子中,平移矩阵是 2×3 矩阵或 2D 阵列。

由[1,0,50]定义的第一行表示沿 x 轴向右移动 50 个像素。如果这个数组的第三个元素是负数,将向左移动。

由[0,1,20]表示的第二行定义了沿着 y 轴向下移动 20 个像素。如果第二行的第三个元素是负数,这将沿着 y 轴向上移动图像。

在第 13 行,我们调用 OpenCV 的warpAffine函数。该函数采用以下参数:

  • 我们要移动的图像的 NumPy 表示。

  • 定义移动方向和移动量的平移矩阵。

  • 最后一个参数是一个元组,它具有我们要在其中移动图像的画布的宽度和高度。在这个例子中,我们保持画布大小与图像的原始高度和宽度相同。

图 3-3 和图 3-4 显示了结果。

img/493065_1_En_3_Fig4_HTML.jpg

图 3-4

移动图像

img/493065_1_En_3_Fig3_HTML.jpg

图 3-3

原象

这里有一个练习:将一幅图像向左移动 50 像素,向上移动 60 像素。

循环

为了将图像旋转某个角度θ,我们首先使用 OpenCV 的cv2.getRotationMatrix2D.定义一个旋转矩阵。我将在清单 3-3 中解释如何创建这个旋转矩阵。要旋转图像,我们只需像前面的翻译一样调用相同的cv2.warpAffine函数。让我们逐行查看旋转代码。

Filename: Listing_3_3.py
1    from __future__ import print_function
2    import cv2
3    import numpy as np
4
5    # Load image
6    imagePath = "images/zebrasmall.png"
7    image = cv2.imread(imagePath)
8    (h,w) = image.shape[:2]
9
10   #Define translation matrix
11   center = (h//2, w//2)
12   angle = -45
13   scale = 1.0
14
15   rotationMatrix = cv2.getRotationMatrix2D(center, angle, scale)
16
17   # Rotate the image
18   rotatedImage = cv2.warpAffine(image, rotationMatrix, (image.shape[1], image.shape[0]))
19
20   cv2.imshow("Rotated image", rotatedImage)
21   cv2.waitKey(0)

Listing 3-3Image Rotation Around the Center of the Image

清单 3-3 展示了如何将图像围绕其中心旋转 45 度角(顺时针)。

第 11 行计算图像的中心。注意,我们通过使用//来划分高度和宽度,只得到它的整数部分。

第 12 行简单地给我们想要旋转图像的角度赋值。负值将顺时针旋转图像,而正值将逆时针旋转。

第 13 行设置旋转比例,它被设置为在旋转时调整图像的大小。值 1.0 在旋转后保持原始大小。如果我们将它设置为 0.5,旋转后的图像将会缩小一半。

在第 15 行,我们使用 OpenCV 的函数cv2.getRotationMatrix2D定义旋转矩阵,并传递以下参数:

  • 一个元组,表示图像需要围绕其旋转的点

  • 以度为单位的旋转角度

  • 调整比例

第 18 行按照旋转矩阵的定义旋转图像。我们使用我们用来翻译图像的相同的warpAffine函数。唯一的区别是,在旋转的情况下,我们传递在第 15 行创建的旋转矩阵。

第 20 行显示旋转的图像,第 21 行在显示的图像关闭之前等待按键。

图 3-5 和图 3-6 显示了我们代码的示例输出。

img/493065_1_En_3_Fig6_HTML.jpg

图 3-6

旋转图像

img/493065_1_En_3_Fig5_HTML.jpg

图 3-5

原象

轻弹

沿着 x 轴水平翻转图像,或者沿着 y 轴垂直翻转图像,都可以通过调用 OpenCV 的便捷函数cv2.flip()轻松完成。这个cv2.flip()函数有两个参数。

  • 原始图像

  • 翻转的方向

    • 0 表示垂直翻转。

    • 1 表示水平翻转。

    • -1 表示先水平翻转,再垂直翻转。

让我们看看清单 3-4 中的图像向不同方向翻转。

Filename: Listing_3_4.py
1    from __future__ import print_function
2    import cv2
3    import numpy as np
4
5    # Load image
6    imagePath = "images/zebrasmall.png"
7    image = cv2.imread(imagePath)
8
9    # Flip horizontally
10   flippedHorizontally = cv2.flip(image, 1)
11   cv2.imshow("Flipped Horizontally", flippedHorizontally)
12   cv2.waitKey(-1)
13
14   # Flip vertically
15   flippedVertically = cv2.flip(image, 0)
16   cv2.imshow("Flipped Vertically", flippedVertically)
17   cv2.waitKey(-1)
18   # Flip horizontally and then vertically
19   flippedHV = cv2.flip(image, -1)
20   cv2.imshow("Flipped H and V", flippedHV)
21   cv2.waitKey(-1)

Listing 3-4Image Flipping Horizontally, Vertically, and Then Horizontally plus Vertically

清单 3-4 不言自明。以防万一,这里是执行翻转的线的解释。

第 10 行调用cv2.flip()函数并传递原始图像和水平翻转的 0 值。

类似地,第 15 行垂直翻转图像,而第 19 行有一个参数-1,使图像先水平翻转,然后垂直翻转。图 3-7 到 3-10 显示了这些翻转的样子。

img/493065_1_En_3_Fig10_HTML.jpg

图 3-10

水平翻转,然后垂直翻转

img/493065_1_En_3_Fig9_HTML.jpg

图 3-9

垂直翻转

img/493065_1_En_3_Fig8_HTML.jpg

图 3-8

水平翻转

img/493065_1_En_3_Fig7_HTML.jpg

图 3-7

原象

种植

图像裁剪是指删除图像中不需要的外部区域。回想一下,OpenCV 将图像表示为 NumPy 数组。裁剪图像是通过对图像数组进行切片来实现的。OpenCV 中没有裁剪图像的特殊功能。我们使用 NumPy 数组特征对图像进行切片。清单 3-5 展示了如何裁剪图像。

Filename: Listing_3_5.py
1    from __future__ import print_function
2    import cv2
3    import numpy as np
4
5    # Load image
6    imagePath = "images/zebrasmall.png"
7    image = cv2.imread(imagePath)
8    cv2.imshow("Original Image", image)
9    cv2.waitKey(0)
10
11   # Crop the image to get only the face of the zebra
12   croppedImage = image[0:150, 0:250]
13   cv2.imshow("Cropped Image", croppedImage)
14   cv2.waitKey(0)

Listing 3-5Image Cropping

第 12 行显示了如何分割 NumPy 数组。在这个例子中,我们使用 150 像素的高度和 250 像素的宽度来裁剪我们的图像,只提取斑马的面部部分。

图 3-11 显示原始图像,图 3-12 显示裁剪后的图像。

img/493065_1_En_3_Fig12_HTML.jpg

图 3-12

裁剪的图像

img/493065_1_En_3_Fig11_HTML.jpg

图 3-11

原象

图像算术和位运算

在构建计算机视觉应用时,您经常需要增强输入图像的属性。为此,您可能需要执行某些算术运算,如加法和减法,以及按位运算,如 OR、and、NOT 和 XOR。

到目前为止,我们已经了解到图像中的每个像素可以有 0 到 255 之间的任何整数值。当你给一个像素加上一个常数,使得结果值大于 255 或者小于 0,如果你从中减去一个常数,会发生什么?例如,假设图像中的一个像素值为 230,您给它加上 30。当然,像素的值不能是 260。那么,我们该怎么办呢?我们应该截断该值以保持像素最大值为 255,还是将其绕回以使其为 4(意思是在 255 之后,回到 0,并在 255 之后添加余数)?

当像素值超出范围[0,255]时,有两种方法来处理这种情况:

  • 饱和运行(或微调):本次运行,230+30 255。

  • 模运算:这里是这样执行模运算的:(230+30)% 256 4。

您可以使用 OpenCV 和 NumPy 的内置函数来执行算术运算。但是,它们处理操作的方式不同。

OpenCV 的加法是饱和操作。另一方面,NumPy 执行模运算。

请注意 NumPy 和 OpenCV 之间的区别,因为这两种技术产生不同的结果,并且在哪里使用它们取决于您的情况和需求。

添加

OpenCV 提供了两种方便的方法来添加两幅图像。

  • cv2.add(),它将两个大小相等的图像作为参数,并将它们的像素值相加以产生结果。

  • cv2.addWeighted(),一般用于两幅图像的融合。稍后将提供关于此功能的更多细节。

请注意,要添加两个图像,它们必须具有相同的深度和类型。

让我们写一些代码来理解这两个加法有什么不同。参见清单 3-6 。

Filename: Listing_3_6.py
1    from __future__ import print_function
2    import cv2
3    import numpy as np
4
5    image1Path = "images/zebra.png"
6    image2Path = "images/nature.jpg"
7
8    image1 = cv2.imread(image1Path)
9    image2 = cv2.imread(image2Path)
10
11   # resize the two images to make them of the same dimension. This is a must to add two images
12   resizedImage1 = cv2.resize(image1,(300,300),interpolation=cv2.INTER_AREA)
13   resizedImage2 = cv2.resize(image2,(300,300),interpolation=cv2.INTER_AREA)
14
15   # This is a simple addition of two images
16   resultant = cv2.add(resizedImage1, resizedImage2)
17
18   # Display these images to see the difference
19   cv2.imshow("Resized 1", resizedImage1)
20   cv2.waitKey(0)
21
22   cv2.imshow("Resized 2", resizedImage2)
23   cv2.waitKey(0)
24
25   cv2.imshow("Resultant Image", resultant)
26   cv2.waitKey(0)
27
28   # This is weighted addition of the two images
29   weightedImage = cv2.addWeighted(resizedImage1,0.7, resizedImage2, 0.3, 0)
30   cv2.imshow("Weighted Image", weightedImage)
31   cv2.waitKey(0)
32
33   imageEnhanced = 255*resizedImage1
34   cv2.imshow("Enhanced Image", imageEnhanced)
35   cv2.waitKey(0)
36
37   arrayImage = resizedImage1+resizedImage2
38   cv2.imshow("Array Image", arrayImage)
39   cv2.waitKey(0)

Listing 3-6Addition of Two Images

第 8 行和第 9 行从磁盘加载了两个不同的映像。正如我前面提到的,图像必须有相同的大小和深度才能加在一起;您可能已经猜到了第 12 行和第 13 行的目的。图像大小调整为 300×300 像素。

第 16 行是这两个图像相加的地方。我们使用 OpenCV 的简单加法函数cv2.add(),它将两幅图像作为参数。参见图 3-15 中的输出图像,查看两幅图像简单相加的结果。

在第 29 行,我们使用 OpenCV 的cv2.addWeighted()函数进行加权加法,工作方式如下:

  • 反相结果=x 图像 1+【β】x 图像 2+(1)****

****其中𝝰是图像 1 的权重,𝛃是图像 2 的权重,𝛄是常数。通过改变这些权重的值,我们创建了期望的加法效果。

通过查看前面的等式,您可以很容易地猜出需要传递给函数cv2.addWeighted()的参数。以下是参数列表:

  • 图像 1 的 NumPy 数组

  • 图像 1 的权重𝝰(在我们的示例代码中,我们传递了一个值 0.7)

  • 图像 2 数组的数量

  • 图像 2 的权重𝛃(在示例代码中我们传递了值 0.3)

  • 最后一个参数,𝛄(在我们的例子中我们传递了一个零值)

让我们检查清单 3-6 的输入和输出。图 3-13 和图 3-14 为原始图像,调整为 300x300,使其尺寸相等。

图 3-15 是使用函数add()将这两幅图像相加后的输出。

图 3-16 是使用功能addWeighted()将输入相加后的结果图像。

img/493065_1_En_3_Fig14_HTML.jpg

图 3-14

添加的原始图像

img/493065_1_En_3_Fig13_HTML.jpg

图 3-13

原象

通过参考图 3-15 和图 3-16 所示的输出,注意简单addaddWeighted功能之间的区别。

img/493065_1_En_3_Fig16_HTML.jpg

图 3-16

cv2.addWeighted()的结果

img/493065_1_En_3_Fig15_HTML.jpg

图 3-15

cv2.add()的结果

减法

图像相减是指从一幅图像的对应像素值中减去另一幅图像的像素值。我们也可以从图像像素中减去一个常数。当我们减去两个图像时,重要的是要注意这两个图像必须具有相同的大小和深度。

当你从一幅图像中减去它本身会发生什么?那么,合成图像的所有像素值将为零(意味着黑色)。该属性在检测图像中的任何变化/改变时是有用的。如果没有变化,两幅图像相减的结果就是一幅全黑的图像。

减去图像的另一个原因是消除任何不均匀的部分或阴影。

我们将通过代码示例看到一些有趣的图像减法结果。参见清单 3-7 。

Filename: Listing_3_7.py
1    import cv2
2    import numpy as np
3
4
5    image1Path = "images/cat1.png"
6    image2Path = "images/cat2.png"
7
8    image1 = cv2.imread(image1Path)
9    image2 = cv2.imread(image2Path)
10
11   # resize the two images to make them of the same dimensions. This is a must to subtract two images
12   resizedImage1 = cv2.resize(image1,(int(500*image1.shape[1]/image1.shape[0]), 500),interpolation=cv2.INTER_AREA)
13   resizedImage2 = cv2.resize(image2,(int(500*image2.shape[1]/image2.shape[0]), 500),interpolation=cv2.INTER_AREA)
14
15   cv2.imshow("Cat 1", resizedImage1)
16   cv2.imshow("Cat 2", resizedImage2)
17
18   # Subtract image 1 from 2
19   cv2.imshow("Diff Cat1 and Cat2",cv2.subtract(resizedImage2, resizedImage1))
20   cv2.waitKey(0)
21
22
23   # subtract images 2 from 1
24   subtractedImage = cv2.subtract(resizedImage1, resizedImage2)
25   cv2.imshow("Cat2 subtracted from Cat1", subtractedImage)
26   cv2.waitKey(0)
27
28   # Numpy Subtraction Cat2 from Cat1
29   subtractedImage2 = resizedImage2 - resizedImage1
30   cv2.imshow("Numpy Subracts Images", subtractedImage2)
31   cv2.waitKey(0)
32
33   # A constant subtraction
34   subtractedImage3 = resizedImage1 - 50
35   cv2.imshow("Constant Subtracted from the image", subtractedImage3)
36   cv2.waitKey(0)

Listing 3-7Image Subtraction

清单 3-7 展示了一些有趣的图像减法行为。这是我们在清单中的内容。

第 5 行到第 9 行是我们从磁盘加载图像的地方(从目录路径)。我们正在加载两只猫的图像,我们试图确定这两只看起来很像的猫是否有什么不同。图 3-17 和 3-18 所示的图像是本例中使用的输入图像。

第 12 行和第 13 行用于调整图像的大小,以确保它们的尺寸相同。请记住,这是必须减去两个图像数组。

在第 19 行,我们显示了从cat2.中减去cat1的结果。为了确定差异,我们使用了 OpenCV 的cv2.subtract()函数,并传递了两幅图像的 NumPy 表示(调整了大小的图像)。在这种情况下,我们要从cat2中减去cat1;因此,我们首先传递resizedImage2变量,然后将resizedImage1作为函数中的第二个参数。从图 3-19 和图 3-20 所示的输出中可以明显看出,顺序很重要。

为了演示顺序的效果,第 24 行在cv2.subtract()函数中将resizedImage1放在第一位,将resizedImage2作为第二个参数。

第 29 行没有使用 OpenCV 的减法函数。这是一个简单的 NumPy 数组减法。注意图 3-21 中显示的输出差异。

第 34 行从图像中减去一个常数。输出如图 3-22 所示。

img/493065_1_En_3_Fig22_HTML.jpg

图 3-22

从图像中减去的常数

img/493065_1_En_3_Fig21_HTML.jpg

图 3-21

数字减法

img/493065_1_En_3_Fig20_HTML.jpg

图 3-20

从图像 1 中减去图像 2

img/493065_1_En_3_Fig19_HTML.jpg

图 3-19

从图像 2 中减去图像 1

img/493065_1_En_3_Fig18_HTML.jpg

图 3-18

Cat2 图像

img/493065_1_En_3_Fig17_HTML.jpg

图 3-17

第一类图像

到目前为止,我们已经学习了两种强大的图像算术技术:加法和减法。现在让我们学习如何对图像像素执行按位逻辑运算。

位运算

计算机视觉中一些最有用的运算是位运算,包括 AND、OR、NOT 和 XOR。

如果您还记得您的布尔代数类,这些位运算是二元运算,并且只处理像素的两种状态:开和关。在灰度图像中,像素可以是 0 到 255 之间的任何值。那么,我们把“开”叫做什么,把“关”叫做什么呢?在图像处理中,对于灰度二值图像,像素值 0 表示关闭,大于 0 的值表示打开。基于像素开或关的概念,我们将探索下面的位运算。

如果“a”和“b”都是 1,则两个操作数“a”和“b”的按位“与”结果为 1;否则,结果为 0。

在图像处理中,两个图像数组的按位 AND 运算计算元素间的合取。需要注意的是,两个数组的维数必须相等,才能执行按位 AND 运算。也可以对数组和标量执行按位 AND 运算。

OpenCV 提供了一个方便的函数cv2.bitwise_and(imageArray1, imageAyyar2)来执行按位 AND 运算。该函数将两个图像数组作为参数。清单 3-8 展示了按位 AND 运算。

运筹学

如果“a”和“b”中的一个或两个为 1,则两个操作数“a”和“b”的按位“或”运算结果为 1;否则,结果为 0。按位 OR 运算计算两个数组或一个数组和一个标量的元素析取。在 OpenCV 中,函数cv2.bitwise_or(imageArray1, imageArray2)计算两个输入数组的按位 OR。清单 3-8 显示了 OR 运算的一个工作示例。

按位 NOT 反转其操作数的位值。OpenCV 的cv2.bitwise_not(imageArray)函数只接受一个图像数组作为参数,对该图像执行按位非运算。参见清单 3-8 中的示例。

异或运算

如果两个操作数“a”和“b”都是 1,但不是,则两个操作数“a”和“b”的按位异或结果为 1;否则,结果为 0。OpenCV 提供了一个方便的函数cv2.bitwise_xor(imageArray1, imageArray2)来执行按位异或。同样,两个图像数组的维数必须相等。清单 3-8 展示了一个按位异或的工作示例。

下表总结了我们将用于各种图像处理需求的按位运算,例如遮罩:

|

操作员

|

用法

|

描述

| | --- | --- | --- | | 按位 AND | a 和 b | 在两个操作数的对应位都为 1 的每个位位置返回 1 | | 按位或 | a 还是 b | 在一个或两个操作数的对应位为 1 的每个位位置返回 1 | | 按位异或 | 就是 | 在每个位位置返回 1,其中任一操作数(但不是两个操作数)的对应位为 1 | | 按位非 | 一个也不 | 反转其操作数的位 |

让我们用清单 3-8 中的程序来理解这些位运算。我们将首先创建两个图像——一个圆形和一个正方形——并执行位运算来查看它们的效果。

Filename: Listing_3_8.py
1    import cv2
2    import numpy as np
3
4    # create a circle
5    circle = cv2.circle(np.zeros((200, 200, 3), dtype = "uint8"), (100,100), 90, (255,255,255), -1)
6    cv2.imshow("A white circle", circle)
7    cv2.waitKey(0)
8
9    # create a square
10   square = cv2.rectangle(np.zeros((200,200,3), dtype= "uint8"), (30,30), (170,170),(255,255,255), -1)
11   cv2.imshow("A white square", square)
12   cv2.waitKey(0)
13
14   #bitwise AND
15   bitwiseAnd = cv2.bitwise_and(square, circle)
16   cv2.imshow("AND Operation", bitwiseAnd)
17   cv2.waitKey(0)
18
19   #bitwise OR
20   bitwiseOr = cv2.bitwise_or(square, circle)
21   cv2.imshow("OR Operation", bitwiseOr)
22   cv2.waitKey(0)
23
24   #bitwise XOR
25   bitwiseXor = cv2.bitwise_xor(square, circle)
26   cv2.imshow("XOR Operation", bitwiseXor)
27   cv2.waitKey(0)
28
29   #bitwise NOT
30   bitwiseNot = cv2.bitwise_not(square)
31   cv2.imshow("NOT Operation", bitwiseNot)
32   cv2.waitKey(0)

Listing 3-8Bitwise Operations

让我们了解一下清单 3-8 中是怎么回事。

第 5 行在 200×200 画布的中心创建了一个白色圆圈。参见清单 2-5 了解如何在画布上画一个圆。

类似地,第 10 行在 200×200 的画布上绘制了一个白色正方形。参见清单 2-4 了解如何在画布上绘制矩形。

第 15 行显示了cv2.bitwise_and()函数的用法。该函数的参数是圆形和方形图像(由 NumPy 数组表示)。

类似地,第 20 行和第 25 行分别显示了cv2.bitwise_or()cv2.bitwise_xor()操作。

所有这三个用于 AND、or 和 XOR 的函数都需要两个数组来运算。

第 30 行显示了cv2.bitwise_not()函数,它只接受一个参数来计算按位 NOT。

图 3-23 至 3-28 显示了列表 3-8 的输出。

img/493065_1_En_3_Fig28_HTML.jpg

图 3-28

按位非

img/493065_1_En_3_Fig27_HTML.jpg

图 3-27

按位异或

img/493065_1_En_3_Fig26_HTML.jpg

图 3-26

按位或

img/493065_1_En_3_Fig25_HTML.jpg

图 3-25

按位 AND

img/493065_1_En_3_Fig24_HTML.jpg

图 3-24

白色正方形

img/493065_1_En_3_Fig23_HTML.jpg

图 3-23

白色圆圈

掩饰

遮蔽是计算机视觉中最强大的技术之一。遮罩是指图像的“隐藏”或“过滤”。

当我们屏蔽一幅图像时,我们用其他图像隐藏了图像的一部分。换句话说,我们通过在图像的剩余部分应用遮罩来将焦点放在图像的一部分上。例如,图 3-29 中有数字 1、2 和 3,而图 3-30 是带有白色剪裁的黑色图像。当我们混合这两个图像时,数字 1 和 3 将被隐藏,唯一可见的数字是数字 2。屏蔽的结果如下图 3-31 所示。

img/493065_1_En_3_Fig31_HTML.png

图 3-31

掩蔽效应

img/493065_1_En_3_Fig30_HTML.png

图 3-30

遮罩影像

img/493065_1_En_3_Fig29_HTML.png

图 3-29

原象

掩蔽技术应用于图像的平滑或模糊以及检测图像内的边缘和轮廓。掩蔽技术也用于目标检测,我们将在本书后面探讨。

清单 3-9 展示了如何使用 OpenCV 执行屏蔽。

Filename: Listing_3_9.py
1    import cv2
2    import numpy as np
3
4    # Load an image
5    natureImage = cv2.imread("images/nature.jpg")
6    cv2.imshow("Original Nature Image", natureImage)
7
8    # Create a rectangular mask
9    maskImage = cv2.rectangle(np.zeros(natureImage.shape[:2], dtype="uint8"), (50, 50), (int(natureImage.shape[1])-50, int(natureImage.shape[0] / 2)-50), (255, 255, 255), -1)
10
11   cv2.imshow("Mask Image", maskImage)
12   cv2.waitKey(0)
13
14   # Using bitwise_and operation perform masking. Notice the mask=maskImage argument
15   masked = cv2.bitwise_and(natureImage, natureImage, mask=maskImage)
16   cv2.imshow("Masked image", masked)
17   cv2.waitKey(0)

Listing 3-9Masking Using Bitwise AND Operation

在 OpenCV 中,图像遮罩是通过使用按位 AND 运算来执行的(还记得按位运算吗?).清单 3-9 展示了一个简单的例子,展示了如何遮蔽图像的一个区域。对于这个例子,我们的目标是提取如图 3-32 所示的云的矩形部分。

清单 3-9 的第 5 行你现在应该很熟悉了。我们在这里所做的就是加载图像(图 3-32 )。

在第 9 行,我们创建了一个黑色的画布,顶部有一个白色的矩形区域(有一些空白)。画布的大小与原始图像的大小相同。注意在图 3-33 中,较大的矩形在顶部有另一个矩形白色部分,该矩形的其余区域为黑色。

第 15 行是执行屏蔽的地方。注意,我们使用的是cv2.bitwise_and()函数,它带有两个强制参数,在本例中是原始图像本身和一个可选的屏蔽参数(mask=maskImage)。这里发生的事情是,这个函数计算图像与其自身的 AND 运算,并按照参数mask=maskImage.的指示应用遮罩。当 OpenCV 看到这个mask参数时,它将只检查遮罩(maskImage)数组中打开的那些像素。该屏蔽操作的输出如图 3-34 所示。

img/493065_1_En_3_Fig34_HTML.jpg

图 3-34

屏蔽图像

img/493065_1_En_3_Fig33_HTML.jpg

图 3-33

用于从图 3-32 中提取云的遮罩

img/493065_1_En_3_Fig32_HTML.jpg

图 3-32

要遮罩的原始图像

标记是计算机视觉中最常用的图像处理技术之一。我们将在机器学习和神经网络的后续章节中了解更多关于它的实际应用。

拆分和合并频道

回想一下第二章中的内容,一幅彩色图像由多个通道(R、G、B)组成。我们已经学习了如何访问这些通道,并将它们表示为 NumPy 数组。在这一节中,我们将学习如何分割这些通道,并将它们存储为单独的图像。OpenCV 提供了一个方便的函数split()来做到这一点。使用这个split()函数,我们可以将图像分割成相应的颜色分量。这里有一个工作代码示例来说明这一点。对于这个例子,我们将再次把我们的“自然”图像(如图 3-32 所示)分割成它的组成颜色。

在清单 3-10 中,第 5 行加载图像。第 8 行将图像分成三个部分,并将它们存储在单独的 NumPy 变量中(bgr)。回想一下,NumPy 以蓝色、绿色和红色(BGR)序列存储颜色,而不是以 RGB 序列存储颜色。第 11、14 和 17 行显示了这些分割图像。输出如图 3-35 、 3-36 和 3-37 所示。

img/493065_1_En_3_Fig37_HTML.jpg

图 3-37

蓝色信道

img/493065_1_En_3_Fig36_HTML.jpg

图 3-36

绿色通道

img/493065_1_En_3_Fig35_HTML.png

图 3-35

红色通道

Filename: Listing_3_10.py
1    import cv2
2    import numpy as np
3
4    # Load the image
5    natureImage = cv2.imread("images/nature.jpg")
6
7    # Split the image into component colors
8    (b,g,r) = cv2.split(natureImage)
9
10   # show the blue image
11   cv2.imshow("Blue Image", b)
12
13   # Show the green image
14   cv2.imshow("Green image", g)
15
16   # Show the red image
17   cv2.imshow("Red image", r)
18
19   cv2.waitKey(0)

Listing 3-10Splitting Channels into Color Components

我们可以通过使用 OpenCV 的merge()函数来合并通道,该函数采用 BGR 序列中的数组。清单 3-11 显示了merge()函数的用法。

Filename: Listing_3_11.py
1    import cv2
2    import numpy as np
3
4    # Load the image
5    natureImage = cv2.imread("images/nature.jpg")
6
7    # Split the image into component colors
8    (b,g,r) = cv2.split(natureImage)
9
10   # show the blue image
11   cv2.imshow("Blue Image", b)
12
13   # Show the green image
14   cv2.imshow("Green image", g)
15
16   # Show the red image
17   cv2.imshow("Red image", r)
18
19   merged = cv2.merge([b,g,r])
20   cv2.imshow("Merged Image", merged)
21   cv2.waitKey(0)

Listing 3-11Split and Merge Functions

第 5 行加载图像。第 8 行到第 17 行与我们之前的 split 函数相关。我们进行了拆分,因此我们有三个组件来演示merge()功能。

第 19 行是我们合并频道的地方。我们简单地将单个通道作为参数传递给merge()函数。请注意,通道是按 BGR 顺序排列的。执行前面的程序,观察最终输出。你拿到原始图像了吗?

分割和合并是有助于为机器学习执行特征工程的图像处理技术。我们将在接下来的章节中应用其中的一些概念。

使用平滑和模糊减少噪声

平滑,也称为模糊,是一种重要的图像处理技术,用于减少图像中存在的噪声。我们在图像中通常会遇到以下类型的噪声:

  • 椒盐噪声:包含随机出现的黑白像素

  • 脉冲噪声:白色像素随机出现

  • 高斯噪声:强度变化遵循高斯正态分布

在本节中,我们将探讨以下用于降噪的模糊/平滑技术。

均值滤波或平均

在平均技术中,我们取图像的一小部分,比如说 k×k 个像素。图像的这一小部分被称为滑动窗口。我们从左到右和从上到下移动这个滑动窗口。这个 k×k 矩阵中心的像素被它周围所有像素的平均值所取代。这个 k×k 矩阵也被称为卷积核或者简称为。典型地,该内核被视为奇数,因此可以计算出明确的中心。内核越大,图像越模糊。例如,与 3×3 内核相比,5×5 内核将产生更模糊的图像。

OpenCV 提供了一个方便的函数来模糊图像。函数cv2.blur()用于通过使用均值滤波或平均技术来模糊图像。这个函数有两个参数。

  • 需要模糊的原始图像的数字表示

  • k×k 核矩阵

清单 3-12 显示了使用不同内核大小的图像模糊。

Filename: Listing_3_12.py
1    import cv2
2    import numpy as np
3
4    # Load the image
5    park = cv2.imread("images/nature.jpg")
6    cv2.imshow("Original Park Image", park)
7
8    #Define the kernel
9    kernel = (3,3)
10   blurred3x3 = cv2.blur(park,karnal)
11   cv2.imshow("3x3 Blurred Image", blurred3x3)
12
13   blurred5x5 = cv2.blur(park,(5,5))
14   cv2.imshow("5x5 Blurred Image", blurred5x5)
15
16   blurred7x7 = cv2.blur(park, (7,7))
17   cv2.imshow("7x7 Blurred Image", blurred7x7)
18   cv2.waitKey(0)

Listing 3-12Smoothing/Blurring by Mean Filtering or Averaging

像往常一样,我们首先加载图像并将其赋给一个数组变量(清单 3-12 中第 5 行的park变量)。

第 9 行定义了一个 3×3 内核。

在第 10 行,我们使用了cv2.blur()函数,并将park图像和kernel作为参数传递。这将产生一个使用 3×3 内核的模糊图像。

为了比较内核大小的影响,第 13 行和第 16 行使用了 5×5 和 7×7 的内核大小。注意在图 3-38 到 3-41 中,随着内核大小的增加,模糊度的增加顺序。

img/493065_1_En_3_Fig41_HTML.jpg

图 3-41

使用 7×7 内核模糊

img/493065_1_En_3_Fig40_HTML.jpg

图 3-40

使用 5×5 内核模糊

img/493065_1_En_3_Fig39_HTML.jpg

图 3-39

使用 3×3 内核进行模糊处理

img/493065_1_En_3_Fig38_HTML.jpg

图 3-38

原象

高斯滤波

高斯滤波是图像处理中最有效的模糊技术之一。这用于减少高斯噪声。与平均技术相比,这种模糊技术给出了更自然的平滑结果。在这个过滤过程中,我们提供了一个高斯核,而不是一个装箱的固定核。

高斯核由 X 和 Y 方向上的高度、宽度和标准偏差组成。

OpenCV 提供了一个方便的函数cv2.GaussianBlur()来执行高斯滤波。此函数cv2.GaussianBlur()采用以下参数:

  • NumPy 数组表示的图像。

  • k×k 矩阵作为内核的高度和宽度。

  • sigmaXsigmaY是 X 和 Y 方向的标准差。

以下是关于标准差的几点说明:

  • 如果仅指定了sigmaX,则sigmaYsigmaX.相同

  • 如果两者都为零,则根据内核大小计算标准偏差。

  • OpenCV 提供了一个函数cv2.getGaussianKernel(),用于自动计算标准偏差。

对于那些有兴趣知道高斯滤波中使用的公式的人,这里是高斯方程:

{G}_0\left(x,y\right)={Ae}^{\frac{-{\left(x-{\mu}_x\right)}²}{2{\sigma}_x²}+\frac{-{\left(y-{\mu}_y\right)}²}{2{\sigma}_y²}}

其中 μ 是平均值(峰值),而σ2 是方差(对于变量xy中的每一个)。

清单 3-13 是一个演示高斯模糊的工作示例。

Filename: Listing_3_13.py
1    import cv2
2    import numpy as np
3
4    # Load the park image
5    parkImage = cv2.imread("images/park.jpg")
6    cv2.imshow("Original Image", parkImage)
7
8    # Gaussian blurring with 3x3 kernel and 0 for standard deviation to calculate from the kernel
9    GaussianFiltered = cv2.GaussianBlur(parkImage, (5,5), 0)
10   cv2.imshow("Gaussian Blurred Image", GaussianFiltered)
11
12   cv2.waitKey(0)

Listing 3-13Smoothing Using the Gaussian Technique

这里我们再次开始加载我们的park图像(清单 3-13 的第 5 行)。第 9 行展示了 OpenCV 的cv2.GaussianBlur()函数的用法。我们提供了一个 5×5 内核和一个 0 来告诉 OpenCV 计算内核大小的标准偏差。

图 3-42 为原图,图 3-43 为高斯模糊效果。

img/493065_1_En_3_Fig43_HTML.jpg

图 3-43

5×5 核高斯模糊图像

img/493065_1_En_3_Fig42_HTML.jpg

图 3-42

原象

中间模糊

中值模糊是减少椒盐噪声的有效技术。中值模糊类似于均值模糊,只是内核的中心值被周围像素的中值所取代。我们使用 OpenCV 的cv2.medianBlur()函数来减少椒盐噪声(参见清单 3-14 )。该函数采用以下两个参数:

  • 需要模糊的原始图像。

  • 内核大小 k. 注意,在均值模糊的情况下,内核大小 k 类似于 k×k 矩阵。

Filename: Listing_3_14.py
1    import cv2
2
3    # Load a noisy image
4    saltpepperImage = cv2.imread("images/salt-pepper.jpg")
5    cv2.imshow("Original noisy image", saltpepperImage)
6
7    # Median filtering for noise reduction
8    blurredImage3 = cv2.medianBlur(saltpepperImage, 3)
9    cv2.imshow("Blurred image 3", blurredImage3)
10
11   # Median filtering for noise reduction
12   blurredImage5 = cv2.medianBlur(saltpepperImage, 5)
13   cv2.imshow("Blurred image 5", blurredImage5)
14
15
16   cv2.waitKey(0)

Listing 3-14Salt-and-Pepper Noise Reduction Using Median Blurring

清单 3-14 显示了cv2.medianBlur()功能的使用。第 8 行和第 12 行从第 4 行加载的原始图像创建模糊图像。注意,函数的kernel参数是一个标量,而不是元组或矩阵。

图 3-44 显示了带有椒盐噪声的图像。请注意,当我们应用不同的内核大小时,噪声降低的程度是不同的。图 3-45 显示了应用内核大小 3 时的输出图像。注意图 3-45 仍然有一些噪声。图 3-45 显示了当内核大小为 5 且应用了中值模糊时,几乎没有噪声的清晰输出。

img/493065_1_En_3_Fig46_HTML.jpg

图 3-46

内核大小为 5 的中值模糊(噪声几乎被移除)

img/493065_1_En_3_Fig45_HTML.jpg

图 3-45

内核大小为 3 的中值模糊(有一些噪点)

img/493065_1_En_3_Fig44_HTML.jpg

图 3-44

椒盐噪声图像

图 3-44 显示了带有椒盐噪声的噪声图像。你会注意到中值模糊在减少噪点方面做得相当不错。图 3-45 显示了使用核大小为 3 的模糊图像。如图 3-46 所示,核大小为 5 时效果较好。

双侧模糊

前三种模糊技术产生模糊的图像,其副作用是我们丢失了图像的边缘。为了在保留边缘的同时模糊图像,我们使用双边模糊,这是高斯模糊的增强。双边模糊需要两个高斯分布来执行计算。

第一个高斯函数考虑空间邻居(x 和 y 空间中靠得很近的像素)。第二个高斯函数考虑相邻像素的像素强度。这确保了只有那些与中心像素具有相似亮度的像素被考虑用于模糊,而边缘保持完整,因为与其他像素相比,边缘往往具有更高的亮度。

虽然这是一种优越的模糊技术,但与其他技术相比,它的速度较慢。

我们使用cv2.bilateralFilter()来执行这种模糊。该函数的参数如下:

  • 需要模糊的图像。

  • 像素邻域的直径。

  • 颜色值。较大的颜色值意味着在计算模糊度时将考虑更多的邻域像素颜色。

  • 空间或距离。较大的空间值意味着将考虑远离中心像素的像素。

让我们检查清单 3-15 来理解双边过滤。

Filename: Listing_3_15.py
1    import cv2
2
3    # Load a noisy image
4    noisyImage = cv2.imread("images/nature.jpg")
5    cv2.imshow("Original image", noisyImage)
6
7    # Bilateral Filter with
8    fileteredImag5 = cv2.bilateralFilter(noisyImage, 5, 150,50)
9    cv2.imshow("Blurred image 5", fileteredImag5)
10
11   # Bilateral blurring with kernal 7
12   fileteredImag7 = cv2.bilateralFilter(noisyImage, 7, 160,60)
13   cv2.imshow("Blurred image 7", fileteredImag7)
14
15   cv2.waitKey(0)

Listing 3-15Bilateral Blurring Example

如清单 3-15 所示,第 8 行和第 12 行用于使用cv2.bilateralFilter()模糊输入图像。第一组参数(第 8 行)是用 NumPy 表示的图像像素、内核或直径、颜色阈值和距中心的距离。

图 3-47 至 3-49 显示了列表 3-15 的输出。

img/493065_1_En_3_Fig49_HTML.jpg

图 3-49

直径为 7 的双侧模糊

img/493065_1_En_3_Fig48_HTML.jpg

图 3-48

直径为 5 的双边模糊

img/493065_1_En_3_Fig47_HTML.jpg

图 3-47

原象

我们已经学习了模糊或平滑图像的不同技术。我们将在整本书中使用这些模糊技术。

在下一节中,我们将学习如何借助一种叫做阈值的技术将灰度图像转换成二进制图像。

阈值二值化

图像二值化是将灰度图像转换为二值图像(黑白图像)的过程。我们应用一种叫做阈值的技术对图像进行二值化。

我们首先决定一个阈值。大于该阈值的像素值被更改为 255,小于该阈值的像素值被设置为 0。生成的图像将只有两个像素值,即 0 和 255,它们是黑白颜色值。因此,灰度图像被转换成黑白图像(也称为二进制 ima ge)。

二值化技术用于从图像中提取重要信息,例如,从扫描的文档中提取光学字符识别(OCR)中的字符。

OpenCV 支持以下类型的阈值技术。

简单阈值处理

在简单的阈值分割中,我们手动选择一个阈值, T. 所有大于这个 T 的像素被设置为 255,所有小于等于 T 的像素被设置为 0。

有时,进行二进制化的逆过程会很有帮助,在这种情况下,大于阈值的像素被设置为 0,小于阈值的像素被设置为 255。

让我们看一个如何使用 OpenCV 的cv2.threshold()函数对图像进行二值化的例子。该函数采用以下参数:

  • 需要二值化的原始灰度图像

  • 阈值 T

  • 像素值大于阈值时将设置的最大值

  • 阈值方法,如cv2.THRESH_BINARYcv2.THRESH_BINARY_INV

threshold 函数返回包含阈值和二值化图像的元组。

清单 3-16 将灰度图像转换为二值图像。

Filename: Listing_3_16.py
1    import cv2
2    import numpy as np
3
4    # Load an image
5    image = cv2.imread("images/scanned_doc.png")
6    # convert the image to grayscale
7    image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
8    cv2.imshow("Original Grayscale Receipt", image)
9
10   # Binarize the image using thresholding
11   (T, binarizedImage) = cv2.threshold(image, 60, 255, cv2.THRESH_BINARY)
12   cv2.imshow("Binarized Receipt", binarizedImage)
13
14   # Binarization with inverse thresholding
15   (Ti, inverseBinarizedImage) = cv2.threshold(image, 60, 255, cv2.THRESH_BINARY_INV)
16   cv2.imshow("Inverse Binarized Receipt", inverseBinarizedImage)
17   cv2.waitKey(0)

Listing 3-16Binarization Using Simple Thresholding

清单 3-16 显示了两种二值化方法:简单二值化和逆二值化。第 5 行加载图像,第 8 行将图像转换为灰度图像,因为阈值函数的输入应该是灰度图像。

第 11 行调用 OpenCV 的cv2.threshold()函数,并将灰度图像、阈值、最大像素值和阈值方法cv2.THRESH_BINARY.作为参数传递。threshold()函数返回一个元组,其中包含我们在参数和二值化图像中提供的相同阈值。在前面的示例中,对于值大于 60 的所有像素,像素值将被设置为最大值 255,对于值等于或小于 60 的像素,像素值将被设置为 0。

第 15 行类似于第 11 行,除了threshold()函数的最后一个参数是cv2.THRESH_BINARY_INV.通过传递cv2.THRESH_BINARY_INV,我们指示threshold()方法做与cv2.THRESH_BINARY方法相反的事情:如果像素强度小于 60,将像素值设置为 255;否则,将其设置为 0。

图 3-50 至 3-52 显示了两种阈值方法的样本输出以及原始图像。

img/493065_1_En_3_Fig52_HTML.jpg

图 3-52

简单逆阈值二值化图像

img/493065_1_En_3_Fig51_HTML.jpg

图 3-51

简单阈值二值化图像

img/493065_1_En_3_Fig50_HTML.jpg

图 3-50

带有深色背景补丁/污点的原始灰度图像

为了演示这个例子,我们拍摄了一张污迹斑斑的文档的扫描图像(图 3-50 ,并使用简单的阈值处理将其二值化。方法cv2.THRESH_BINARY生成了输出,其中包含白色背景上的黑色文本。方法cv2.THRESH_BINARY_INV用黑色背景上的白色文本创建了图像。

在简单的阈值处理中,一个全局阈值应用于图像中的所有像素,您需要预先知道阈值。如果您正在处理大量图像,并且希望根据图像类型和强度变化来调整阈值,简单阈值可能不是理想的方法。

在下面的部分中,我们将检查其他阈值方法:自适应阈值和 Otsu 方法。

自适应阈值

自适应阈值处理用于二值化具有不同程度的像素强度的灰度图像,并且一个单一的阈值可能不适于从图像中提取信息。在自适应阈值处理中,该算法基于像素周围的小区域来确定像素的阈值。这将为同一幅图像中的不同区域提供不同的阈值。当像素强度在图像内变化时,自适应阈值处理与简单的阈值处理相比往往给出更好的结果。

清单 3-17 显示了使用自适应阈值将灰度图像二值化。

Filename: Listing_3_17.py
1    import cv2
2    import numpy as np
3
4    # Load an image
5    image = cv2.imread("images/boat.jpg")
6    # convert the image to grayscale
7    image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
8
9    cv2.imshow("Original Grayscale Image", image)
10
11   # Binarization using adaptive thresholding and simple mean
12   binarized = cv2.adaptiveThreshold(image, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 7, 3)
13   cv2.imshow("Binarized Image with Simple Mean", binarized)
14
15   # Binarization using adaptive thresholding and Gaussian Mean
16   binarized = cv2.adaptiveThreshold(image, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 3)
17   cv2.imshow("Binarized Image with Gaussian Mean", binarized)
18
19   cv2.waitKey(0)

Listing 3-17Binarization Using Adaptive Thresholding

我们使用了一个具有不同程度阴影和颜色强度的示例图像。使用自适应阈值,我们希望将图像转换为二值图像。下面是对清单 3-17 中发生的事情的解释。

像往常一样,第 5 行加载图像。第 7 行将图像转换为灰度图像,因为阈值函数的输入是灰度图像。

第 12 行实际上是使用 OpenCV 的cv2.adaptiveThreshold()函数执行二值化。该函数采用以下参数:

  • 需要二值化的灰度图像

  • 最大值

  • 计算阈值的方法(稍后将提供更多信息)

  • 二值化方法,如cv2.THRESH_BINARYcv2.THRESH_BINARY_INV

  • 计算阈值时要考虑的邻域大小

  • 将从计算的阈值中减去的常数值 C

在我们的例子中,在第 12 行,我们使用了cv2.ADAPTIVE_THRESH_MEAN_C来表示我们想要通过取周围像素的平均值来计算像素的阈值。在我们的例子中,邻域的大小是 7×7。第 12 行的最后一个参数 3 是将从计算的阈值中减去的常数。

第 16 行类似于第 12 行,除了我们使用cv2.ADAPTIVE_GAUSSIAN_C来表示我们想要通过取一个像素周围所有像素的加权平均值来计算该像素的阈值。

图 3-53 至 3-55 显示了清单 3-17 的部分输出样本。

img/493065_1_En_3_Fig55_HTML.jpg

图 3-55

高斯均值自适应阈值二值化图像

img/493065_1_En_3_Fig54_HTML.jpg

图 3-54

简单均值自适应阈值二值化图像

img/493065_1_En_3_Fig53_HTML.jpg

图 3-53

原象

大津二值化

在简单的阈值处理中,我们选择一个任意选择的全局阈值。很难知道阈值的正确值是多少,所以我们可能需要做几次试错实验,才能得到正确的值。即使您获得了一种情况下的理想值,它也可能不适用于具有不同像素强度特征的其他图像。

Otsu 的方法从图像直方图中确定最佳全局阈值。我们将在下一章学习更多关于直方图的知识。现在,就把直方图想象成像素值的频率分布。

为了执行 Otsu 的二进制化,我们在cv2.threshold()函数中传递cv2.THRESH_OTSU作为额外的标志。例如,我们在threshold()函数中传递cv2.THRESH_BINARY+cv2.THRESH_OTSU来指示使用 Otsu 的方法。threshold()方法需要一个阈值。当使用 Otsu 的方法时,我们传递一个任意值(可能是 0),算法自动计算阈值并作为输出之一返回。

清单 3-18 显示了如何使用 Otsu 的二进制化方法的代码示例。

Filename: Listing_3_18.py
1    import cv2
2    import numpy as np
3
4    # Load an image
5    image = cv2.imread("images/scanned_doc.png")
6    # convert the image to grayscale
7    image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
8    cv2.imshow("Original Grayscale Receipt", image)
9
10   # Binarize the image using thresholding
11   (T, binarizedImage) = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)
12   print("Threshold value with Otsu binarization", T)
13   cv2.imshow("Binarized Receipt", binarizedImage)
14
15   # Binarization with inverse thresholding
16   (Ti, inverseBinarizedImage) = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
17   cv2.imshow("Inverse Binarized Receipt", inverseBinarizedImage)
18   print("Threshold value with Otsu inverse binazarion", Ti)
19   cv2.waitKey(0)

Listing 3-18Otsu’s Binarization

你会注意到清单 3-18 中的代码示例与清单 3-16 中的代码几乎相同,除了以下例外:

  • 第 11 行使用一个附加标志cv2.THRESH_OTSUcv2.THRESH_BINARY,阈值作为 0 传递。

  • 第 16 行使用标志cv2.THRESH_OTSUcv2.THRESH_BINARY_INV,并且阈值再次被设置为 0。

  • 我们在第 12 行和第 18 行使用了print语句来打印计算出的阈值。图 3-56 显示了这些print语句的样本输出。

图 3-57 到 3-59 显示了 Otsu 的输出样本。

img/493065_1_En_3_Fig59_HTML.jpg

图 3-59

用 Otsu 法进行反二值化

img/493065_1_En_3_Fig58_HTML.jpg

图 3-58

用 Otsu 法进行二值化

img/493065_1_En_3_Fig57_HTML.jpg

图 3-57

具有不同背景阴影(污点和暗斑)的原始图像

img/493065_1_En_3_Fig56_HTML.jpg

图 3-56

根据 Otsu 方法计算的阈值输出示例

二值化是从图像中提取显著特征的一种有用的图像处理技术。在这一节中,我们已经学习了不同的二值化技术,以及它们基于像素强度及其变化的用法。在接下来的部分,我们将学习另一种强大的图像处理技术,称为边缘检测

梯度和边缘检测

边缘检测涉及一组方法来寻找图像中像素亮度明显变化的点。

我们将学习两种在图像中寻找边缘的方法:寻找梯度和 Canny 边缘检测。

OpenCV 提供了以下两种寻找渐变的方法。

索贝尔衍生物(cv2。Sobel()函数)

Sobel 方法是高斯平滑和 Sobel 微分的组合,其计算图像强度函数的梯度的近似值。由于高斯平滑,这种方法是抗噪声的。

通过分别传递参数xorderyorder,我们可以在水平或垂直方向上进行求导。Sobel()函数还有一个参数ksize,我们用它来定义内核的大小。如果我们将ksize设置为-1,OpenCV 将在内部应用一个 3×3 的 Schar 过滤器,与 3×3 的 Sobel 过滤器相比,它通常会给出更好的结果。

我们将在清单 3-19 中看到 Sobel 函数的运行。

Filename: Listing_3_19.py
1    import cv2
2    import numpy as np
3    # Load an image
4    image = cv2.imread("images/sudoku.jpg")
5    cv2.imshow("Original Image", image)
6    image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
7    image = cv2.bilateralFilter(image, 5, 50, 50)
8    cv2.imshow("Blurred image", image)
9
10   # Sobel gradient detection
11   sobelx = cv2.Sobel(image,cv2.CV_64F,1,0,ksize=3)
12   sobelx = np.uint8(np.absolute(sobelx))
13   sobely = cv2.Sobel(image,cv2.CV_64F,0,1,ksize=3)
14   sobely = np.uint8(np.absolute(sobely))
15
16   cv2.imshow("Sobel X", sobelx)
17   cv2.imshow("Sobel Y", sobely)
18
19   # Schar gradient detection by passing ksize = -1 to Sobel function
20   scharx = cv2.Sobel(image,cv2.CV_64F,1,0,ksize=-1)
21   scharx = np.uint8(np.absolute(scharx))
22   schary = cv2.Sobel(image,cv2.CV_64F,0,1,ksize=-1)
23   schary = np.uint8(np.absolute(schary))
24   cv2.imshow("Schar X", scharx)
25   cv2.imshow("Schar Y", schary)
26
27   cv2.waitKey(0)

Listing 3-19Sobel and Schar Gradient Detection

这里发生了很多事情。因此,让我们通过仔细阅读代码清单来理解渐变的概念。

第 4 行只是从磁盘加载一个图像。我们应用双边滤波器来减少第 7 行中的噪声。图 3-60 显示原始输入图像,图 3-61 显示用作 Sobel 和 Schar 梯度检测功能输入的模糊图像。

梯度检测从第 11 行开始。我们使用了接受以下参数的cv2.Sobel()函数:

  • 我们想要检测渐变的模糊图像。

  • 一种数据类型,cv2.CV_64F,它是一种 64 位浮点数。为什么?从黑到白的过渡被认为是正斜率,而从白到黑的过渡是负斜率。8 位无符号整数不能保存负数。因此,我们需要使用 64 位浮点数;否则,当从白色到黑色的过渡发生时,我们将丢失渐变。

  • 第三个参数表示我们是否要计算 X 方向的梯度。值 1 意味着我们要计算 X 方向的梯度。

  • 类似地,第四个参数指示是否计算 Y 方向的梯度。1 表示是,0 表示不是。

  • 第五个参数,ksize,定义了内核大小。ksize=5表示内核大小为 5×5。

由于我们想要确定第 11 行 X 方向的梯度,我们将函数cv2.Sobel()中的第三个参数设置为 1,并将第四个参数设置为 0。

第 12 行简单地获取梯度的绝对值,并将其转换回 8 位无符号整数。请记住,图像被表示为一个 8 位无符号整数 NumPy 数组。

第 13 行类似于第 11 行,只是第三个参数设置为 0,第四个参数设置为 1,以指示 Y 方向的梯度计算。

如前所述,第 14 行将 64 位浮点数转换为 8 位无符号整数。

图 3-62 和图 3-63 显示了 16 和 17 线的样本输出。您会注意到,X 和 Y 方向上的边缘检测都不是很清晰。让我们尝试一个简单的改进,看看对边缘清晰度的影响。

img/493065_1_En_3_Fig63_HTML.jpg

图 3-63

Y 方向上的 Sobel 边缘检测

img/493065_1_En_3_Fig62_HTML.jpg

图 3-62

X 方向上的 Sobel 边缘检测

img/493065_1_En_3_Fig61_HTML.jpg

图 3-61

模糊图象

img/493065_1_En_3_Fig60_HTML.jpg

图 3-60

原象

第 20 至 23 行与清单 3-19 的第 11 至 14 行相似。不同的是ksize的值是-1,它指示 OpenCV 内部调用内核大小为 3×3 的 Schar 函数。你会注意到,与 Sobel 函数相比,边缘的清晰度要好得多。图 3-64 和图 3-65 是图 3-61 所示图像的沙尔滤波结果。

img/493065_1_En_3_Fig65_HTML.jpg

图 3-65

X 方向上的沙尔边缘检测

img/493065_1_En_3_Fig64_HTML.jpg

图 3-64

X 方向上的沙尔边缘检测

Sobel 和 Schar 计算沿 X 和 Y 方向的梯度幅度,允许我们确定沿水平和垂直方向的边缘。

拉普拉斯导数(cv2。拉普拉斯()函数)

拉普拉斯算子计算像素强度函数的二阶导数,以确定图像中的边缘。拉普拉斯算子基于以下等式计算梯度:

\mathrm{Laplace}\left(\mathrm{f}\right)=\frac{\partial²\mathrm{f}}{\partial {x}²}+\frac{\partial²\mathrm{f}}{\partial {y}²}

OpenCV 提供了一个函数cv2.Laplacian(),用于计算边缘检测的梯度。该函数采用以下参数:

  • 需要检测边缘的图像

  • 数据类型,通常是用来保存浮点值的cv2.CV_64F

清单 3-20 显示了使用 OpenCV 的拉普拉斯函数进行边缘检测的工作示例。

像往常一样,第 5 行加载图像,第 6 行将图像转换为灰度,第 8 行使用双边过滤模糊图像。

第 12 行是调用cv2.Laplacian()函数进行梯度计算以检测图像边缘的地方。同样,当从白色到黑色的过渡发生时,我们传递了CV_64F数据类型来保存梯度的可能负值。

第 13 行将 64 位浮点数转换为 8 位无符号整数。

Filename: Listing_3_20.py
1    import cv2
2    import numpy as np
3
4    # Load an image
5    image = cv2.imread("images/sudoku.jpg")
6    image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
7
8    image = cv2.bilateralFilter(image, 5, 50, 50)
9    cv2.imshow("Blurred image", image)
10
11   # Laplace function for edge detection
12   laplace = cv2.Laplacian(image,cv2.CV_64F)
13   laplace = np.uint8(np.absolute(laplace))
14
15   cv2.imshow("Laplacian Edges", laplace)
16
17   cv2.waitKey(0)

Listing 3-20Edge Detection Using Laplacian Derivatives

图 3-66 显示了Laplacian()功能的示例显示。

img/493065_1_En_3_Fig66_HTML.jpg

图 3-66

使用拉普拉斯导数的边缘检测

Canny 边缘检测

Canny 边缘检测是图像处理中最流行的边缘检测方法之一。这是一个多步骤的过程。它首先模糊图像以减少噪声,然后计算 X 和 Y 方向上的 Sobel 梯度,抑制计算非最大值的边缘,最后通过应用滞后阈值来确定像素是否是“边缘样的”。

OpenCV 的cv2.canny()函数将所有这些步骤封装成一个函数。让我们直接看代码,看一个使用 Canny 函数进行边缘检测的例子。参见清单 3-21 。

Filename: Listing_3_21.py
1    import cv2
2    import numpy as np
3
4    # Load an image
5    image = cv2.imread("images/sudoku.jpg")
6    image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
7    cv2.imshow("Blurred image", image)
8
9    # Canny function for edge detection
10   canny = cv2.Canny(image, 50, 170)
11   cv2.imshow("Canny Edges", canny)
12
13   cv2.waitKey(0)

Listing 3-21Canny Edge Detection

清单 3-21 中重要的一行是第 10 行,我们在这里调用cv2.Canny()函数,并将最小和最大阈值传递给需要检测边缘的图像。任何大于最大阈值的梯度值都被视为边缘。低于最小阈值的任何值都不被视为边缘。根据边缘的强度变化,考虑边缘之间的梯度值。

图 3-67 显示了 Canny 边缘检测器的样本输出。请注意,在这种情况下,边缘非常清晰。

img/493065_1_En_3_Fig67_HTML.jpg

图 3-67

Canny 边缘检测

轮廓

等高线是连接相同强度的连续点的曲线。确定轮廓对于物体识别、人脸检测和识别是有用的。

为了检测轮廓,我们执行以下操作:

  1. 将图像转换为灰度。

  2. 使用任何阈值方法将图像二值化。

  3. 应用 Canny 边缘检测方法。

  4. 使用findContours()方法找到图像中的所有轮廓。

  5. 最后,如果需要,使用drawContours()功能绘制轮廓。

我们将在清单 3-22 中看到轮廓检测和绘图。

Filename: Listing_3_22.py
1    import cv2
2    import numpy as np
3
4    # Load an image
5    image = cv2.imread("images/sudoku.jpg")
6    image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
7    cv2.imshow("Blurred image", image)
8
9    # Binarize the image
10   (T,binarized) = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
11   cv2.imshow("Binarized image", binarized)
12
13   # Canny function for edge detection
14   canny = cv2.Canny(binarized, 0, 255)
15   cv2.imshow("Canny Edges", canny)
16
17   (contours, hierarchy) = cv2.findContours(canny,cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
18   print("Number of contours determined are ", format(len(contours)))
19
20   copiedImage = image.copy()
21   cv2.drawContours(copiedImage, contours, -1, (0,255,0), 2)
22   cv2.imshow("Contours", copiedImage)
23   cv2.waitKey(0)

Listing 3-22Contour Detection and Drawing

下面是清单 3-22 的逐行解释。

第 5 行加载图像。第 6 行将图像转换为灰度,第 10 行使用 Otsu 的方法将图像二值化。第 14 行使用 Canny 函数计算边缘检测的梯度。

第 17 行调用 OpenCV 的cv2.findContours()函数来确定轮廓。该函数的参数如下:

  • 第一个参数是我们想要使用 Canny 函数检测边缘的图像。

  • 第二个参数cv2.RET_EXTERNAL,决定了我们感兴趣的轮廓类型。cv2.RET_EXTERNAL仅检索最外面的轮廓。我们也可以使用cv2.RET_LIST来检索所有轮廓,cv2.RET_COMP acv2.RET_TREE,以包括层次轮廓。

  • 第三个参数cv2.CHAIN_APPROAX_SIMPLE,去掉多余的点,压缩轮廓,从而节省内存。cv2.CHAIN_APPROAX_NONE存储轮廓的所有点(需要更多内存来存储)。

cv2.findContours()函数的输出是一个元组,其中包含以下项目:

  • 元组的第一项是图像中所有轮廓的 Python 列表。每个单独的轮廓是对象边界点的(x,y)坐标的 NumPy 数组。

  • 输出元组的第二项是轮廓层次。

注意第 18 行,这里我们打印了识别出的轮廓数。

绘制等高线

我们正在使用cv2.drawContours()功能绘制等高线(清单 3-22 的第 21 行)。以下是该函数的参数:

  • 第一个参数是要在其中绘制轮廓的图像。

  • 第二个参数是所有轮廓点的列表。

  • 第三个参数是要绘制的轮廓的索引。如果我们想画第一个轮廓,传递一个 0。同样,传递 1 来绘制第二个轮廓,以此类推。如果要绘制所有轮廓,请将-1 传递给该参数。

  • 第四个参数是轮廓的颜色。

  • 第五个也是最后一个参数是轮廓的厚度。

图 3-68 至 3-70 显示了清单 3-22 的部分输出样本。

img/493065_1_En_3_Fig70_HTML.jpg

图 3-70

在原始图像上绘制的轮廓

img/493065_1_En_3_Fig69_HTML.jpg

图 3-69

使用 Canny 函数的轮廓

img/493065_1_En_3_Fig68_HTML.jpg

图 3-68

模糊图象

摘要

在这一章中,我们探讨了对构建计算机视觉应用有用的各种图像处理技术。我们学习了各种图像变换的方法,如调整大小、旋转、翻转和裁剪。我们还学习了如何对图像进行算术和位运算。本章的后半部分介绍了一些强大而有用的图像处理功能,如遮罩、降噪、二值化、边缘和轮廓检测。

我们将在后面的章节中使用大多数图像处理技术,特别是当我们学习机器学习的特征提取和工程时。****