机器学习和图像处理实践指南-全-
机器学习和图像处理实践指南(全)
一、设置环境
在这一章中,我们准备我们的系统来运行本书中包含的代码。让我们看看如何安装以下内容:
-
蟒蛇
-
开放计算机视觉
-
硬
除了列表中的最后两个包,我们需要的大多数包都预装了 Anaconda。让我们从 Anaconda 开始,然后是 OpenCV 和 Keras。
安装 Anaconda
Anaconda 安装页面宣称它是“最流行的 Python 数据科学平台”使用 Anaconda、安装支持软件、设置虚拟环境等等都非常容易,并且该包附带了 Python 数据科学的最佳集成开发环境(ide)之一:Jupyter Notebook。Jupyter 不仅能帮助你编写 Python 代码,还能让你的代码看起来美观大方。那么,让我们开始安装 Anaconda。
Windows 操作系统
如果您使用的是 Windows,流程如下:
图 1-2
一个新的 Python 脚本
- 在“文件”标签的右上角,您会看到下拉菜单“新建”。单击向下箭头并选择 Python 3。现在你已经准备好编码了(图 1-2 )!
图 1-1
打开屏幕
-
去 www。蟒蛇。com 。
-
在屏幕的右上方,是下载按钮。点击它。
-
向下滚动,您将看到两个版本的 Anaconda:Python 3.7 版和 Python 2.7 版。在 Python 3.7 版本框中,选择 64 位图形安装程序(如果您的系统是 32 位系统,请选择 32 位选项)。
-
等待下载完成,然后双击安装文件。
-
完成安装并重启系统。
-
现在,打开开始菜单,搜索 Anaconda 提示符,并选择它。将出现一个名为 Anaconda 提示符的 shell。在 shell 中键入
Jupyter Notebook
,您将看到如图 1-1 所示的屏幕。
苹果
如果您使用的是 macOS,下面是 Anaconda 的安装过程:
-
为 macOS 下载 Anaconda,就像为 Windows 下载一样。
-
双击。pkg 文件并遵循安装过程。
-
打开你的终端,输入
Jupyter Notebook
。您将看到如图 1-1 所示的相同屏幕。
人的本质
在 Ubuntu 中下载 Anaconda 的过程如下:
-
像下载 Windows 一样下载 Anaconda for Linux。
-
转到安装文件夹并键入
bash Anaconda-latest-Linux-x86_64.sh
。 -
遵循安装程序,打开您的终端,并键入
Jupyter Notebook
。您将看到如图 1-1 所示的相同屏幕。
安装 OpenCV
现在我们已经安装了蟒蛇和 Jupyter 笔记本。接下来要做的是安装它的支持软件。对于 OpenCV,请执行以下操作:
-
打开 Anaconda 提示符。
-
类型
conda install -c conda-forge opencv
。 -
你也可以输入
conda install -c conda-forge/label/broken opencv
。 -
几分钟后,OpenCV 将安装在您的环境中。
硬安装
要安装 Keras,请按照下列步骤操作:
-
打开 Anaconda 提示符。
-
几分钟后,Keras 将安装在您的环境中。
Type conda install -c conda-forge keras.
测试安装
在进一步操作之前,您需要按如下方式测试安装:
-
开放 Jupyter 笔记型电脑。
-
打开一个新的 Python 3 笔记本。
-
类型
import cv2
。如果您没有收到错误,那么 OpenCV 已经完美安装。如果出现错误,要么是您在安装过程中做错了什么,要么可能存在兼容性问题。要进行纠正,要么重新启动安装过程,要么参考 OpenCV 文档页面。 -
类型
import keras
。如果您没有收到一个错误,那么 Keras 已经安装完美。如果出现错误,要么是您在安装过程中做错了什么,要么可能存在兼容性问题。要进行纠正,请重新启动安装过程,或者参考 Keras 文档页面。
虚拟环境
现在,我们已经安装了所需的软件,让我们来看看虚拟环境。当您想要开发多个项目时,虚拟环境非常重要。如果我们正在使用 Python 3 开发一个产品,但我们想使用 Python 2.7 创建另一个项目,我们该怎么办?如果直接做,可能会遇到问题,因为安装了不同版本的 Python。或者,我们可以创建一个虚拟环境,安装 Python 2.7,并在该环境中开发产品。无论您在虚拟环境中开发什么,它都不会影响环境外的任何代码。让我们看看如何创建虚拟环境:
-
类型
conda create -n environment_name python=version anaconda
。代替environment_name
,键入您希望为您的环境指定的任何名称。代替version
,输入您想要使用的任何 Python 版本(例如,2.7、3.5、3.6 等等)。 -
既然我们已经创建了环境,我们必须激活它。我们通过键入
source activate environment_name
来实现这一点。 -
我们现在可以打开 Jupyter 笔记本,开始在这种环境下工作。
-
要停用环境,请键入
source deactivate
。
二、图像处理导论
在这一章中,我们将研究什么是图像,以及它的相关属性。学完本章后,你应该对以下概念有所了解:
-
形象
-
像素
-
图像分辨率
-
每英寸像素(PPI)和每英寸点数(DPI)
-
位图图像
-
无损压缩和有损压缩
-
不同的图像文件格式
-
不同类型的色彩空间
-
高级图像概念
形象
现实生活中的物体(人或任何其他物体)以二维形式的视觉表现被称为图像。图像不过是不同色彩空间中像素的集合。图 2-1 是普通图像的一个例子。
图 2-1
正常图像
像素
你可能会认为一个完整的图像是由小样本组成的集合。这些样本被称为像素。它们是任何数字图像中最小的元素。你有没有把图像放大到可以看到小方块的程度?那些是像素。因此,像素是图像的子样本,当它们组合在一起时,给我们完整的图像。图 2-2 显示了不同颜色的像素看起来的样子。
图 2-2
各种颜色的像素(来源: www.freeimages.co.uk
)
图像分辨率
图像分辨率是图像中的像素数量。像素数量越多,质量越好。例如,图像分辨率描述为 320 × 240、640 × 480、800 × 600、1024 × 768 等。这意味着,例如,有 1024 个像素列和 768 个像素行。像素的总数是通过将这两个数字相乘得到的,得到 786,432 个像素。图 2-3 显示了不同图像分辨率的对比图。
图 2-3
对比图像分辨率(来源: www.freeimages.co.uk
)
PPI 和 DPI
正如本章开头所提到的,PPI 的意思是“每英寸的像素”,而 DPI 的意思是“每英寸的点数”它们是测量图像分辨率的单位。
如果我们考虑一英寸的图像,我们能够看到的像素的平方数由 PPI 表示。另一方面,DPI 与打印相关。当我们打印图像并查看一英寸的印刷品时,使用的墨水点数由 DPI 表示。
如图 2-4 所示,PPI 看起来更平滑,而 DPI 看起来更清晰。
图 2-4
PPI 和 DPI 表示法
位图图像
一般来说,当我们看像素值时,它们是一个整数范围。但是,当我们将整数范围转换成字节时,我们就有了位图图像。
一种位图是二进制图像,其中每个像素有两个数字:0 或 1。它们代表黑色或白色,通常用于高效存储图像。图 2-5 显示了一幅二进制位图图像。
图 2-5
图 2-1 的二进制位图表示
无损压缩
当我们想要减小文件(可以是图像)的大小,但不想牺牲质量时,这种压缩称为无损压缩。压缩文件可以保存,但是当我们需要它的时候,在解压缩过程中,所有的信息都被恢复,我们得到了实际的图像(图 2-6 )。第一种类型的压缩优先考虑文件中包含的信息,尤其是在压缩文本时,我们不能丢失哪怕是一条信息。
图 2-6
无损压缩过程
有损压缩
另一方面,有损压缩可能会丢失一些数据。有损压缩优先考虑节省空间,而不是检索文件的准确性。某些文件,如包含音乐或图像的文件,可以被修剪,但仍不受压缩的影响。可能会有一些损失,但并不令人担忧(图 2-7 )。
图 2-7
有损压缩过程
图像文件格式
以下是一些最广泛使用的图像格式,如表 2-1 所述:
表 2-1
不同图像类型的描述和使用
|图像格式
|
描述
|
使用
|
| --- | --- | --- |
| 联合图像专家组 | 原始图像的有损压缩 | 照片和绘画 |
| JPEG2000 | JPEG 的优化形式;更好的压缩比;无损和有损压缩 | 监督 |
| 争执 | 无损压缩;可以在不丢失信息的情况下存储和检索 | 文档存储 |
| GIF 格式 | 位图图像格式;支持动画;无损压缩 | 游戏和动画 |
| 位图文件的扩展名(Bitmap) | 独立于显示设备;缺乏压缩 | 在 Windows 中 |
| PNG | 无损数据压缩;支持不同的色彩空间 | 互联网上的图像传输 |
| WebP | 无损和有损压缩;尺寸小,但图像质量与 JPEG 相当 | 消息应用中的贴纸 |
| 挽救(saving 的简写) | 用于交互性和动画;以 XML 格式定义的行为和图像;它们可以被搜索、索引和压缩 | 网站开发 |
-
JPEG :联合图像专家组
-
JPEG 2000:2000 年开发的新 JPEG 格式
-
TIFF :标记图像文件格式
-
GIF :图形交换格式
-
BMP :位图
-
PNG :便携式网络图形
-
WebP :谷歌开发的格式
-
SVG :可缩放矢量图形
色彩空间
特定格式的图像中颜色的组织称为颜色空间。颜色的表现方式被称为颜色模型。每个图像都使用以下颜色空间之一来有效地呈现图片:
-
RGB :红色、绿色、蓝色
-
XYZ:x、y、z 维度的颜色
-
HSV/HSL :色调、饱和度和值/色调、饱和度和亮度
-
LAB :亮度,以及绿-红和蓝-黄颜色分量
-
LCH :亮度、色度和色调
-
YPbPr :绿色、蓝色和红色线缆
-
YUV :亮度和色度,或颜色
-
YIQ :亮度、同相参数、正交
让我们一个一个地看看所有这些颜色模型。
RGB
使用 RGB 颜色空间,红色、绿色和蓝色以不同的方式混合,以形成不同的颜色组合。我们为什么要用 RGB?因为我们的眼睛有颜色感受器,可以相当有效地感知这三种颜色及其组合。
理论上,我们可以从这三种颜色中形成任何颜色。每种颜色的强度定义在 0 到 255 的范围内。这个范围叫做色深。
RGB 色彩空间还有两个分量:
-
白点色度
-
伽马连接曲线
图 2-8 显示了 RGB 颜色空间的文氏图。
图 2-8
RGB 颜色重叠
坐标
RGB 颜色有一个饱和度阈值。他们不能超越我们所能看到的。XYZ 色彩空间帮助我们超越了这个门槛。现在,你可能想知道为什么我们想要超越门槛。嗯,我们人类的眼睛可能无法感知某些颜色,但在数字世界中,你可能需要使用这些颜色。比如 XYZ 可以用来配色;我们可以输入一个颜色代码,然后在不同的应用中复制它,例如打印。利用 XYZ,我们可以对现实世界中存在的所有颜色进行编码。这个色彩空间被称为 XYZ ,因为它在三维空间中外推 RGB 颜色:x、y 和 z。图 2-9 呈现了图像的 XYZ 表示。
图 2-9
XYZ 颜色空间
像素阈值
阈值用于建立条件。比如一个像素强度大于 47,就把它变成黑色或者白色;47 被称为阈值。
推断
如果我们根据某个值与以前值的关系来预测或估计它,我们就是在外推。白色像素的邻居可以是白色的(通过假设或推断)。
HSV/HSL
HSV/HSL 是 RGB 颜色空间的替代表示。它由以下组件组成:
-
顺化(越南城市)
-
浸透
-
价值
-
轻
色调是描述三种颜色的属性:绿色、红色和洋红色。它也可以是两种纯色的混合:红色和黄色,黄色和绿色
饱和度衡量图像的强度。它告诉我们一种颜色离灰色有多远。较低的值意味着颜色接近灰色。
明度是指颜色相对于白色的强度。它告诉我们一种颜色离白色有多远。
价值是强度的另一个衡量标准。它告诉我们一种颜色离黑色有多远。图 2-10 显示了图像的 HSV 表示
图 2-10
HSV 颜色空间
实验室
LAB 色彩空间有三个组成部分:
-
亮度
-
a*,它是绿色和红色分量
-
b*,即蓝色和黄色分量
我们可以感知的颜色和我们不能感知的颜色都包含在 LAB 颜色空间中。人类能够感知一个点,有固定的坐标,以及到一个点的距离。在一起的一个点和到它的距离有个柱坐标。任何没有柱坐标的东西,人类都无法感知。LAB 色彩空间最好的部分是它不依赖于设备;它可以用于印刷、纺织和许多其他应用。LAB 颜色空间是表示颜色的最精确的方法之一。图 2-11 显示了图像的实验室表示。
图 2-11
LAB 色彩空间
组织郎格罕细胞增生症
LCH 类似于 LAB 颜色空间,但它不使用圆柱坐标,而是使用直角坐标。这使得坐标类似于我们人眼所看到的,即不仅基于其位置坐标,还基于与参考点的距离来描述一个点。因此,它对于人眼感知来说是理想的,因为在这种情况下参考点是我们的眼睛。
YPbPr
YPbPr 色彩空间用于视频电子设备,如 DVD 播放器。它由以下三个部分组成:
-
Y :绿色线缆
-
Pb :蓝色线缆
-
Pr :红色线缆
这三个分量仅来自 RGB 颜色空间。y 指亮度;Pb 和 Pr 是两种不同的颜色信号。通常,当使用计算机时,数字颜色分量是从 RGB 颜色空间中导出的。然而,当我们谈论电子设备(如 DVD 播放器)时,我们需要使用 RGB 颜色空间的模拟对应物,即 YPbPr。图 2-12 显示了标准的 YPbPr 电缆。
图 2-12
YPbPr 电缆
YUV
YUV 色彩空间有点类似于 YPbPr,因为两者都在视频电子中使用。区别在于 YUV 也支持黑白电视。
-
Y :图像中存在的亮度。它的值可以从 0 到 255。
-
U 和 V :色度或颜色分量。它的值可以从–128 到+127(对于有符号整数)或从 0 到 255(对于无符号整数)。
如果我们去掉 U 和 V 分量,我们得到一个灰度图像。u 和 V 是颜色矩阵(图 2-13 )。
图 2-13
YUV 颜色空间
YIQ
YIQ 色彩空间(图 2-14 )用于彩色电视(NTSC 模式:国家电视系统委员会)。它由以下三个部分组成:
图 2-14
YIQ 颜色空间
-
Y :图像中的亮度
-
I :同相参数
-
Q :代表颜色信息的正交
高级图像概念
现在,我们已经研究了一些与颜色相关的基本概念,让我们看看与图像处理相关的术语和概念:
-
贝塞尔曲线
-
椭圆体
-
伽马校正
-
结构相似指数
-
反褶积
-
单应性
-
盘旋
贝塞尔曲线
贝塞尔曲线是一条有许多控制点的曲线。控制点是画布上的几个选择点,我们可以用它们来调整曲线。当我们改变控制点的位置时,曲线的形状也会改变,它被用来操纵帧和运动。它还可以用于缩放、选择图像的位置、更改或变换图像的一部分等等。图 2-15 显示了一条普通的贝塞尔曲线。
图 2-15
贝塞尔曲线和控制点
椭圆体
圆是具有恒定直径或半径的二维图形。球体是一个半径或直径不变的三维圆。但是,如果我们拿一个球体,把它的两面压扁,它就变成了一个椭球体。
椭圆体没有恒定的直径。一边直径较大,称为长轴;较小的一侧被称为短轴。图 2-16 显示了一个球体和两个椭球体。
图 2-16
一个球体与两个椭球体的比较
伽马校正
伽玛校正用于在屏幕上精确显示图像,控制图像的亮度,并可用于更改红、绿、蓝的比例。
如果有一个像素我们想要以特定的强度显示(例如 x),而电脑屏幕的 gamma 值为 2.5,那么电脑显示器上的像素强度就变成了 x 2.5 。因为强度总是在 0 到 1 之间测量,所以在这种情况下监视器上的图像变得模糊。
为了消除这个问题,应该对输入值进行伽玛校正。伽马连接完成后,输出几乎与输入相似。例如,如果输入值被提升到 1/2.5 的幂,那么这个过程被称为 2.5 的伽马校正。图 2-17 显示了使用不同伽马值的图像。
图 2-17
使用不同值对图像进行伽马校正
结构相似指数
结构相似性指数,或 SSIM,用于衡量图像的质量。它告诉我们一个图像与另一个图像在结构上有多相似,这意味着我们需要两个图像来执行 SSIM 计算。这里的一个约束是,我们必须知道哪个图像是原始的;否则,该算法无法区分哪个图像比其他图像更好。SSIM 公式是
ssim(x,y)=(2μx【μ】y+c1)×(2σxy+c2)*(μ
其中μ是图像的平均值,σ是图像的标准差,σ 2 是图像的方差。
SSIM(x,y)应该等于 SSIM(y,x)。这就是相似性条件。
反褶积
一般来说,去卷积用于校正模糊的图像,这有助于恢复对比度。对于模糊的图像,很难确定像素强度。为了进行这种校正,我们使用所谓的点扩散函数 (PSF)。我们在图像中选择一个点,使用 PSF,我们可以在三维空间中用光的图案(从该点发出)来表示该点,这有助于使图像更清晰。图 2-18 显示了一张去卷积的月球图像。
图 2-18
月球图像的去卷积
假设我们在恶劣的天气条件下捕捉一幅图像。由于光线条件异常,图像的对比度可能不理想。我们使用对比度恢复来调整图像对比度,以获得更好的图片。在对比度恢复过程中,分析邻近像素,并且还考虑其他参数,如图像的深度、图像的结构等。然后使用它们去卷积定义图像的最佳对比度。
单应性
单应在图像处理中有多种用途:生成马赛克和全景图像、图像拼接、图像配准、图像对齐等等。它用于将图像从一个投影平面转换到另一个投影平面。因此,它可以用来改变图像的平面和视角。除了图像的 x 和 y 坐标(这产生了一个平面的二维图像),增加了第三维:z。图 2-19 显示了应用单应性后的同一点,产生了变化的视角。
图 2-19
改变图像视角的单应性应用
卷积
卷积是一个简单的过程,在这个过程中,我们将一个矩阵(也称为内核或过滤器)应用于图像,以便我们可以缩小它,或者添加几个填充层来保持大小不变。卷积也用于从图像中提取特定特征,如形状、边缘等。卷积在图像处理中有很多应用,特别是在卷积神经网络和人脸检测中。我们将在第六章中详细讨论卷积。
接下来,在第三章中,我们将研究基本的 Python 概念,并通过编写 Python 脚本来实现本章中讨论的一些概念。
三、Python 和 Scikit Image 的基础知识
在不使用编程语言的情况下进行图像处理就像你凝视夜空时数星星一样。有太多复杂的方法,即使我们尝试手动操作,也是不可能的。但是,如果我们使用 Python、R、C++、MATLAB 等编程语言,同样的工作可以在瞬间完成。
问题是,在我们开始应用任何图像处理方法之前,我们应该了解这门语言。本章旨在帮助你实现这两个目标。本章的前半部分讲述了 Python 的基本概念,这些概念在应用图像处理技术时非常有用。本章的后半部分着眼于 Python 的图像处理库:Scikit Learn。我们在前一章学习的所有概念,以及其他一些概念,都可以通过 Scikit Learn 在 Python 中应用。学完这一章,你应该对 Python 概念和基本的图像处理应用感到舒服了。
Python 基础
在本节中,我们将简要介绍以下概念:
-
变量和数据类型
-
数据结构
-
控制流语句
-
条件语句
-
功能
变量和数据类型
关于 Python,我们需要了解的第一件事是如何保存数据,以及数据应该以何种格式保存。我们需要将我们的成像变量设置为一个容器,在其中存储数据。如果我们不使用变量,我们也许能够进行计算,但是我们将不能保存我们的输出。让我们看一个例子:
name = 'Saurav'
age = 20
height = 6.5
在本例中,name
、age
和height
分别是存储 Saurav、20 和 6.5 值的变量。命名变量时,我们需要遵循几条规则:
-
变量名必须以字母或下划线开头。
-
变量名的其余部分可以由字母、数字和下划线组成。
-
名称区分大小写。
-
我们不能使用 Python 内置名称。
现在我们需要看看数据类型。在前面的例子中,我们存储了三种数据:文本,用单引号括起来;一个整数和一个带小数的值。Python 自动知道引号内的任何东西都是字符串,不带小数的是 Int,带小数的是 Float。这是 Python 中的三种数据类型。
现在我们知道了如何在变量中保存一个值,我们可能需要打印它。打印可以按如下方式进行
name = 'Saurav'
age = 20
height = 6.5
print(name,age,height)
Output: 20,6.5
或者作为
name = 'Saurav'
age = 20
height = 6.5
print(name)
print(age)
print(height)
Output:
Saurav
20
6.5
或者作为
name = 'Saurav'
age = 20
height = 6.5
print("the name is", name)
print("the age is", age)
print("the height is", height)
Output:
the name is Saurav'
the age is 20
the height is 6.5
另一种打印方式是使用连接器:
name = 'Saurav'
age = 20
height = 6.5
print("My name is %s. My age and height is %d, %f" %(name, age, height))
Output:
My name is Saurav'. My age and height is 20, 6.5
上例中,%s
代表字符串,其中“s”代表字符串;%d
代表 Int,其中“d”代表数字或整数;而%f
代表小数,其中“f”代表浮点数。因此,第一个连接器连接到第一个变量,第二个连接到第二个,第三个连接到第三个。一切都用“%
”(图 3-1 )连接。
图 3-1
连接器如何工作
数据结构
在上一节中,我们看到了如何在变量中保存一个值。但是,如果我们想要保存多个值,那么我们必须使用 Python 数据结构,包括以下内容:
-
列表
-
字典
-
元组
这些结构是图像处理中最广泛使用的结构。
列表
我们可以使用列表在一个变量中存储多个值。举个例子,
Age = [24,35,26,42]
Names = ["Sachin","Saurav","Rahul"]
如您所见,列表总是以方括号开始和结束。
字典
字典是键和值的组合。就像普通的字典有单词和意思一样,你可以把键和值看作单词和意思。举个例子,
Details ={"Sachin":24, "Saurav":35, "Rahul":42}
字典总是以花括号开始和结束。此外,键和值由冒号分隔。冒号前的第一个元素是键;冒号后的元素是值。
元组
元组也以类似于列表的方式存储值。区别在于元组是不可变的——这意味着,一旦定义了元组,就不能修改值。元组以括号开始和结束。举个例子,
Height = (6.5, 5.4, 5.11)
控制流语句
有两种控制流语句:
-
一个循环
-
一个循环
如果我们想多次重复一个特定的操作,我们使用控制流语句。假设我们想要生成一个两个乘法表。让我们看看如何使用一个while
循环和一个for
循环来实现这一点。
count = 1
while count<=10:
table = 2*count
count = count+1
print table
Output:
2
4
6
8
10
12
14
16
18
20
让我们检查一下while
循环的语法。图 3-2 描述了一个while
回路的功能。
图 3-2
一个while
循环如何工作
我们的代码片段中的循环将运行十次,因为我们插入了小于或等于十的条件。当计数达到 11 时,条件失败,我们退出循环。
现在让我们看看如何使用for
循环来解决同样的问题。
for i in range(10):
table=2*(i+1)
print(table)
Output:
2
4
6
8
10
12
14
16
18
20
图 3-3 描述了for
回路的功能。
图 3-3
一个for
循环如何工作
我们示例中的for
循环将运行十次,因为我们已经规定了 10 的范围。当 I 的值等于 10 时,循环停止。
当我们需要基于条件的循环时,我们使用while
循环;当我们有预定义的数字时,我们使用for
循环。这两个循环在图像处理领域都非常重要,您将在后面看到。
条件语句
条件语句用于根据您提供的条件给出二进制结果。表 3-1 中讨论的所有条件均可用于以下示例。如果结果为 true 或 1,则执行条件语句中的代码块;否则,它不会。条件语句可以是以下类型:
表 3-1
条件运算符
|条件
|
意为
|
| --- | --- |
| a == b
| 检查 a 是否等于 b |
| a != b
| 检查 a 是否不等于 b |
| a < b
| 检查 a 是否小于 b |
| a <= b
| 检查 a 是否小于等于 b |
| a > b
| 检查 a 是否大于 b |
| a >= b
| 检查 a 是否大于等于 b |
-
if
-
if-else
-
if-elif-else
让我们用一个例子来看看这三者。假设我们想给某次考试 80 分以上的学生打 A,给 60 分以上 80 分以下的学生打 B,给 59 分以下的学生打 C。代码如下:
marks = 45
if marks >= 80:
print("You got A Grade")
elif marks >=60 and marks <80:
print("You got B Grade")
else:
print("You got C Grade")
Output:
You got C Grade
这段代码一个接一个地执行。首先,变量marks
被赋值为 45。然后遇到第一个条件语句。如果marks
的值大于或等于 80,则分配 A 级。如果不是这种情况,就会遇到elif
语句,它检查第二个条件。如果没有一个条件是真的,学生被分配一个 c。
功能
当您希望将复杂的代码封装在一个包装器中,然后多次使用该包装器而无需反复编写代码时,可以使用函数。这就像我们专门用一个罐子来装糖,每当我们想取出糖时,我们只使用那个罐子,而不是你放糖、盐、蔬菜和零食的袋子。
图 3-4 简单描述了功能。函数可以将一个或多个值作为输入,如 I1、I2、I3 等(In),并将一个或多个结果作为输出(O)。
图 3-4
函数如何工作
让我们基于下面的例子来看看如何使用函数。到目前为止,我们已经使用for
和while
循环生成了两个乘法表,但是假设我们想要生成一个我们想要的任意数字的表呢?下面是实现这一点的代码:
def table (a):
for i in range(10):
table = a*(i+1)
print(table)
该函数将a
作为输入,并生成一个包含a
存储的任何值的表。让我们看看如何调用函数。假设我们想生成一个 10 和 17 的乘法表。我们称这个函数为:
table(10)
Output:
10
20
30
40
50
60
70
80
90
100
table(17)
Output:
17
34
51
68
85
102
119
136
153
170
现在您已经了解了 Python 的基础知识,让我们继续讨论 Scikit Image。
Scikit Image
Scikit Image 是一个用于进行基本图像处理的模块。在开始之前,让我们看一下模块的定义。模块是 Python 文件、类或函数的集合。我们可以将复杂冗长的代码保存在不同的文件中。为此,我们需要导入文件并在我们的环境中使用它们。首先,我们需要将 Scikit 映像导入到我们的环境中,如下所示:
import skimage
这一行代码导入了进行基本图像分析所需的类和函数的完整集合。
我们可以使用 Scikit Image 应用我们在第二章中看到的所有概念。
在本节中,我们将了解使用 Scikit Image 和 Python 的以下操作:
-
上传和查看图像
-
获取图像分辨率
-
查看像素值
-
转换色彩空间
-
保存图像
-
创建基本绘图
-
进行伽马校正
-
旋转、移动和缩放图像
-
确定结构相似性
上传和查看图像
让我们看看如何将图像导入 Python 环境并在那里查看。我们从导入一个名为skimage
的模块开始,这个模块包含了不同的图像处理算法。为了上传和查看图像,我们使用了来自skimage
模块的一个名为io
的类。在这个类内部,我们使用imread
函数来上传和读取一个图像;功能imshow
用于查看图像。让我们看一下代码。
from skimage import io
img = io.imread('puppy.jpg')
io.imshow(img)
Output:
获取图像分辨率
为了获得图像的分辨率,我们使用一个名为shape
的内置函数。当读取一幅图像时,所有的像素值都以数组格式存储;这个数组叫做 numpy 数组。在我们读取图像并将其转换为数组后,我们使用shape
函数来查看分辨率。
在下面的代码中,您可以看到我们有一个分辨率为 1536 × 2048 的图像,它有三个通道(因为它是 RGB 颜色格式)。
#Getting Image Resolution
from skimage import io
img = io.imread('puppy.jpg')
img.shape
Output:
(1536, 2048, 3)
查看像素值
既然我们知道了图像的分辨率,我们可能想要查看每个像素值。为此,我们将 numpy 数组保存在一行中,换句话说,我们使用一行来存储所有的像素值。当你看下面的代码时,你可以看到我们正在导入另一个名为pandas
的模块。Pandas 用于读取、写入和处理各种文件格式。这里,我们以 Excel 格式保存像素值:
#Getting Pixel Values
from skimage import io
import pandas as pd
img = io.imread('puppy.jpg')
df = pd.DataFrame(img.flatten())
filepath = 'pixel_values1.xlsx'
df.to_excel(filepath, index=False)
当我们查看导入行—import pandas as pd
—这意味着我们将导入的模块重命名为pd
。flatten
功能用于将 RGB 图像的三维转换为一维。然后,我们将该图像保存在名为pixel_values.xlsx
的 excel 文件中。为此,我们使用名为to_excel
的熊猫函数。DataFrame
函数将一维数组转换成类似 Excel 的格式,包含行和列。可以打印df
变量来查看数据帧结构。
转换色彩空间
假设我们的图像在 RGB 颜色空间。我们可能希望将其转换成不同的颜色格式,如第二章所述。在这一节中,我们看看不同的转换,然后将图像转换回其原始的 RGB 格式。
在查看代码之前,我们必须检查我们将使用的函数。为了将图像转换成不同的颜色格式,我们需要使用类color
,它存在于skimage
模块中。在这个类中,我们可以使用以下函数:
-
rgb2hsv
-
hsv2rgb
-
rgb2xyz
-
xyz2rgb
-
rgb2grey
-
grey2rgb
-
rgb2yuv
-
yuv2rgb
-
rgb2lab
-
lab2rgb
-
rgb2yiq
-
yiq2rgb
-
rgb2ypbpr
-
ypbpr2rgb
另外,我们还需要使用一个模块,叫做pylab
。我们通过使用*
来导入pylab
中的所有类。我们用pylab
来看不同区块的不同图形。然后我们使用函数figure
一次显示多个图像。现在让我们看看所有的代码及其输出。
RGB 到 HSV,反之亦然
#Import libraries
from skimage import io
from skimage import color
from skimage import data
from pylab import *
#Read image
img = io.imread('puppy.jpg')
#Convert to HSV
img_hsv = color.rgb2hsv(img)
#Convert back to RGB
img_rgb = color.hsv2rgb(img_hsv)
#Show both figures
figure(0)
io.imshow(img_hsv)
figure(1)
io.imshow(img_rgb)
Output:
RGB 到 XYZ,反之亦然
#Import libraries
from skimage import io
from skimage import color
from skimage import data
#Read image
img = io.imread('puppy.jpg')
#Convert to XYZ
img_xyz = color.rgb2xyz(img)
#Convert back to RGB
img_rgb = color.xyz2rgb(img_xyz)
#Show both figures
figure(0)
io.imshow(img_xyz)
figure(1)
io.imshow(img_rgb)
Output:
RGB 到 LAB,反之亦然
#Import libraries
from skimage import io
from skimage import color
#Read image
img = io.imread('puppy.jpg')
#Convert to LAB
img_lab = color.rgb2lab(img)
#Convert back to RGB
img_rgb = color.lab2rgb(img_lab)
#Show both figures
figure(0)
io.imshow(img_lab)
figure(1)
io.imshow(img_rgb)
Output:
RGB 到 YUV,反之亦然
#Import libraries
from skimage import io
from skimage import color
#Read image
img = io.imread('puppy.jpg')
#Convert to YUV
img_yuv = color.rgb2yuv(img)
#Convert back to RGB
img_rgb = color.yuv2rgb(img_yuv)
#Show both figures
figure(0)
io.imshow(img_yuv)
figure(1)
io.imshow(img_rgb)
Output:
RGB 到 YIQ,反之亦然
#Import libraries
from skimage import io
from skimage import color
#Read image
img = io.imread('puppy.jpg')
#Convert to YIQ
img_yiq = color.rgb2yiq(img)
#Convert back to RGB
img_rgb = color.yiq2rgb(img_yiq)
#Show both figures
figure(0)
io.imshow(img_yiq)
figure(1)
io.imshow(img_rgb)
Output:
RGB 到 YPbPr,反之亦然
#Import libraries
from skimage import io
from skimage import color
#Read image
img = io.imread('puppy.jpg')
#Convert to YPbPr
img_ypbpr= color.rgb2ypbpr(img)
#Convert back to RGB
img_rgb= color.ypbpr2rgb(img_ypbpr)
#Show both figures
figure(0)
io.imshow(img_ypbpr)
figure(1)
io.imshow(img_rgb)
Output:
保存图像
每次图像分析后,我们可能想要保存图像。为此,我们使用名为imsave
的skimage io
函数。在下面的代码中,第一个参数包括要保存图像的文件的名称;第二个是包含图像的变量。
#Import libraries
from skimage import io
from skimage import color
from pylab import *
#Read image
img = io.imread('puppy.jpg')
#Convert to YPbPr
img_ypbpr= color.rgb2ypbpr(img)
#Convert back to RGB
img_rgb= color.ypbpr2rgb(img_ypbpr)
io.imsave("puppy_ypbpr.jpg", img_ypbpr)
创建基本绘图
在一幅图像中,我们可能想要绘制某些图形。这些图形可以是简单的,如直线,也可以是复杂的,如椭球面。我们来看一些使用名为draw
的skimage
绘图类的基本绘图。
线
line
功能用于在图像上绘制简单的线条。在下面的代码中,前两个参数表示第一个点;最后两个参数表示第二点。然后用这些点画一条线。然后我们可以改变线条的像素值,这样我们就可以在图像上看到线条。
from skimage import io
from skimage import draw
img = io.imread('puppy.jpg')
x,y = draw.line(0,0,1000,1000)
img[x, y] = 0
io.imshow(img)
Output:
长方形
为了画矩形,我们使用函数polygon
。我们不仅可以画矩形,还可以画任何我们想要的多边形。我们所要做的就是给出 x 和 y 坐标,然后定义宽度和高度。
在下面的代码中,我使用了函数rectangle
。它返回一个带有我们更改的像素值的形状,就像前面的线条示例一样。
from skimage import io
from skimage import draw
img = io.imread('puppy.jpg')
def rectangle(x, y, w, h):
rr, cc = [x, x + w, x + w, x], [y, y, y + h, y + h]
return (draw.polygon(rr, cc))
rr, cc = rectangle(10, 10, 500,500)
img[rr, cc] = 1
io.imshow(img)
Output:
环
circle
功能用于画圆。在下面的代码中,前两个参数指示圆在图像中的位置;最后一个参数表示半径。
#Import libraries
from skimage import io
from skimage import draw
#Load image
img = io.imread('puppy.jpg')
#Define circle coordinates and radius
x, y = draw.circle(500,500, 100)
#Draw circle
img[x, y] = 1
#Show image
io.imshow(img)
Output:
贝塞尔曲线
为了画一条贝塞尔曲线,我们使用函数bezier_curve. W
e 需要指出三个或更多控制点的位置,然后塑造曲线。以下代码中的前六个参数定义了三点;最后一个参数定义了线中的张力。用不同的值改变曲线。
#Import libraries
from skimage import io
from skimage import draw
#Load image
img = io.imread('puppy.jpg')
#Define Bezier curve coordinates
x, y = draw.bezier_curve(0,0, 500, 500, 900,1200,100)
#Draw Bezier curve
img[x, y] = 1
#Show image
io.imshow(img)
Output:
进行伽马校正
为了执行图像的伽马校正,基于显示仪器,我们使用skimage
中的exposure
类。exposure
类包含一个名为adjust_gamma
的函数,我们用它给出一个图像作为输入和我们想要的最终灰度值。通过这种方式,我们得到了伽马校正的图像。
from skimage import exposure
from skimage import io
from pylab import *
img = io.imread('puppy.jpg')
gamma_corrected1 = exposure.adjust_gamma(img, 0.5)
gamma_corrected2 = exposure.adjust_gamma(img, 5)
figure(0)
io.imshow(gamma_corrected1)
figure(1)
io.imshow(gamma_corrected2)
Output:
旋转、移动和缩放图像
有时我们可能想要旋转图像或改变其大小。为此,我们使用了skimage
模块中的transform
类。transform
有两个功能:rotate
和resize
。rotate
以旋转角度为参数;resize
将新尺寸作为其参数。
from skimage import io
from skimage.transform import rotate
img = io.imread('puppy.jpg')
img_rot = rotate(img, 20)
io.imshow(img_rot)
Output:
from skimage import io
from skimage.transform import resize
img = io.imread('puppy.jpg')
img_res = resize(img, (100,100))
io.imshow(img_res)
io.imsave("ss.jpg", img_res)
Output:
确定结构相似性
正如我前面解释的,结构相似性被用来寻找指示两幅图像相似程度的指数。值越接近 1 意味着图像非常相似;越接近零的值意味着它们越不相似。在下面的代码中,对于相似图像的第一次比较,我们得到的 SSIM 输出为 1.0。在第二段代码中,我们将该图像与其 YPbPr 对应图像进行比较,我们得到的 SSIM 值为 0.43,这表明相似度较低。
from skimage import io
from skimage.measure import compare_ssim as ssim
img_original = io.imread('puppy.jpg')
img_modified = io.imread('puppy_ypbpr.jpg')
ssim_original = ssim(img_original, img_original, data_range=img_original.max() - img_original.min(), multichannel=True)
ssim_different = ssim(img_original, img_modified, data_range=img_modified.max() - img_modified.min(), multichannel=True)
print(ssim_original,ssim_different)
Output:
1.0 0.4348875243670361
SSIM 提出了三个论点。第一指形象;第二个指示像素的范围(最高像素颜色值减去最低像素颜色值)。第三个参数是multichannel
。True 值意味着图像包含多个通道,如 RGB。False 表示只有一个通道,如灰度。
在下一章中,我们将使用名为 OpenCV 的计算机视觉库来研究高级图像处理概念。
四、使用 OpenCV 的高级图像处理
既然我们已经使用 Scikit 图像库研究了基本的图像处理技术,我们可以继续研究它的更高级的方面。在本章中,我们使用最全面的计算机视觉库之一:OpenCV,并研究以下概念:
-
混合两幅图像
-
更改图像的对比度和亮度
-
向图像添加文本
-
平滑图像
-
改变图像的形状
-
实现图像阈值处理
-
计算梯度以检测边缘
-
执行直方图均衡化
混合两幅图像
假设您有两幅图像,并且您想要混合它们,以便两幅图像的特征都可见。我们使用图像配准技术将一个图像混合到第二个图像上,并确定是否有任何变化。让我们看看代码:
#import required packages
import cv2
#Read image 1
img1 = cv2.imread('cat_1.jpg')
#Read image 2
img2 = cv2.imread('cat_2.jpg')
#Define alpha and beta
alpha = 0.30
beta = 0.70
#Blend images
final_image = cv2.addWeighted(img1, alpha, img2, beta, 0.0)
#Show image
io.imshow(final_image)
让我们看看这段代码中使用的一些函数:
-
import cv2
:完整的 OpenCV 库在包 cv2 中。在第一章中,我们学习了如何安装 OpenCV。现在我们需要做的就是导入这个包来使用其中存储的类和函数。 -
cv2.imread()
:类似于skimage.io.imread()
,我们有cv2.imread()
,用于从特定目的地读取图像。 -
cv2.addWeighted()
:此功能融合两幅图像。alpha 和 beta 参数表示两幅图像的透明度。有几个公式有助于确定最终的混合。最后一个参数叫做伽马。目前它的值为零。它只是一个标量,添加到公式中,以便更有效地转换图像。一般来说,伽玛为零。 -
cv2.imshow()
:与skimage.io.imshow()
类似,cv2.imshow()
帮助在新窗口显示图像。 -
cv2.waitKey(): waitKey()
用于显示输出的窗口,直到我们点击关闭或按下退出键。如果我们在cv2.imshow()
后不包括此功能,图像将不会显示。 -
cv2.DestroyAllWindows()
:当我们点击关闭或按下退出键后,该功能会销毁所有已经打开并保存在内存中的窗口。
下面的图片是前面代码的输出:
改变对比度和亮度
要改变图像的对比度和亮度,我们应该了解这两个术语的含义:
-
对比度:对比度是最大和最小像素强度之差。
-
亮度:亮度是指一幅图像的明度或暗度。为了使图像更亮,我们给图像中的所有像素加上一个常数。
让我们看看代码和输出,看看对比度和亮度的区别。
#import required packages
import cv2
import numpy as np
#Read image
image = cv2.imread("cat_1.jpg")
#Create a dummy image that stores different contrast and brightness
new_image = np.zeros(image.shape, image.dtype)
#Brightness and contrast parameters
contrast = 3.0
bright = 2
#Change the contrast and brightness
for y in range(image.shape[0]):
for x in range(image.shape[1]):
for c in range(image.shape[2]):
new_image[y,x,c] = np.clip(contrast*image[y,x,c] + bright, 0, 255)
figure(0)
io.imshow(image)
figure(1)
io.imshow(new_image)
在这段代码中,我们没有使用任何cv2
函数来改变亮度或对比度。我们使用 numpy 库和切片概念来改变参数。我们做的第一件事是定义参数。我们给对比度值 3,亮度值 2。第一个for
循环给出图像宽度,第二个给出图像高度,第三个给出图像通道。因此,第一个循环运行宽度若干次,第二个循环运行高度若干次,最后一个循环运行颜色通道若干次。如果 RGB 图像在那里,那么循环对三个通道运行三次。
np.clip()
将数值限制在特定范围内。在前面的代码中,范围是 0 到 255,这只是每个通道的像素值。于是,推导出一个公式:
(特定像素值×对比度)+亮度。
使用这个公式,我们可以改变每一个像素值,np.clip()
确保输出值不会超过 0 到 255。因此,循环遍历每个通道的每个像素,并执行转换。
以下是输出图像:
向图像添加文本
cv2.putText()
是cv2
模块中的一个功能,允许我们给图像添加文本。该函数采用以下参数:
-
图像,您要在其中写入文本
-
您要编写的文本
-
文本在图像上的位置
-
字体类型
-
字体比例
-
文本的颜色
-
文本的厚度
-
使用的线路类型
在下面的代码中可以看到,使用的字体是FONT_HERSHEY_SIMPLEX
。cv2
还支持以下字体:
-
FONT_HERSHEY_SIMPLEX
-
FONT_HERSHEY_PLAIN
-
FONT_HERSHEY_DUPLEX
-
FONT_HERSHEY_COMPLEX
-
FONT_HERSHEY_TRIPLEX
-
FONT_HERSHEY_COMPLEX_SMALL
-
FONT_HERSHEY_SCRIPT_SIMPLEX
-
FONT_HERSHEY_SCRIPT_COMPLEX
-
FONT_ITALIC
代码中使用的行类型是cv2.LINE_AA
。其他受支持的线条类型有
-
FILLED
:完全填充的线条 -
LINE_4
:四条连线 -
LINE_8
:八条连线 -
LINE_AA
:抗锯齿线
您可以使用所有不同的参数进行实验,并检查结果。让我们看看代码及其输出。
#import required packages
import cv2
import numpy as np
#Read image
image = cv2.imread("cat_1.jpg")
#Define font
font = cv2.FONT_HERSHEY_SIMPLEX
#Write on the image
cv2.putText(image, "I am a Cat", (230, 50), font, 0.8, (0, 255, 0), 2, cv2.LINE_AA)
io.imshow(image)
输出:
平滑图像
在这一节中,我们来看看三个用于平滑图像的过滤器。这些过滤器如下:
-
中值滤波器(
cv2.medianBlur
) -
高斯滤波器(
cv2.GaussianBlur
) -
双边过滤器(
cv2.bilateralFilter
)
中值滤波器
中值滤波器是最基本的图像平滑滤波器之一。这是一种非线性过滤器,通过使用相邻像素找到中值来消除图像中存在的黑白噪声。
为了使用中值滤波器平滑图像,我们查看第一个 3 × 3 矩阵,找到该矩阵的中值,然后通过该中值移除中心值。接下来,我们向右移动一步,重复这个过程,直到所有的像素都被覆盖。最终图像是平滑的图像。如果你想在模糊的同时保留图像的边缘,中值滤波器是你最好的选择。
cv2.medianBlur
是用于实现中值模糊的函数。它有两个参数:
-
我们想要平滑的图像
-
内核大小,应该是奇数。因此,值 9 意味着 9 × 9 矩阵。
高斯滤波器
高斯滤波器取决于图像(分布)的标准偏差,并假设平均值为零(我们也可以定义不同于零的平均值)。高斯滤波器不处理边缘。某些统计参数的值定义了保存。它用于基本的图像模糊。它通常通过定义内核来工作。假设我们定义一个 3 × 3 的核。我们将这个内核应用于图像中的每一个像素,并对结果进行平均,从而得到一个模糊的图像。这里有一个例子:
cv2.GaussianBlur()
是用于应用高斯滤波器的函数。它有三个参数:
-
需要进行模糊处理的图像
-
内核的大小(本例中为 3 × 3)
-
标准偏差
双边过滤器
如果我们想平滑图像并保持边缘完整,我们使用双边滤波器。它的实现很简单:我们用相邻像素的平均值替换像素值。这是一种非线性平滑方法,采用相邻像素的加权平均值。“邻居”的定义如下:
-
两个像素值彼此接近
-
两个像素值彼此相似
cv2.bilateralFilter
有四个参数:
-
我们想要平滑的图像
-
像素邻域的直径(定义邻域直径以搜索邻居)
-
颜色的西格玛值(查找相似的像素)
-
空间的西格玛值(查找更近的像素)
让我们看一下代码:
#import required packages
import cv2
import numpy as np
#Read images for different blurring purposes
image_Original = cv2.imread("cat_1.jpg")
image_MedianBlur = cv2.imread("cat_1.jpg")
image_GaussianBlur = cv2.imread("cat_1.jpg")
image_BilateralBlur = cv2.imread("cat_1.jpg")
#Blur images
image_MedianBlur=cv2.medianBlur(image_MedianBlur,9)
image_GaussianBlur=cv2.GaussianBlur(image_GaussianBlur,(9,9),10)
image_BilateralBlur=cv2.bilateralFilter(image_BilateralBlur,9,100,75)
#Show images
figure(0)
io.imshow(image_Original)
figure(1)
io.imshow(image_MedianBlur)
figure(2)
io.imshow(image_GaussianBlur)
figure(3)
io.imshow(image_BilateralBlur)
输出:
改变图像的形状
在这一节中,我们研究腐蚀和膨胀,这是用来改变图像形状的两种操作。膨胀导致像素添加到对象的边界;侵蚀导致像素从边界上消失。
两个侵蚀或扩大一个图像,我们首先定义邻域内核,这可以通过三种方式来完成:
-
MORPH_RECT
:制作一个长方形的内核 -
MORPH_CROSS
:做十字形的内核 -
做一个椭圆形的内核
内核查找像素的邻居,这有助于我们腐蚀或放大图像。对于膨胀,最大值生成新的像素值。对于侵蚀,内核中的最小值生成新的像素值。
在图 4-1 中,我们应用了一个 3 × 1 矩阵来寻找每行的最小值。对于第一个元素,内核从之前的一个单元格开始。因为左边的新单元格中不存在该值,所以我们将其视为空白。这个概念叫做填充。因此,在无、141 和 157 之间检查第一个最小值。因此,141 是最小值,您会看到 141 是右边矩阵中的第一个值。然后,内核向右移动。现在要考虑的单元格是 141、157 和 65。这一次,65 是最小值,所以新矩阵中的第二个值是 65。第三次,内核比较 157,65,无,因为没有第三个单元格。因此,最小值是 65,这成为最后一个值。对每一个单元格都执行这个操作,你就得到如图 4-1 所示的新矩阵。
图 4-1
扩张
腐蚀操作与膨胀操作类似,只是我们不是找到最小值,而是找到最大值。图 4-2 显示了操作。
图 4-2
侵蚀
与膨胀一样,内核大小是一个 3 × 1 的矩形。cv2.getStructuringElement()
是用于定义内核并将其传递给侵蚀或扩张函数的函数。让我们看看它的参数:
-
侵蚀/扩张型
-
内核大小
-
内核应该启动的点
在应用cv2.getStructuringElement()
并得到最终内核后,我们使用cv2.erode()
和cv2.dilate()
来执行具体的操作。让我们看看代码及其输出:
#DILATION CODE
:
#Import package
import cv2
#Read image
image = cv2.imread("cat_1.jpg")
#Define erosion size
s1 = 0
s2 = 10
s3 = 10
#Define erosion type
t1 = cv2.MORPH_RECT
t2 = cv2.MORPH_CROSS
t3 = cv2.MORPH_ELLIPSE
#Define and save the erosion template
tmp1 = cv2.getStructuringElement(t1, (2*s1 + 1, 2*s1+1), (s1, s1))
tmp2= cv2.getStructuringElement(t2, (2*s2 + 1, 2*s2+1), (s2, s2))
tmp3 = cv2.getStructuringElement(t3, (2*s3 + 1, 2*s3+1), (s3, s3))
#Apply the erosion template to the image and save in different variables
final1 = cv2.erode(image, tmp1)
final2 = cv2.erode(image, tmp2)
final3 = cv2.erode(image, tmp3)
#Show all the images with different erosions
figure(0)
io.imshow(final1)
figure(1)
io.imshow(final2)
figure(2)
io.imshow(final3)
#EROSION CODE
:
#Import packages
import cv2
#Read images
image = cv2.imread("cat_1.jpg")
#Define dilation size
d1 = 0
d2 = 10
d3 = 20
#Define dilation type
t1 = cv2.MORPH_RECT
t2 = cv2.MORPH_CROSS
t3 = cv2.MORPH_ELLIPSE
#Store the dilation templates
tmp1 = cv2.getStructuringElement(t1, (2*d1 + 1, 2*d1+1), (d1, d1))
tmp2 = cv2.getStructuringElement(t2, (2*d2 + 1, 2*d2+1), (d2, d2))
tmp3 = cv2.getStructuringElement(t3, (2*d3 + 1, 2*d3+1), (d3, d3))
#Apply dilation to the images
final1 = cv2.dilate(image, tmp1)
final2 = cv2.dilate(image, tmp2)
final3 = cv2.dilate(image, tmp3)
#Show the images
figure(0)
io.imshow(final1)
figure(1)
io.imshow(final2)
figure(2)
io.imshow(final3)
输出:
实现图像阈值处理
进行图像阈值处理的主要原因是为了分割图像。我们试图通过去除背景和聚焦物体来从图像中去除物体。为此,我们首先将图像转换为灰度,然后转换为二进制格式,这意味着图像只包含黑色或白色。
我们提供一个参考像素值,高于或低于它的所有值都转换为黑色或白色。有五种阈值类型:
-
二进制:如果像素值大于参考像素值(阈值),则转换为白色(255);否则,转换为黑色(0)。
-
二进制反转:如果像素值大于参考像素值(阈值),则转换为黑色(0);否则,转换为白色(255)。与二进制类型正好相反。
-
截断:如果像素值大于参考像素值(阈值),则转换为阈值;否则,不要更改该值。
-
阈值归零:如果像素值大于参考像素值(阈值),则不改变该值;否则转换为黑色(0)。
-
阈值到零反转:如果像素值大于参考像素值(阈值),则转换为黑色(0);否则,不要改变。
我们使用cv2.threshold()
函数进行图像阈值处理,该函数使用以下参数:
-
要转换的图像
-
阈值
-
最大像素值
-
阈值的类型(如前所列)
让我们看看代码及其输出。
#Import packages
import cv2
#Read image
image = cv2.imread("cat_1.jpg")
#Define threshold types
"'
0 - Binary
1 - Binary Inverted
2 - Truncated
3 - Threshold To Zero
4 - Threshold To Zero Inverted
"'
#Apply different thresholds and save in different variables
_, img1 = cv2.threshold(image, 50, 255, 0 )
_, img2 = cv2.threshold(image, 50, 255, 1 )
_, img3 = cv2.threshold(image, 50, 255, 2 )
_, img4 = cv2.threshold(image, 50, 255, 3 )
_, img5 = cv2.threshold(image, 50, 255, 4 )
#Show the different threshold images
figure(0)
io.imshow(img1) #Prints Binary Image
figure(1)
io.imshow(img2) #Prints Binary Inverted Image
figure(2)
io.imshow(img3) #Prints Truncated Image
figure(3)
io.imshow(img4) #Prints Threshold to Zero Image
figure(4)
io.imshow(img5) #Prints Threshold to Zero Inverted Image
输出:
计算渐变
在这一节中,我们来看看使用 Sobel 导数的边缘检测。边缘存在于两个方向:垂直方向和水平方向。利用该算法,我们只强调那些具有非常高的空间频率的区域,这些区域可能对应于边缘。空间频率是一个重要区域的详细程度。
在下面的代码中,我们读取图像,应用高斯模糊以去除噪声,然后将图像转换为灰度。我们使用cv2.cvtColor()
函数将图像转换成灰度。我们也可以使用skimage
函数来做同样的事情。最后,我们把灰度输出给cv2.Sobel()
函数。让我们看看 Sobel 函数的参数:
-
输入图像
-
输出图像的深度。图像的深度越大,错过任何边界的可能性就越小。您可以试验下面列出的所有参数,看看它们是否能根据您的要求有效地捕捉边界。深度可以是以下类型:
-
–1(与原始图像的深度相同)
-
cv2.CV_16S
-
cv2.CV_32F
-
cv2.CV_64F
-
-
导数 x 的阶(定义寻找水平边缘的导数阶)
-
导数 y 的阶(定义寻找垂直边的导数阶)
-
内核的大小
-
应用于导数的比例因子
-
要作为标量添加到公式中的增量值
-
像素外推的边框类型
cv2.convertScaleAbs()
函数用于将值转换成一个绝对数字,采用无符号 8 位类型。然后,我们混合 x 和 y 梯度,我们发现找到图像中的整体边缘。
让我们看看代码及其输出。
#Import packages
import cv2
#Read image
src = cv2.imread("cat_1.jpg")
#Apply gaussian blur
cv2.GaussianBlur(src, (3, 3), 0)
#Convert image to grayscale
gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)
#Apply Sobel method to the grayscale image
grad_x = cv2.Sobel(gray, cv2.CV_16S, 1, 0, ksize=3, scale=1, delta=0, borderType=cv2.BORDER_DEFAULT) #Horizontal Sobel Derivation
grad_y = cv2.Sobel(gray, cv2.CV_16S, 0, 1, ksize=3, scale=1, delta=0, borderType=cv2.BORDER_DEFAULT) #Vertical Sobel Derivation
abs_grad_x = cv2.convertScaleAbs(grad_x)
abs_grad_y = cv2.convertScaleAbs(grad_y)
grad = cv2.addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0) #Apply both
#Show the image
io.imshow(grad)#View the image
输出:
执行直方图均衡化
直方图均衡化用于调整图像的对比度。我们首先绘制像素强度分布的直方图,然后修改它。每个图像都有一个累积概率函数。直方图均衡化给出了该函数的线性趋势。我们应该使用灰度图像来执行直方图均衡化。
cv2.equalizeHist()
功能用于直方图均衡化。让我们看一个例子。
#Import packages
import cv2
#Read image
src = cv2.imread("cat_1.jpg")
#Convert to grayscale
src = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)
#Apply equalize histogram
src_eqlzd = cv2.equalizeHist(src) #Performs Histogram Equalization
#Show both images
figure(0)
io.imshow(src)
figure(1)
io.imshow(src_eqlzd)
figure(2)
io.imshow(src_eqlzd)
输出:
现在我们知道了使用skimage
的基本图像处理算法,以及使用 OpenCV 的一些高级操作。在下一章中,我们继续前进,应用机器学习算法进行图像处理。
五、使用机器学习的图像处理
本章开始时,我们将研究一些最广泛使用的图像处理算法,然后继续讨论图像处理中的机器学习实现。这一章的内容如下:
-
使用尺度不变特征变换(SIFT)算法的特征映射
-
基于随机样本一致性算法的图像配准
-
基于人工神经网络的图像分类
-
基于卷积神经网络的图像分类
-
使用机器学习的图像分类
-
重要术语
使用 SIFT 算法的特征映射
假设我们有两幅图像。一张图片是公园里的长椅。第二个图像是整个公园,也包括长凳。现在假设我们想要编写代码来帮助我们找到公园图像中的长椅。你可能认为这是一个简单的任务,但是让我增加一些复杂性。如果长凳的图像是放大的图像怎么办?或者是旋转了呢?还是两者都有?你现在打算怎么处理?
答案在于尺度不变特征变换,或 SIFT 算法。顾名思义,它是比例不变的,这意味着无论我们将图像放大(或缩小)多少,我们仍然可以找到相似之处。这种算法的另一个特点是它是旋转不变的。无论旋转角度如何,它仍然表现良好。这个算法的唯一问题是它是有专利的,这意味着对于学术目的来说它是好的,但是对于商业目的来说,使用它可能会涉及很多法律问题。然而,这不会阻止我们现在学习和应用这个算法。
我们首先必须了解算法的基础。然后,我们可以使用 Python 来应用它来寻找两幅图像之间的相似之处,然后我们将逐行查看代码。
让我们看看 SIFT 算法在处理过程中试图提取的图像特征:
-
缩放(放大或缩小的图像)
-
循环
-
照明
-
远景
如你所见,SIFT 算法不仅考虑了缩放和旋转,还考虑了图像中的光照和我们观看的视角。但是它是如何做到这一切的呢?让我们来看看使用 SIFT 算法的一步一步的过程:
-
寻找并构建一个空间以确保尺度不变性
-
找出高斯人之间的区别
-
找到图像中的重点
-
删除不重要的点,以进行有效的比较
-
为步骤 3 中发现的要点提供方向
-
独特地识别关键特征。
第一步:空间建设
在第一步中,我们获取原始图像并执行高斯模糊,以便我们可以移除图像中的一些不重要的点和额外的噪声。完成后,我们调整图像的大小并重复这个过程。调整大小和模糊取决于各种因素,但我们不会在这里进入数学细节。
第二步:高斯人之间的差异
在第二步中,我们获取第一步中的图像,并找出它们的值之间的差异。这使得图像比例不变。
第三步:重点
在第三步中,我们确定重要点(也称为关键点)。我们在步骤 3 中发现的高斯图像之间的差异用于确定局部最大值和最小值。我们取每个像素,然后检查它的邻居。如果该像素在其所有邻居中最大(最大)或最小(最小),则该像素被标记为关键点。
下一步是找到子像素最大值和/或最小值。我们使用一种叫做泰勒展开的数学概念来寻找子像素。当找到子像素时,我们使用相同的过程再次尝试找到最大值和最小值。此外,为了只将拐角作为关键点,我们使用了一个叫做海森矩阵的数学概念。角落总是被认为是最好的关键点。
第四步:不重要的要点
在这一步中,我们首先确定一个阈值。在关键点生成的图像和亚像素图像中,我们用阈值检查像素强度。如果它小于阈值,我们认为它是一个不重要的关键点,拒绝它。
步骤 5:关键点的定位
我们找到每个关键点及其相邻点的梯度方向和幅度,然后我们查看关键点周围最普遍的方向,并将其分配给它。我们使用直方图来寻找这些方向并得到最终的方向。
第六步:关键特征
为了使关键点独一无二,我们从中提取关键特征。此外,我们确保在将这些关键点与第二幅图像进行比较时,它们看起来不应该完全相似,而是几乎相似。
现在我们知道了算法的基础,让我们来看看算法应用于一对图像的代码。
import cv2
import numpy as np
import matplotlib.pyplot as plt
from Sift_Operations import *
print("'Make Sure that both the images are in the same folder"')
x = input("Enter First Image Name: ")
Image1 = cv2.imread(x)
y = input("Enter Second Image Name: ")
Image2 = cv2.imread(y)
Image1_gray = cv2.cvtColor(Image1, cv2.COLOR_BGR2GRAY)
Image2_gray = cv2.cvtColor(Image2, cv2.COLOR_BGR2GRAY)
Image1_key_points, Image1_descriptors = extract_sift_features(Image1_gray)
Image2_key_points, Image2_descriptors = extract_sift_features(Image2_gray)
print( 'Displaying SIFT Features')
showing_sift_features(Image1_gray, Image1, Image1_key_points);
norm = cv2.NORM_L2
bruteForce = cv2.BFMatcher(norm)
matches = bruteForce.match(Image1_descriptors, Image2_descriptors)
matches = sorted(matches, key = lambda match:match.distance)
matched_img = cv2.drawMatches(
Image1, Image1_key_points,
Image2, Image2_key_points,
matches[:100], Image2.copy())
plt.figure(figsize=(100,300))
plt.imshow(matched_img)
上面的代码将整个 SIFT 算法应用于一对图像。但是,该算法保存在一个名为Sift_Operations.py
的 Python 文件中,与该代码位于同一目录下。让我们看看里面的代码。
import cv2
import numpy as np
import matplotlib.pyplot as plt
def extract_sift_features(img):
sift_initialize = cv2.xfeatures2d.SIFT_create()
key_points, descriptors = sift_initialize.detectAndCompute(img, None)
return key_points, descriptors
def showing_sift_features(img1, img2, key_points):
return plt.imshow(cv2.drawKeypoints(img1, key_points, img2.copy()))
现在让我们检查代码,根据需要从一个文件跳到另一个文件:
- 接下来,我们将图像转换为灰度。SIFT 需要灰度图像来执行其操作。我们使用 OpenCV 函数
cv2.cvtColor
进行颜色格式转换。
图 5-1
原始图像
-
在主代码中,我们导入了重要的库:OpenCV、Numpy、Matplotlib 和自定义模块
Sift_Operations
。import *
表示导入 Python 文件中存在的所有内容。 -
接下来,我们读取两幅图像,我们必须对其应用 SIFT 操作。图 5-1 显示了我导入的图像。
-
现在我们将这两个图像传递给函数
extract_sift_features
,它存储在文件Sift_Operations.py
中。该函数返回在图像中找到的关键点,以及这些点的特征和描述符的名称。让我们从内部来看看这个函数:sift_initialize = cv2.xfeatures2d.SIFT_create()
-
前面一行代码将整个 SIFT 存储在变量
sift_initialize
中。 -
detectAndCompute
方法用于将算法应用于图像,w 返回关键点和描述符:key_points, descriptors = sift_initialize.detectAndCompute(img, None)
-
然后返回这些值:
return key_points, descriptors
-
回到调用代码,这些值存储在特定于图像的不同变量中:
-
Image1_gray = cv2.cvtColor(Image1, cv2.COLOR_BGR2GRAY)
Image2_gray = cv2.cvtColor(Image2, cv2.COLOR_BGR2GRAY)
-
接下来,初始化
cv2.BFMatcher
函数。它用于寻找关键点的描述符之间的匹配。然后将norm
变量作为参数传递。它告诉BFMatcher
使用曼哈顿距离来执行匹配。初始化的算法保存在一个名为bruteForce
的变量中。 -
将两个描述符进行匹配
bruteForce.match
,然后根据曼哈顿距离对匹配进行排序:matches = bruteForce.match(Image1_descriptors, Image2_descriptors) matches = sorted(matches, key = lambda match:match.distance)
-
T
两幅图像的关键点基于前 100 个排序匹配进行连接:matched_img = cv2.drawMatches( Image1, Image1_key_points, Image2, Image2_key_points, matches[:100], Image2.copy())
-
最后,显示匹配的图像:
plt.figure(figsize=(100,300)) plt.imshow(matched_img)
图 5-2
曼哈顿距离
-
然后显示这些特征,以便我们可以看到关键点和相似之处。方法
showing_sift_features
就是用来做这件事的。 -
让我们从内部来看这个方法。
cv2.drawKeypoints
用于绘制两幅图像中发现的关键点。 -
变量
norm
随后被初始化并用于寻找关键点之间的距离。cv2.Norm_L2
用于计算曼哈顿距离(图 5-2 ),曼哈顿距离是沿直角-90 度轴测量的两点之间的距离。不是直线距离;它遵循网格方法。
Image1_key_points, Image1_descriptors = extract_sift_features(Image1_gray)
Image2_key_points, Image2_descriptors = extract_sift_features(Image2_gray)
整个代码的输出如图 5-3 所示。
图 5-3
使用 SIFT 算法找到的相似性
使用 RANSAC 算法的图像配准
假设我们有一个地方的两张鸟瞰图。一张图片使用卫星描绘了这个地方,而第二张图片使用无人机展示了同一张图片的一部分。卫星图像以年为单位进行更新,而无人机图像的拍摄频率要高得多。因此,可能会出现无人机图像捕捉到卫星图像中看不到的发展的情况。在这种情况下,我们可能希望将无人机图像放在与卫星图像完全相同的位置,但也显示最新的更新。这种将一幅图像叠加在另一幅图像上的过程被称为图像配准。
RANSAC 是用于图像配准的最佳算法之一,它包括四个步骤:
-
特征检测和提取
-
特征匹配
-
转换函数拟合
-
图像变换和图像重采样
在第三步中使用 RANSAC 算法来寻找变换函数。我们拍摄两幅图像,然后使用 RANSAC 算法,找到这些图像之间的单应性(相似性)。让我们简单地看一下算法:
-
随机找到两幅图像的四个共同特征点,然后找到单应矩阵*。
-
重复这个步骤多次,直到我们得到一个具有最大内联数的单应矩阵
让我们用 Python 来应用这个算法。代码由三个自定义模块组成:Ransac.py
、Affine.py
和Align.py
。ransac
包含整个 RANSAC 算法,Affine
用于对图像进行旋转、平移和缩放操作。Align
用于对齐图像,使其完全对准原始图像。
让我们一行一行地看代码:
-
首先,我们导入重要的库以及刚刚提到的定制模块。
import numpy as np Import cv2 from Ransac import * from Affine import * from Align import *
-
然后,我们上传想要在第二个图像(目标图像)上注册的图像。接下来,我们上传目标图像。
img_source = cv2.imread("source.jpg") img_target = cv2.imread("target.jpg")
-
现在,我们使用存储在
Align
模块中的函数extract_SIFT
来提取关键点和相关描述符(我在上一节解释了这段代码)。keypoint_source, descriptor_source = extract_SIFT(img_source) keypoint_target, descriptor_target = extract_SIFT(img_target)
-
接下来,我们使用函数
match_SIFT
获得上一步中找到的所有点的位置:pos = match_SIFT(descriptor_source, descriptor_target)
-
在
match_SIFT
方法中,我们试图在所有匹配的描述符中获得最佳的两个匹配。为此,我们使用函数BFMatcher
和knnMatch
。让我们来看看这段代码,它保存在Align
模块:bf = cv2.BFMatcher() matches = bf.knnMatch(descriptor_source, descriptor_target, k=2)
中
-
我们必须创建一个空的 numpy 数组来存储关键点的位置。姑且称之为
pos
。根据 D. Lowe 的比率测试,我们只将那些比率小于或等于 0.8 的点放入pos
中(参见本章末尾的重要术语)。for i in range(matches_num): if matches[i][0].distance <= 0.8 * matches[i][1].distance: temp = np.array([matches[i][0].queryIdx, matches[i][0].trainIdx]) pos = np.vstack((pos, temp))
-
trainIdx
返回描述符在源中的索引,而queryIdx
返回描述符在目标中的索引。这些是我们在pos
变量中垂直堆栈的实际位置,然后返回它。return pos
-
现在我们有了描述符的位置,我们使用
Align
模块中的函数affine_matrix
来获得单应矩阵,我们在图像配准中使用它。H = affine_matrix(keypoint_source, keypoint_target, pos)
-
让我们看看函数的内部:
-
首先,我们基于存储在
pos
变量中的最佳描述符位置,将所有关键点存储在s
和t
变量中。s = s[:, pos[:, 0]] t = t[:, pos[:, 1]]
-
Then we need to find the inliers. Inliers are the points in the two images that show maximum similarity, and hence can be used to draw RANSAC models. We use the function
ransac_fit
, stored inransac
module to get these key points._, _, inliers = ransac_fit(s, t)
Inside
ransac_fit
, we initialize a few basic variables: the number of inliers, the matrices required to do affine transformation, and a variable that stores the position of the inliers:inliers_num = 0 A = None t = None inliers = None
Next, we need to find temporary matrices that help us do affine transformation. For this, we generate indices randomly for our points, then extract points from those indices to pass as a parameter to the function
estimate_affine
.idx = np.random.randint(0, pts_s.shape[1], (K, 1)) A_tmp, t_tmp = estimate_affine(pts_s[:, idx], pts_t[:, idx])
Now that we have these temporary matrices, we pass them to the function
residual_lengths
, which calculates the error, which helps us decide the final matrix.residual = residual_lengths(A_tmp, t_tmp, pts_s, pts_t)
以下章节给出了对功能
residual_lengths
和estimate_affine
的解释。既然我们知道了残差/误差,我们就用阈值来检查它。我们已经指定了一个阈值限制。如果残差小于阈值,那么我们计算这些实例的数量。inliers_tmp = np.where(residual < threshold)
We then compare the inliers having residual with total inliers defined (which is zero). If the residual inliers are greater than the predefined inliers, we update the predefined inlier with the new inlier value and then update the affine transformation matrices with the temporary matrices A and t. Also, we store the indices of those inliers.
inliers_tmp = np.where(residual < threshold) inliers_num_tmp = len(inliers_tmp[0]) if inliers_num_tmp > inliers_num: inliers_num = inliers_num_tmp inliers = inliers_tmp A = A_tmp t = t_tmp
We repeat this process 2,000 times to get the best possible matrices, then return them.
for i in range(ITER_NUM=2000)r return A, t, inliers
-
既然我们有了内联数字,我们就用它们来提取最佳源关键点和目标关键点。
s = s[:, inliers[0]] t = t[:, inliers[0]]
-
我们使用这些关键点,并将它们发送给
estimate_affine
函数,它给出了我们最终的转换矩阵。A, t = estimate_affine(s, t)
-
最后,我们将矩阵水平堆叠,并将其作为一个矩阵返回:单应矩阵。
M = np.hstack((A, t)) return M
-
-
现在我们有了单应矩阵,剩下要做的就是图像配准。为此,我们首先从目标图像中提取行数和列数:
```py
rows, cols, _ = img_target.shape
```
- 然后,将使用我们的源图像,应用单应矩阵,并将其缩放到目标图像的行和高度:
```py
warp = cv2.warpAffine(img_source, H, (cols, rows))
```
- 现在我们把两幅图像混合在一起。为此,我们给目标图像 50%的权重,给扭曲图像 50%的权重
- 现在,我们要做的就是出示登记证。我们还可以根据自己的要求保存图像。
```py
cv2.imshow('img', merge)
cv2.waitKey(0)
cv2.destroyAllWindows()
```
估计 _ 仿射
estimate_affine
函数将源图像和目标图像的关键点总数作为输入,并将仿射变换矩阵作为输出返回。基于源关键点的维数,我们初始化一个虚拟矩阵,然后用源关键点填充它以用作循环。然后我们取目标关键点,在找到它的转置后,将维数整形为 2000×1。最后,我们对这两个矩阵进行线性回归,得到直线的斜率和截距。利用这些,我们计算出最终的矩阵 X 和 y。代码如下:
def estimate_affine
(s, t):
num = s.shape[1]
M = np.zeros((2 * num, 6))
for i in range(num):
temp = [[s[0, i], s[1, i], 0, 0, 1, 0],
[0, 0, s[0, i], s[1, i], 0, 1]]
M[2 * i: 2 * i + 2, :] = np.array(temp)
b = t.T.reshape((2 * num, 1))
theta = np.linalg.lstsq(M, b)[0]
X = theta[:4].reshape((2, 2))
Y = theta[4:]
return X, Y
剩余长度
residual_lengths
函数用于确定我们的模型中存在的误差,并确保我们生成的仿射矩阵或我们匹配的描述符具有尽可能少的误差。首先,我们在仿射矩阵和源关键点之间建立一个线性模型,它给出了目标图像的估计点。我们将它们与实际目标点进行比较,以确定最终误差。然后,我们用这些估计的点减去目标点,取它们的平方,然后求平方根以消除负值的影响。这是均方根误差估计,或残差估计。最后,我们返回值。该操作的代码如下:
def residual_lengths(X, Y, s, t):
e = np.dot(X, s) + Y
diff_square = np.power(e - t, 2)
residual = np.sqrt(np.sum(diff_square, axis=0))
return residual
处理图像
让我们看看我们的目标图像(图 5-4 ):
图 5-4
目标图像
完整的代码
以下是图像注册的完整代码:
主要代码:
import numpy as np
import cv2
from Ransac import *
from Affine import *
from Align import *
img_source = cv2.imread("2.jpg")
img_target = cv2.imread("target.jpg")
keypoint_source, descriptor_source = extract_SIFT(img_source)
keypoint_target, descriptor_target = extract_SIFT(img_target)
pos = match_SIFT(descriptor_source, descriptor_target)
H = affine_matrix(keypoint_source, keypoint_target, pos)
rows, cols, _ = img_target.shape
warp = cv2.warpAffine(img_source, H, (cols, rows))
merge = np.uint8(img_target * 0.5 + warp * 0.5)
cv2.imshow('img', merge)
cv2.waitKey(0)
cv2.destroyAllWindows()
兰萨奇. py:
import numpy as np
from Affine import *
K=3
threshold=1
ITER_NUM = 2000
def residual_lengths(X, Y, s, t):
e = np.dot(X, s) + Y
diff_square = np.power(e - t, 2)
residual = np.sqrt(np.sum(diff_square, axis=0))
return residual
def ransac_fit(pts_s, pts_t):
inliers_num = 0
A = None
t = None
inliers = None
for i in range(ITER_NUM):
idx = np.random.randint(0, pts_s.shape[1], (K, 1))
A_tmp, t_tmp = estimate_affine(pts_s[:, idx], pts_t[:, idx])
residual = residual_lengths(A_tmp, t_tmp, pts_s, pts_t)
if not(residual is None):
inliers_tmp = np.where(residual < threshold)
inliers_num_tmp = len(inliers_tmp[0])
if inliers_num_tmp > inliers_num:
inliers_num = inliers_num_tmp
inliers = inliers_tmp
A = A_tmp
t = t_tmp
else:
pass
return A, t, inliers
Affine.py:
import numpy as np
def estimate_affine(s, t):
num = s.shape[1]
M = np.zeros((2 * num, 6))
for i in range(num):
temp = [[s[0, i], s[1, i], 0, 0, 1, 0],
[0, 0, s[0, i], s[1, i], 0, 1]]
M[2 * i: 2 * i + 2, :] = np.array(temp)
b = t.T.reshape((2 * num, 1))
theta = np.linalg.lstsq(M, b)[0]
X = theta[:4].reshape((2, 2))
Y = theta[4:]
return X, Y
Align.py:
import numpy as np
from Ransac import *
import cv2
from Affine import *
def extract_SIFT(img):
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
sift = cv2.xfeatures2d.SIFT_create()
kp, desc = sift.detectAndCompute(img_gray, None)
kp = np.array([p.pt for p in kp]).T
return kp, desc
def match_SIFT(descriptor_source, descriptor_target):
bf = cv2.BFMatcher()
matches = bf.knnMatch(descriptor_source, descriptor_target, k=2)
pos = np.array([], dtype=np.int32).reshape((0, 2))
matches_num = len(matches)
for i in range(matches_num):
if matches[i][0].distance <= 0.8 * matches[i][1].distance:
temp = np.array([matches[i][0].queryIdx,
matches[i][0].trainIdx])
pos = np.vstack((pos, temp))
return pos
def affine_matrix(s, t, pos):
s = s[:, pos[:, 0]]
t = t[:, pos[:, 1]]
_, _, inliers = ransac_fit(s, t)
s = s[:, inliers[0]]
t = t[:, inliers[0]]
A, t = estimate_affine(s, t)
M = np.hstack((A, t))
return M
基于人工神经网络的图像分类
在对一组图像应用人工神经网络进行分类之前,让我们首先检查什么是神经网络。例如,当我们受伤时会发生什么?一个信号被立即发送到我们的大脑,然后大脑根据信号的强度做出反应。信号的传递是通过神经元进行的。神经元以突触的形式将信号传递给另一个神经元,这个过程一直持续到信号到达大脑。神经元的结构如图 5-5 所示。
图 5-5
生物神经元
图 5-5 中对我们很重要的信息包括树突和轴突。树突接收来自另一个神经元的信号,轴突将信号传递给下一个神经元。这个链条止于最后一个节点,就是大脑。
人工神经网络使用相同的类比,并使用人工神经元处理信息。信息从一个人工神经元转移到另一个人工神经元,最终导致激活功能,它像大脑一样工作并做出决定。简单人工神经网络的结构如图 5-6 所示。
图 5-6
简单人工神经网络
所以要处理的信息存储在输入节点中。接下来是隐藏层,在这里信息被实际处理。最后,是输出。激活函数在隐藏层和输出层之间。我们称该层为隐藏层,因为我们看不到隐藏层内部发生了什么。在人工神经网络结构中可以有一个或多个隐藏层。隐藏层数越多,网络越深(相对于浅网络)。
现在,如果我们深入研究神经网络的完整细节,这本书将变得非常长,我们将偏离我们的主要主题:图像处理。因此,与其详细研究神经网络,我建议你自己去研究它们。也就是说,现在让我们继续将人工神经网络应用于手写识别。我强烈建议你不要在没有很好的神经网络知识基础的情况下继续学习。
修改后的国家标准和技术研究所(MNIST)数据库包括一个数据集,该数据集包含大约 60,000 幅训练图像和 10,000 幅手写数字测试图像。我们将使用训练数据集来训练我们的神经网络,然后我们将使用测试数据集来查看其准确性。最后,你可以给出你自己的手写数字来检验我们训练好的模型的预测。
首先,让我们看一个如何处理神经网络的流程图(图 5-7 )。
图 5-7
流程图
首先,我们必须从 MNIST 数据库下载训练和测试数据集。我们可以从 kaggle 网站( https://www.kaggle.com/c/digit-recognizer/data#
)下载。
下载完数据后,我们需要将数据上传到我们的 Python 环境中。
import pandas as pd
input_data = pd.read_csv("train.csv")
现在让我们来看看我们的训练集。它由 785 列组成。每幅图像的分辨率为 28 × 28。因此,784 列包含每个数字的像素值。最后一个表示由像素值表示的实际数字。让我们看看图像和数据集的预览(图 5-8 )。
图 5-8
MNIST 数据集
我们必须用 Python 创建两个数据框。一个将存储所有像素值 X;另一个将存储实际的数字 y。
y = input_data['label']
input_data.drop('label', axis=1, inplace = True)
X = input_data
现在我们将y
中的标签转换成虚拟标签(见重要术语)。
y = pd.get_dummies(y)
现在我们有了 X 和 y 的数据,我们可以从神经网络开始。让我们使用 Keras 创建四个隐藏层,一个输入层和一个输出层。
classifier = Sequential()
classifier.add(Dense(units = 600, kernel_initializer = 'uniform', activation = 'relu', input_dim = 784))
classifier.add(Dense(units = 400, kernel_initializer = 'uniform', activation = 'relu'))
classifier.add(Dense(units = 200, kernel_initializer = 'uniform', activation = 'relu'))
classifier.add(Dense(units = 10, kernel_initializer = 'uniform', activation = 'sigmoid'))
第一个隐藏层有 600 个神经元,第二个有 400 个,第三个有 200 个,最后一个有 10 个神经元。然后,我们通过给定kernel_initializer = 'uniform'
来初始化规范化格式的参数w
和b
。在前三层,我们给出激活函数relu
;最后一层包含了sigmoid
函数。此外,第一层包含 784 的输入维度。我们的网络现在看起来如图 5-9 所示。
图 5-9
深度神经网络
注意
在图 5-9 中,每一个节点都连接到下一层的每一个节点。
现在我们需要计算随机梯度下降算法*来最小化损失:
classifier.compile(optimizer = 'sgd', loss = 'mean_squared_error', metrics = ['accuracy'])
最后,我们通过给出 10 的批量和 10 的时期来开始训练:
classifier.fit(X_train, y_train, batch_size = 10, epochs = 10)
这给了我们 98.95 的准确度。让我们看看输出:
最后,我们在测试数据集上进行预测。首先我们上传它,然后我们做预测:
test_data = pd.read_csv("test.csv")
y_pred = classifier.predict(test_data)
所有的预测都保存在变量y_pred
中。
让我们看看完整的代码:
import pandas as pd
import keras
from keras.models import Sequential
from keras.layers import Dense
input_data = pd.read_csv("train.csv")
y = input_data['label']
input_data.drop('label',axis=1,inplace = True)
X = input_data
y = pd.get_dummies(y)
classifier = Sequential()
classifier.add(Dense(units = 600, kernel_initializer = 'uniform', activation = 'relu', input_dim = 784))
classifier.add(Dense(units = 400, kernel_initializer = 'uniform', activation = 'relu'))
classifier.add(Dense(units = 200, kernel_initializer = 'uniform', activation = 'relu'))
classifier.add(Dense(units = 10, kernel_initializer = 'uniform', activation = 'sigmoid'))
classifier.compile(optimizer = 'sgd', loss = 'mean_squared_error', metrics = ['accuracy'])
classifier.fit(X, y, batch_size = 10, epochs = 10)
test_data = pd.read_csv("test.csv")
y_pred = classifier.predict(test_data)
基于细胞神经网络的图像分类
CNN 用于图像处理和分类问题。在上一节中,我们看到了如何使用人工神经网络并将其应用于 MNIST 数据集。在这一节中,我们来看一下 CNN 及其在同一数据集上的应用。
对于 CNN,除了正常的神经网络层之外,还有一些额外的层。在上一节中,我们看到每个节点都连接到下一层的每个节点。这可能会非常耗时,还会导致过度拟合*的问题。CNN 被用来纠正这个问题。有了有线电视新闻网,我们就不必连接到每一个节点。对于 CNN,我们应用选择性过滤。图 5-10 显示了一个简单 CNN 的基本结构。
图 5-10
CNN 的摘要
简要总结一下 CNN 是如何工作的,从卷积层开始,我们将一个过滤器(也称为内核)应用于输入图像。这个内核一个块一个块地跨越图像,其中每个块都是像素单元的集合。在这个过程中,我们执行矩阵乘法,这导致了较低分辨率的图像。在二次采样层(也称为下采样层)中,我们找到平均像素值(称为平均池)或最大像素值(称为最大池),并获得更低分辨率的图像。最后,输出连接到全连接层,其中每个最大池输出连接到全连接层中的每个节点。在这之后,接着是神经网络架构。关于 CNN 如何工作的详细解释,请参见吴恩达在 Coursera 上的课程( https://www.coursera.org/learn/neural-networks-deep-learning
)。
让我们看看如何将 CNN 应用于 MNIST 数据集。首先,我们创建一个名为load_and_preprocess and
的 Python 文件,将其导入到我们的代码中进行数据预处理。然后我们找到训练和测试数据集。在这段代码中,我没有使用 Kaggle 提供的测试数据集;相反,我将向您展示如何将数据分成训练数据集和测试数据集,然后检查测试数据集的准确性。首先让我们分析一下load_and_preprocess
模块的代码。
-
首先,我们定义图像的维度。数据集中的每个图像都是 28 × 28。我们将在变量
r
和c
:r, c = 28, 28
中保存像素行数和像素列数
-
然后,我们使用 0 到 9 的标签来定义类的数量,这意味着总共有十个类:
num_classes = 10
-
在
keras
模块中,我们有整个 MNIST 数据集。因此,我们没有使用从 Kaggle 下载的.csv
工作表,而是直接使用来自 Keras 的数据集:from keras.datasets import mnist
-
接下来,我们通过调用 Keras 中名为
load_data()
:(x_train, y_train), (x_test, y_test) = mnist.load_data()
的内置方法,从中提取训练集和测试集
-
然后我们重塑 numpy 的功能。目前,数据是没有适当结构的数组格式。使用
reshape
,我们给出数据结构。我们通过告诉 Python 以这样一种方式转换数组来做到这一点,即它只在一列中包含所有的像素值:x_train = x_train.reshape(x_train.shape[0], r, c, 1) x_test = x_test.reshape(x_test.shape[0], r, c, 1)
-
目前
x_train
和x_test
的数据类型为 Int(整数)。我们想把它转换成浮点数,这样我们就可以很容易地对它进行预处理:x_train = x_train.astype('float32') x_test = x_test.astype('float32')
-
现在我们需要进行规范化。我们用最高像素强度值 255 来划分每个像素,以便数据产生 0 到 1 的较低范围。这有助于有效地训练模型。
x_train /= 255 x_test /= 255
-
既然我们已经处理了自变量,我们需要处理因变量,它们是实际的数字标签。为此,我们将当前为整数格式的值转换为分类值*:
y_train = keras.utils.to_categorical(y_train, num_classes) y_test = keras.utils.to_categorical(y_test, num_classes)
-
最后,我们将所有处理过的数据返回到我们的原始代码:
return (x_train,x_test,y_train,y_test,input_shape)
让我们看看完整的代码:
from keras.datasets import mnist
import keras
def load_and_preprocess():
r, c = 28, 28
num_classes = 10
x_train, y_train, x_test, y_test = mnist.load_data()
x_train = x_train.reshape(x_train.shape[0], r, c, 1)
x_test = x_test.reshape(x_test.shape[0], r, c, 1)
input_shape = (r, c, 1)
x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
x_train /= 255
x_test /= 255
# convert class vectors to binary class matrices
y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)
return (x_train,x_test,y_train,y_test,input_shape)
回到我们的主代码,我们现在将声明我们的卷积和子采样层:
model = Sequential()
model.add(Conv2D(32, kernel_size=(3, 3), activation="relu", input_shape=input_shape))
model.add(Conv2D(64, (3, 3), activation="relu"))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(128, activation="relu"))
model.add(Dropout(0.5))
model.add(Dense(num_classes, activation="softmax"))
在前面的代码中,我们使用函数Conv2D
定义了两个卷积层。第二个的输出被提供给子采样层。Dropout
用于使我们的训练避免过度拟合。它的值介于 0 和 1 之间,我们可以尝试不同的值来找到更好的准确性。Dense
功能有助于给出relu
或 softmax(参见重要术语)激活功能的输出。
接下来,我们通过使用 AdaDelta 算法将误差降至最低(参见重要术语):
model.compile(loss=keras.losses.categorical_crossentropy, optimizer=keras.optimizers.Adadelta(), metrics=['accuracy'])
最后,我们以 128 的批量和 12 的时期开始训练。我们给出了参数验证数据,因此它可以将训练好的模型应用于测试数据集,并允许我们查看其准确性。
model.fit(x_train, y_train, batch_size=128, epochs=12, validation_data=(x_test, y_test))
为了打印准确性,我们使用以下代码:
score = model.evaluate(x_test, y_test, verbose=0)
print('Test loss:', score[0])
print('Test accuracy:', score[1])
让我们看看完整的代码。输出如图 5-11 所示。
主代码
import keras
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras.layers import Conv2D, MaxPooling2D
from Load_and_Preprocess import *
x_train,x_test,y_train,y_test, input_shape = load_and_preprocess()
num_classes=10
model = Sequential()
model.add(Conv2D(32, kernel_size=(3, 3),activation='relu',input_shape=input_shape))
model.add(Conv2D(64, (3, 3), activation="relu"))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(128, activation="relu"))
model.add(Dropout(0.5))
model.add(Dense(num_classes, activation="softmax"))
model.compile(loss=keras.losses.categorical_crossentropy,
optimizer=keras.optimizers.Adadelta(),
metrics=['accuracy'])
model.fit(x_train, y_train,
batch_size=128,
epochs=12,
validation_data=(x_test, y_test))
score = model.evaluate(x_test, y_test, verbose=0)
print('Test loss:', score[0])
print('Test accuracy:', score[1])
加载和预处理
from keras.datasets import mnist
import keras
def load_and_preprocess():
r, c = 28, 28
num_classes = 10
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train = x_train.reshape(x_train.shape[0], r, c, 1)
x_test = x_test.reshape(x_test.shape[0], r, c, 1)
input_shape = (r, c, 1)
x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
x_train /= 255
x_test /= 255
y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)
return (x_train,x_test,y_train,y_test,input_shape)
输出
图 5-11
代码输出
使用机器学习方法的图像分类
在本节中,我们将探讨三种著名的机器学习算法的应用:
-
决策树
-
支持向量机
-
逻辑回归
首先,让我们检查这三个算法的基础。然后我们将它们应用于 MNIST 数据集,如前所述,这是一个庞大的手写数字数据库。我们使用这个数据集进行手写识别。
决策树
当我们想要在生活中做一个重大决定时,我们会做正反两方面的分析。决策树类似于这种方法。基于某个统计阈值,我们确定某个特定的事物是属于一类还是另一类。假设我们想知道一个人是印度人还是外国人。第一个门槛可能是看肤色。下一个阈值可能是语音音调。另一个门槛可能是体格。在应用了所有这些阈值之后,我们制作了一个帮助我们确定那个人所属类别的树。我们不研究统计细节,但与决策树相关的一些重要术语如下:
-
节点:树中的一个块
-
纯节点:包含单个类元素的节点,如外国人
-
纯度:一个节点中同类元素的程度
-
熵:用于确定阈值的统计方法
-
信息增益:两级节点的熵之差;用于决定何时停止生成树
支持向量机
支持向量机使用数学平面(最大间隔超平面)的概念来区分多个类别。因此,使用我们之前的例子,支持向量机在两个类之间画一个平面——在我们的例子中,是印度人和外国人。我们试图最大化这个平面与两个类的距离,这就是为什么它被称为最大边缘超平面。为了构建这个超平面,我们使用支持向量的概念,支持向量是每个类的最外面的点。在线性分类的情况下(见重要术语),此边距直接绘制。但是,当涉及到非线性分类时,支持向量机使用核技巧将非线性转换为线性,然后找到超平面。
逻辑回归
逻辑回归是机器学习中最著名的算法之一。它是线性回归的一种改进形式,其中我们使用 logits 来确定元素属于特定类的概率。它给出了 0 到 1 之间的输出。如果输出大于 0.5,则该元素被认为属于一个类;否则,它属于另一个。我们还可以画一条曲线来测试我们的模型的效率。
密码
现在我们已经知道了所有三种算法的基础,让我们将它们应用到我们的数据集。第一步是使用 pandas 库读取数据集,并将其存储在变量data
中:
import pandas as pd
data = pd.read_csv("train.csv")
接下来,我们找到因变量和自变量。我们将因变量存储在变量y
中,将自变量存储在X
中:
y = data['label']
data.drop('label',axis=1,inplace = True)
X = data
y = pd.Categorical(y)
这样做之后,我们导入三个算法,它们保存在sklearn
模块中:
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.svm import LinearSVC
导入库后,我们必须创建它们的一个实例:
logreg = LogisticRegression()
dt = DecisionTreeClassifier()
svc = LinearSVC()
现在我们的整个算法存储在三个独立的变量中:logreg
、dt
和svc
。接下来,我们要训练我们的模型。我们调用fit
函数,它直接开始训练:
model_logreg = logreg.fit(X,y)
model_dt = dt.fit(X,y)
model_svc = svc.fit(X,y)
将训练好的模型保存在变量中后,我们尝试预测测试数据集中出现的新值:
X_test = pd.read_csv("test.csv")
pred_logreg = model_logreg.predict(X_test)
pred_dt = model_logreg.predict(X_test)
pred_svc = model_logreg.predict(X_test)
我们还可以在训练数据集上检查训练模型的准确性:
from sklearn.accuracy import accuracy_score
pred1 = model_logreg.predict(X)
pred2 = model_dt.predict(X)
pred3 = model_svc.predict(X)
print("Decision Tree Accuracy is: ", accuracy_score(pred1, y)*100)
print("Logistic Regression Accuracy is: ", accuracy_score(pred2, y)*100)
print("Support Vector Machine Accuracy is: ", accuracy_score(pred3, y)*100)
让我们看看完整的代码和输出:
import pandas as pd
data = pd.read_csv("train.csv")
y = data['label']
data.drop('label',axis=1,inplace = True)
X = data
y = pd.Categorical(y)
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.svm import LinearSVC
logreg = LogisticRegression()
dt = DecisionTreeClassifier()
svc = LinearSVC()
model_logreg = logreg.fit(X,y)
model_dt = dt.fit(X,y)
model_svc = svc.fit(X,y)
X_test = pd.read_csv("test.csv")
pred_logreg = model_logreg.predict(X_test)
pred_dt = model_dt.predict(X_test)
pred_svc = model_svc.predict(X_test)
from sklearn.accuracy import accuracy_score
pred1 = model_logreg.predict(X)
pred2 = model_dt.predict(X)
pred3 = model_svc.predict(X)
print("Decision Tree Accuracy is: ", accuracy_score(pred_dt, y)*100)
print("Logistic Regression Accuracy is: ", accuracy_score(pred_logreg, y)*100)
print("Support Vector Machine Accuracy is: ", accuracy_score(pred_svc, y)*100)
输出:
Decision Tree Accuracy is: 100.0
Logistic Regression Accuracy is: 93.8547619047619
Support Vector Machine Accuracy is: 88.26190476190476
重要术语
-
AdaDelta 算法梯度下降算法的替代算法;模型从数据集自动学习,而无需预先定义学习速率(在梯度下降算法中是强制性的);有助于消除过度配合和配合不足的问题
-
批处理大小在模型中一次传递一部分数据(批处理),直到数据集结束;在所有批次被处理之后,结果是一个时期
-
非数字的分类值标签(例如,猫、狗、高、中、低);与在 MNIST 数据集中使用相反,在那里标注是数字,这使得预测更加有效
-
因变量我们实际预测的一个元素
-
当我们将分类变量转换为分配给每个类别的二进制数时
-
时期模型最小化误差所需的步骤数
-
梯度下降算法用于基于反向传播和微分概念最小化误差;该模型从该算法中学习数据集中存在的所有重要特征,这有助于它进行有效的预测
-
单应矩阵映射两幅图像的一种方式,以找到它们之间的共同模式;用于图像配准
-
自变量用于预测因变量的元素
-
线性分类一种基于直线分叉的元素分类方法
-
过拟合当模型考虑到所有特征,包括不必要的特征;给出错误的结果
-
【Lowe 博士的比率测试用于确定从图像中提取的特征是否可用于寻找它们之间的相似性的测试
-
一个激活函数,用于在模型完成训练后进行分类;有助于确定元素属于哪个类别;具有介于 0 和 1 之间的值
-
测试集数据集的一部分,我们在其中测试模型的效率
-
训练集数据集的一部分,用于训练和制作模型
-
欠拟合当一个模型不能照顾到所有的重要特征,从而给出错误的结果
六、实时用例
现在,我们已经了解了图像处理的基础和高级概念,是时候看看一些实时用例了。在本章中,我们将了解五种不同的概念证明,它们可以根据您自己的要求进行调整:
-
寻找掌纹
-
检测人脸
-
识别面孔
-
跟踪运动
-
检测车道
寻找掌纹
我们将使用 Python 和 OpenCV 库来确定我们手掌中的主要掌纹。首先,我们需要阅读原始图像:
import cv2
image = cv2.imread("palm.jpg")
cv2.imshow("palm",image) #to view the palm in python
cv2.waitKey(0)
现在我们将图像转换为灰度:
gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
然后利用滤波算法 Canny 边缘检测器找到掌纹。对于不同的图像,我们需要相应地改变参数。
edges = cv2.Canny(gray,40,55,apertureSize = 3)
cv2.imshow("edges in palm",edges)
cv2.waitKey(0)
现在我们恢复颜色,使识别的线条变成黑色:
edges = cv2.bitwise_not(edges)
输出:
接下来,我们将之前的图像与原始图像混合:
cv2.imwrite("palmlines.jpg", edges)
palmlines = cv2.imread("palmlines.jpg")
img = cv2.addWeighted(palmlines, 0.3, image, 0.7, 0)
最终输出:
我们可以改变参数以获得更有效的输出。
检测人脸
在本节中,我们将面部识别代码应用于包含一张脸的图像,然后将相同的代码应用于包含多张脸的图像。我们必须做的第一件事是导入重要的库:
import cv2
import matplotlib.pyplot as plt
接下来,我们阅读包含一张脸的图像。读取后,我们将其转换为灰度,然后在新窗口中显示:
img1 = cv2.imread("single_face.jpg")
gray_img = cv2.cvtColor(img11, cv2.COLOR_BGR2GRAY)
cv2.imshow("Original_grayscale_image",gray_img)
cv2.waitKey(0)
现在我们需要在图像上应用哈尔级联。我们有几个 Haar 级联来检测 OpenCV 项目中已经存储的多种东西。为了方便起见,我在本书的 sharepoint 中附加了所需的级联,以 XML 文件格式存储。以下是级联列表:
在我们的例子中,我们将使用haarcascade_frontalface_alt.xml
:
haar_face_cascade = cv2.CascadeClassifier('haarcascade_frontalface_alt.xml')
faces = haar_face_cascade.detectMultiScale(gray_img, scaleFactor=1.1, minNeighbors=5)
for (x, y, w, h) in faces:
cv2.rectangle(img1, (x, y), (x+w, y+h), (0, 255, 0), 2)
前面的代码将级联算法加载到一个变量中。然后,使用该算法,它试图检测面部,并在检测到的面部上画一个圆。scaleFactor
用来照顾大小脸。如果你靠近镜头,你看起来脸很大;否则,它会显得更小。minNeighbors
查看矩形内检测到的人脸,并决定包含什么和拒绝什么。现在,让我们展示检测到的图像:
cv2.imshow("Final_detected_image",cv2.COLOR_BGR2RGB(img1))
cv2.waitKey(0)
前面几行代码给出了以下输出:
如果我们使用另一个包含多个面的图像,而不是一个面,代码会给出以下输出:
识别面孔
我们已经成功地检测出图像中的人脸,但是我们如何识别哪张脸属于谁呢?为了解决这个问题,我们将使用高级 OpenCV 方法。
第一步是检测人脸。我们在方法detect_face()
中加入了与上一节完全相同的代码:
def detect_face(img):
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
face_cascade = cv2.CascadeClassifier('haarcascade_frontalface_alt.xml')
faces = face_cascade.detectMultiScale(gray, scaleFactor=1.2, minNeighbors=5)
(x, y, w, h) = faces[0]
return gray[y:y+w, x:x+h], faces[0]
代码返回坐标,而不是在脸部周围画一个矩形。
下一步是提供足够的数据,这样系统就可以知道多张脸属于一个特定的人。下一次,它将能够从新的图像中识别出这个人。
def prepare_training_data(data_folder_path):
dirs = os.listdir(data_folder_path)
faces = []
labels = []
for dir_name in dirs:
if not dir_name.startswith("s"):
continue
label = int(dir_name.replace("s", ""))
subject_dir_path = data_folder_path + "/" + dir_name
subject_images_names = os.listdir(subject_dir_path)
for image_name in subject_images_names:
image_path = subject_dir_path + "/" + image_name
image = cv2.imread(image_path)
face, rect = detect_face(image)
if face is not None:
faces.append(face)
labels.append(label)
cv2.waitKey(0)
cv2.destroyAllWindows()
return faces, labels
前面的代码首先读取特定文件夹中的每张图片,然后尝试检测人脸并将人脸坐标存储在列表faces[]
和labels[]
中。在这段代码中,这个人的名字应该是文件夹名,这个人的所有图像都应该保存在文件夹中。上面的函数返回所有的面部坐标和标签,这在以后有助于训练数据。
接下来是训练部分。为此,我们将使用LBPHFaceRecognizer
函数。让我们将训练函数应用于面部和标签:
face_recognizer = cv2.face.LBPHFaceRecognizer_create()
face_recognizer.train(faces, np.array(labels))
这段代码训练模型,查看面部坐标和标签。
接下来我们要做的是预测。假设我们试着识别两张脸。我拍了兰维尔和萨钦·坦杜尔卡尔的照片。该模型已经在这些图像上进行了训练,它将尝试预测新的图像。
subjects = ["", "Sachin Tendulkar", "Ranveer"]
img = "ranveer.jpg"
face, rect = detect_face(img)
label= face_recognizer.predict(face)[0]
label_text = subjects[label]
(x, y, w, h) = rect
cv2.rectangle(img, (x, y), (x+w, y+h), (0, 255, 0), 2)
cv2.putText(img, label_text, (rect[0], rect[1]-5), cv2.FONT_HERSHEY_PLAIN, 1.5, (0, 255, 0), 2)
这段代码读取图像,并试图预测图像是否是 Ranveer。让我们看看这段代码的输出。整个代码都在 sharepoint 中。
跟踪运动
假设你有一支笔,但你不是在纸上写字,而是在空气中写字,这样东西就会自动写出来。听起来像魔术吗?嗯,使用先进的图像处理方法可以做到这一点。
假设我有一个蓝色笔尖的制笔器。我想让它在空中移动,然后用相机跟踪蓝色笔尖,在屏幕上画出完全相同的动作。让我们看看如何实现这一点。
首先,我们必须只捕捉蓝色笔尖。这意味着所有其他的东西——手、背景等等——都只是噪音。我们必须去除噪声,我们通过腐蚀和膨胀来达到这个目的。让我们看看代码:
mask=cv2.inRange(hsv,Lower_green,Upper_green)
mask = cv2.erode(mask, kernel, iterations=2)
mask=cv2.morphologyEx(mask,cv2.MORPH_OPEN,kernel)
mask = cv2.dilate(mask, kernel, iterations=1)
res=cv2.bitwise_and(img,img,mask=mask)
cnts,heir=cv2.findContours(mask.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[-2:]
接下来,我们必须定义我们想要的蓝色范围:
Lower_green = np.array([110,50,50])
Upper_green = np.array([130,255,255])
现在我们需要跟踪运动。为了做到这一点,我们找到图像的轮廓,然后找到图像的矩,这些矩在以后用来画线。要了解有关图像矩和轮廓的更多信息,请参考附录。
if len(cnts) > 0:
c = max(cnts, key=cv2.contourArea)
((x, y), radius) = cv2.minEnclosingCircle©
M = cv2.moments©
center = (int(M["m10"] / M["m00"]), int(M["m01"] / M["m00"]))
if radius > 5:
cv2.circle(img, (int(x), int(y)), int(radius),(0, 255, 255), 2)
cv2.circle(img, center, 5, (0,0,255), -1)
pts.appendleft(center)
for i in range(1,len(pts)):
if pts[i-1]is None or pts[i] is None:
continue
thick = int(np.sqrt(len(pts) / float(i + 1)) * 2.5)
cv2.line(img, pts[i-1],pts[i],(0,0,248),thick)
最后,我们的代码准备好了。让我们看看我们的代码是否跟踪笔。
cv2.imshow("Frame", img)
cv2.imshow("mask",mask)
cv2.imshow("res",res)
我们得到以下输出:
检测车道
我们都知道,自动驾驶汽车是当今汽车行业最大的新闻人物之一。汽车知道什么时候向左转,什么时候停下来,如何阅读交通标志,等等。在本节中,我们将学习汽车如何在高速公路上观察车道并理解其含义,从而定义其边界,换句话说,就是不离开车道。如果你想成为自动驾驶汽车编程专家,有一个 Udacity 的 nanodegree 项目可以参加。
首先,我们需要校准我们的相机。因为整个自动驾驶汽车概念取决于摄像头的精度,我们必须校准它。为此,我们在 OpenCV 中使用一个名为findChessboardCorners(), which (as you might imagine)
的函数来寻找给定棋盘图像的内角。
def convert3D_to_2D(path, x, y):
rwp = np.zeros((y*x, 3), np.float32)
tmp = np.mgrid[0:x, 0:y].T.reshape(-1, 2)
rwp[:,:2] = tmp
rwpoints = []
imgpoints = []
images = glob.glob(path)
for fname in images:
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
corner_found, corners = cv2.findChessboardCorners(gray, (x,y), None)
if corner_found == True:
rwpoints.append(rwp)
imgpoints.append(corners)
cv2.drawChessboardCorners(img, (x,y), corners, corner_found)
return (rwpoints, imgpoints)
Sharepoint 中提供了完整的逐行代码解释。前面的代码读取一个目录中存在的不同棋盘图像,找到它们的内角,并将最终的点保存在两个列表中:rwpoints
和imgpoints
。rwpoints
在三维空间中包含真实的空间点;imgpoints
点是二维的。该函数返回两个列表。
接下来,我们使用这两个列表来校准相机,然后对图像进行去失真。不失真意味着去除噪声和平滑图像。简单来说,我们说我们把图像从三维转换成二维。让我们看看代码:
def calibrate_camera(test_img_path, rwpoints, imgpoints):
img = mpimg.imread(test_img_path)
img_size = (img.shape[1], img.shape[0])
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(rwpoints, imgpoints, img_size, None, None)
undst_img = cv2.undistort(img, mtx, dist, None, mtx)
f, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 10))
ax1.set_title("Original Image")
ax1.imshow(img)
ax2.set_title("Undistorted Image")
ax2.imshow(undst_img)
return (mtx, dist)
calibrateCamera()
功能首先试图通过使用rwpoints
和imgpoints
列表来提高相机的效率。它给了我们两个重要的矩阵——mtx
和dst
——帮助我们不失真的形象。未失真的输出如下所示:
因为无失真效果很好,所以让我们将同样的方法应用于道路图像:
def undistort_test_img(mtx, dist):
test_image = mpimg.imread("road.jpg")
undistorted_img = cv2.undistort(test_image, mtx, dist, None, mtx)
f, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 10))
ax1.set_title("Original Image")
ax1.imshow(test_image)
ax2.set_title("Undistorted Image")
ax2.imshow(undistorted_img)
将上面的代码应用于测试图像后,我们得到以下输出:
现在我们有了二维图像(未失真的图像),我们需要执行透视变换。透视,通俗地说,就是你看一个特定事物的角度和方向。所以两个人看一个特定的东西总是有不同的视角。
在自动驾驶汽车的情况下,摄像头总是看着道路线。这些道路线从来都不是恒定的,因此它们的视角一直在变化。为了确保相机的视角始终保持不变,我们使用了透视变换。
我们需要做的第一件事是在车道上选择四个点,这在透视变换期间引导我们。我们随机选择这些点。我对这些点进行了硬编码,使它们与图像中的精确位置相匹配。
src = np.float32([
[203, 720],
[585, 460],
[695, 460],
[1127, 720]])
dst = np.float32([
[270, 720],
[310, 0],
[960, 0],
[1010, 720]])
现在我们把这两点给函数getPerspectiveTransform()
求透视变换矩阵和逆透视变换矩阵:
M = cv2.getPerspectiveTransform(src, dst)
Minv = cv2.getPerspectiveTransform(dst, src)
我们使用这些矩阵进行透视变换,然后再返回到我们的原始图像。首先,让我们看看如何进行转换:
test_image = mpimg.imread("road.jpg")
img_size = (test_image.shape[1], test_image.shape[0])
undistorted_img = cv2.undistort(test_image, mtx, dist, None, mtx)
i = draw_polygon(undistorted_img)
warped = cv2.warpPerspective(undistorted_img, M, img_size)
f, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 10))
ax1.set_title("Original Image")
ax1.imshow(i)
ax2.set_title("Undistorted Warped Image")
ax2.imshow(warped)
当我们运行这段代码时,我们得到以下输出:
最后,我们得到了转换后的图像。我们将图像转换为二进制格式,这样相机就能够找到并理解道路线。为此,我们使用 Sobel 变换。这种边缘检测算法有助于我们跟踪道路上的线条。
所以,如前所述,我们不失真并改变图像的视角,然后将其转换为二值图像(黑白的组合)。之后,我们应用进一步的变换。
img = cv2.undistort(test_image, mtx, dist, None, mtx)
color_binary, edges_img = find_edges(img)
img_size = (edges_img.shape[1], edges_img.shape[0])
warped_img = cv2.warpPerspective(edges_img, M, img_size, flags=cv2.INTER_LINEAR)
为了找到边缘,我们创建了一个名为find_edges
的函数,用于检测边缘,并获得包含这些边缘的彩色和二值图像。然后,我们应用warpPerspective()
函数,使用矩阵M
对生成的图像进行透视变换。
当我们将前面的代码应用于原始图像时,我们得到以下输出:
最后,我们将创建的图像包装到原始图像上。只有这样,我们才能确定摄像头是否正确检测到了道路。为此,我们使用第二个矩阵:Minv
。
我们再次使用函数warpPerspective()
,但是用Minv
代替M
:
newwarp = cv2.warpPerspective(color_warp, Minv, (warped_img.shape[1], warped_img.shape[0])
color_warp
用于创建包含代码检测到的道路线的图像,并用颜色填充它们。我们通过使用Minv
来保留我们最初的视角。Sharepoint 中给出了代码的详细解释。让我们看看输出:
所以,我们已经成功地检测到了道路线。汽车已经探测到它必须行驶的道路和路径。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库